Files
wukuang/packages/lowcode-create-v2/TECHNICAL_DESIGN.md
T
2026-05-23 14:05:22 +08:00

838 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# lowcode-create-v2 技术设计文档
> **版本**: v0.0.1
> **更新时间**: 2026-05-04
> **范围**: 仅限运行态渲染引擎(不含设计态)
---
## 一、背景与动机
### 1.1 v1 架构瓶颈
v1 渲染引擎 (`@lingshu/lowcode-create`) 基于递归 Vue 组件树实现,核心渲染链路为:
```
LowCodeFormRender.vue → LowCodeFormItem.vue → Operation.vue → Component.vue → FormItem.vue / Col.vue
```
每个 schema 节点 = 5+ 层 Vue 组件实例。对于一个 50 字段的表单,v1 会创建 **1600+ 个组件实例**,主要性能问题:
| 问题 | 原因 | 影响 |
|------|------|------|
| 首屏渲染慢 | 递归组件树深度优先 mount,阻塞主线程 | FCP > 2s |
| 组件实例膨胀 | 每个字段 5+ 层包装组件 | 内存占用高、GC 压力大 |
| 分帧效率低 | `LowCodeFormRender` 按 chunkSize 分帧,但每帧内仍是同步递归 | 帧间隙利用不充分 |
| Schema 重复解析 | 每个 `LowCodeFormItem` 独立解析 schema | CPU 浪费 |
### 1.2 v2 设计目标
1. **组件实例减少 85%**:布局层零组件实例(纯 CSS + innerHTML
2. **同步渲染**:骨架即时呈现,业务组件同步一次性挂载,渲染完成后触发事件通知
3. **API 完全兼容**`useLowCodeFormCreate()` 返回值与 v1 签名一致,上层零改动切换
4. **模块复用**:非渲染模块(FormApi、CodeProtocol、SandBox、ComponentMap100% 复用 v1
---
## 二、整体架构
### 2.1 架构概览
```
┌─────────────────────────────────────────────────────────────────┐
│ 调用方 (PreviewForm.vue) │
│ │ │
│ useLowCodeFormCreate() │
│ ┌────┴────┐ │
│ FeForm (FC) FormApi / CodeProtocol │
│ │ (复用 v1) │
│ ┌────┴────┐ │
│ │ FormRoot │ ← Provide 上下文 │
│ └────┬────┘ │
│ ┌──────────┴──────────┐ │
│ analyzeSchema() LayoutScope │
│ (前置分析) (递归渲染单元) │
│ │ │ │
│ AnalyzedNode[] ┌───────┴────────┐ │
│ │ │ │
│ generateLayoutHtml collectBusinessNodes │
│ (HTML 骨架) (业务组件列表) │
│ │ │ │
│ innerHTML Teleport × N │
│ (一次写入) (平级挂载) │
│ │ │
│ IslandWrapper │
│ (统一渲染入口) │
│ ┌─────┴─────┐ │
│ 叶子组件 容器组件 │
│ │ │ │
│ FormItemWrapper LayoutScope │
│ (验证包装) (递归下一层) │
└─────────────────────────────────────────────────────────────────┘
```
### 2.2 核心设计理念:"岛屿架构" (Island Architecture)
借鉴 Astro 等框架的岛屿架构思想:
- **静态海洋**:布局层(Col、FLEX_LAYOUT、PAGE_ROOT、SLOT)全部转为纯 HTML 字符串,通过 `innerHTML` 一次性写入 DOM。**零 Vue 组件实例、零响应式开销**。
- **动态岛屿**:业务组件(输入框、表格、按钮等)通过 `<Teleport>` 精准挂载到 HTML 占位符上,每个都是独立的"岛屿"。
- **递归分层**:容器组件(GROUP_PANEL、TAB 等)的 slot 内容由嵌套的 `LayoutScope` 独立处理,形成"层层独立、逐层渲染"的架构。
### 2.3 包结构
```
packages/lowcode-create-v2/
├── src/
│ ├── index.ts # 包入口 (12行)
│ ├── composables/
│ │ └── FeFormCreate.tsx # useLowCodeFormCreate (87行)
│ ├── core/
│ │ ├── SchemaAnalyzer.ts # Schema 前置分析 (137行)
│ │ ├── LayoutRenderer.tsx # HTML 骨架生成 + 业务组件收集 (117行)
│ │ ├── IslandWrapper.tsx # 组件岛统一渲染入口 (203行)
│ │ ├── IslandScheduler.ts # 组件岛分批挂载调度器 (80行)
│ │ └── renderKeys.ts # Provide/Inject Symbol 定义 (20行)
│ ├── components/
│ │ ├── FormRoot.tsx # 根容器 + LayoutScope (296行)
│ │ └── FormItemWrapper.tsx # FormItem 轻量包装 (124行)
│ ├── styles/
│ │ ├── col-grid.css # 24列 CSS 栅格 (392行)
│ │ ├── flex-layout.css # Flex 布局样式 (25行)
│ │ └── root-canvas.css # 根容器样式 (39行)
│ └── utils/
│ └── colCss.ts # Col class 计算 (42行)
├── package.json
└── tsconfig.json
```
**总代码量**: ~1,176 行 TypeScript/TSX + 456 行 CSS
---
## 三、核心模块详解
### 3.1 入口层
#### 3.1.1 `index.ts` — 包入口
**路径**: `src/index.ts` (12行)
统一导出 + 样式副作用导入。三个 CSS 文件在此处导入确保使用方 `import` 包后样式自动生效。
```typescript
// 样式副作用
import './styles/col-grid.css';
import './styles/flex-layout.css';
import './styles/root-canvas.css';
// 对外 API
export { useLowCodeFormCreate } from './composables/FeFormCreate';
export { IslandScheduler } from './core/IslandScheduler';
export type { AnalyzedNode } from './core/SchemaAnalyzer';
```
**调用关系**: 外部 → `index.ts``FeFormCreate` / `IslandScheduler`(渲染完成检测) / `SchemaAnalyzer`(类型)
---
#### 3.1.2 `FeFormCreate.tsx` — useLowCodeFormCreate Composable
**路径**: `src/composables/FeFormCreate.tsx` (87行)
v2 的唯一对外入口函数,API 签名与 v1 **完全一致**,上层代码无需修改:
```typescript
function useLowCodeFormCreate<FormState, ExposeApi>(
options: UseLowCodeFormCreateOptions<FormState, ExposeApi>
): [ReturnType<typeof defineComponent>, FormApi, CodeProtocol]
```
**内部流程**:
```
useLowCodeFormCreate(options)
├── 1. 创建/复用 FormApi 实例
│ └── new FormApi({ schemas, formState, runtimeApi, pageType, ... })
├── 2. 注册 FormApi 到 runtimeApi
│ └── apis.runtimeApi.registerFormApi(formApi, pageUUID)
├── 3. 创建 CodeProtocol(含 SandBox
│ └── useCodeProtocol({ util, apis, sandbox })
├── 4. 注册 scope dispose 清理
│ └── onScopeDispose(() => codeProtocol.destroy())
├── 5. 创建 FeForm (FunctionalComponent)
│ └── 内部渲染 <FormRoot {...mergedProps} />
└── 6. 返回 [FeForm, formApi, codeProtocol]
```
**关键设计决策**: `FeForm` 是一个 `FunctionalComponent`(无状态无实例),仅作为 props 透传层将所有参数传给 `FormRoot`。这样既保持了 v1 的 API 兼容性(上层通过 `<FeForm />` 使用),又避免了额外的组件实例开销。
**模块复用**: `FormApi``SandBox``UserDefine``useCodeProtocol` 全部复用 `@lingshu/lowcode-create` v1 的实现。
---
### 3.2 核心渲染引擎
#### 3.2.1 `SchemaAnalyzer.ts` — Schema 前置分析器
**路径**: `src/core/SchemaAnalyzer.ts` (137行)
**职责**: 将原始 `LowCodeFormSchemaItem[]` 数组递归转换为 `AnalyzedNode[]` 树,一次性完成分类、优先级标注、CSS 类名计算。
**AnalyzedNode 接口**:
```typescript
interface AnalyzedNode {
schema: LowCodeFormSchemaItem // 原始 schema 引用
nodeType: 'slot' | 'hidden' | 'business' | 'container' // 节点分类
colClasses: string[] // CSS 栅格类名(lc-col-* + el-col-*
priority: number // 渲染优先级: 0=容器, 1=业务, 2=隐藏
componentCode: string // 唯一组件标识
schemaType: string // schema.type
children?: AnalyzedNode[] // 递归子节点
}
```
**分类规则** (`analyzeNode` 函数):
```
Schema 节点
├── type === 'PAGE_ROOT'
│ → nodeType: 'container', priority: 0
│ → 递归分析 children
├── type === 'SLOT'
│ → nodeType: 'slot', priority: 1
│ → 递归分析 children
├── controls.hidden === true
│ → nodeType: 'hidden', priority: 2
│ → 不展开 children
├── isContainer(schema) === true
│ (在 CONTAINER_WIDGET_TYPES 集合中 或 children 含 SLOT
│ → nodeType: 'container', priority: 0
│ → 递归分析 children
└── 其他
→ nodeType: 'business', priority: 1
→ 不展开 children(无子节点)
```
**显式容器类型集合** (`CONTAINER_WIDGET_TYPES`):
```typescript
const CONTAINER_WIDGET_TYPES = new Set([
'FLEX_LAYOUT', // 弹性布局
'ROW_LAYOUT', // 栅格布局
'COL_CONTAINER', // 栅格列容器
'TAB', 'TAB_PANEL', // Tab 控件
'GROUP_PANEL', // 分组面板
'TOOLBAR', // 工具栏
'ANCHOR', // 锚点
'PARTITION_CONTAINER', // 分割容器
'STEP', 'STEP_NAVBAR_ITEM', // 步骤条
]);
```
**调用关系**: `FormRoot.tsx``computed(() => analyzeSchema(props.schemas))``SchemaAnalyzer.analyzeSchema()`
---
#### 3.2.2 `LayoutRenderer.tsx` — HTML 布局生成 + 业务组件收集
**路径**: `src/core/LayoutRenderer.tsx` (117行)
**职责**: 两个核心函数,配合完成"静态海洋"和"动态岛屿"的分离。
##### `generateLayoutHtml(nodes)` — 生成 HTML 骨架字符串
递归遍历 `AnalyzedNode` 树,输出原生 HTML 字符串:
```
AnalyzedNode
├── PAGE_ROOT → 透明展开(零 DOM),直接递归 children
├── SLOT → 透明展开(零 DOM),直接递归 children
├── hidden → <div data-component-code="xxx" style="display:none!important"></div>
├── FLEX_LAYOUT → 生成 flex 容器 HTML
│ ┌── 若有 colClasses: <div class="com-col-item el-col lc-col-* ...">
│ │ <div class="flex-layout-row ls-size-full ls-flex" style="...">
│ │ {递归 children HTML}
│ │ </div>
│ └── </div>
└── 其他(业务/容器组件)→ 生成占位符:
┌── 若有 colClasses: <div class="com-col-item el-col lc-col-* ...">
│ <div data-component-code="xxx"></div>
└── </div>
```
**关键点**:
- `data-component-code` 属性是 Teleport 的定位锚点
- FLEX_LAYOUT 的样式从 `schema.metaProps.component.customStyle.wrapper` 提取
- Col 包装的样式从 `schema.metaProps.col.customStyle.wrapper` 提取
- `objectToStyleString()` 负责 camelCase → kebab-case 转换
##### `collectBusinessNodes(nodes)` — 收集当前层业务组件
**只收集当前 LayoutScope 层** 的业务组件,不深入容器的 children:
```typescript
function collectBusinessNodes(nodes: AnalyzedNode[]): AnalyzedNode[] {
for (const node of nodes) {
if (isLayoutNode(node)) {
// PAGE_ROOT / SLOT / FLEX_LAYOUT → 透明展开,继续收集
result.push(...collectBusinessNodes(node.children));
} else if (node.nodeType !== 'hidden') {
// 业务组件 / 容器组件 → 收集(不进入 children!)
result.push(node);
}
// hidden → 跳过
}
}
```
**为何不进入容器的 children** 因为容器组件(如 GROUP_PANEL)的内部 slot 内容由 `IslandWrapper` 中嵌套的 `LayoutScope` 独立处理。每层只管自己的事。
**调用关系**: `LayoutScope.setup()` 中的 `computed(() => generateLayoutHtml(props.nodes))``computed(() => collectBusinessNodes(props.nodes))`
---
#### 3.2.3 `IslandWrapper.tsx` — 组件岛统一渲染入口
**路径**: `src/core/IslandWrapper.tsx` (203行)
**职责**: 所有业务组件和容器组件的 **统一渲染入口**。Teleport 挂载到占位符后,每个组件由一个 IslandWrapper 负责:
1. 解析注册组件
2. 构建 props(含 v-model 绑定)
3. 构建命名插槽(容器组件)
4. 决定是否需要 FormItem 包装
**完整渲染流程**:
```
IslandWrapper.setup(props: { node: AnalyzedNode })
├── inject 上下文: apis, configs, pageType, LayoutScopeComp
├── onBeforeMount: registerActionScript(schema) // 注册事件脚本
└── render()
├── 1. 解析组件
│ runtimeApi.getRenderCompName(schema) → compName
│ getRegisteredComponent(compName) → Component
│ 若 Component 为 null → 渲染红色错误提示
├── 2. 提取 FormItem icon slot
│ 遍历 node.children 中的 SLOT 节点
│ area === FormItemLabelPrefixIcon → prefixIconNode
│ area === FormItemLabelSuffixIcon → suffixIconNode
├── 3. 构建命名插槽 (容器组件)
│ 遍历 node.children 中的 SLOT 节点
│ slotFns[area] = () => h(LayoutScopeComp, { nodes: child.children })
│ ↑ 这里产生递归:LayoutScope → IslandWrapper → LayoutScope
├── 4. 构建组件 props
│ baseProps = { ...schema.metaProps.component, data-field-id, ref: setWidgetRef }
│ sharedProps = { __widget-common-class, __schema-root-class, __schema-code-root-class }
│ v-model = { modelValue, onUpdate:modelValue } (若 supportsVModel)
│ bindValueMap = 多修饰符 v-model (若 schema.controls.bindValueMap 存在)
│ compProps = { ...baseProps, ...sharedProps, schema, fieldPath }
├── 5. 创建基础 VNode
│ baseVNode = h(Component, compProps, slotFns)
└── 6. FormItem 包装判断
if (nodeType !== 'container' && schema.metaProps.formItem) {
return h(FormItemWrapper, { schema, prefixIconNode, suffixIconNode },
{ default: () => baseVNode });
}
return baseVNode;
```
**v-model 绑定策略** (与 v1 `Component.vue` 一致):
```
fieldKey 存在 且 无 bindValueMap:
→ 简单 v-model: modelValue = formApi.getFieldValue(fieldKey)
onUpdate:modelValue = formApi.setFieldValue(fieldKey, value)
bindValueMap 存在:
→ 多修饰符 v-model: 遍历 bindValueMap 的 key-modifier 对
对每个 modifier: [modifier] = getFieldValue(fieldKey.key)
onUpdate:[modifier] = setFieldValue(fieldKey.key, value)
均不存在:
→ 无 v-model 绑定
```
**调用关系**: `LayoutScope` render → `h(IslandWrapper, { node })``IslandWrapper` render → `h(LayoutScopeComp, { nodes })` (递归)
---
#### 3.2.4 `IslandScheduler.ts` — 组件岛分批挂载调度器
**路径**: `src/core/IslandScheduler.ts` (80行)
**职责**: RAF 驱动的优先级队列调度器,将 island 组件分批挂载以避免长任务阻塞主线程。
```typescript
class IslandScheduler {
private queue: Array<{ id, priority, mount }>
private batchSize: number // 每帧挂载数量(默认 16
private rafId: number | null // 当前 RAF 句柄
private started: boolean
private destroyed: boolean
constructor(batchSize = 16)
enqueue(priority, id, mount) // 注册待挂载 island(自动排序 + 自动启动)
start() // 手动启动 RAF 循环
mountAll() // 立即同步挂载所有(用于 validate 前)
destroy() // 清理 RAF + 清空队列
}
```
**调度机制**:
- `enqueue()` 插入队列并按 priority 排序,自动触发 RAF 循环
- 每个 RAF 帧执行 `batchSize` 个 mount 回调
- 队列清空后 RAF 循环自动停止;新 enqueue 到达时自动恢复
- `mountAll()` 跳过 RAF 直接同步执行所有剩余任务(表单 validate 场景)
**Priority 约定**:
| Priority | 含义 | 特点 |
|----------|------|------|
| 0 | 容器组件 | 首帧全部挂载 |
| 1 | 普通业务组件 | 每帧 16 个 |
| 2 | 隐藏/低优先级 | 最后挂载 |
---
#### 3.2.5 `renderKeys.ts` — Provide/Inject Symbol 定义
**路径**: `src/core/renderKeys.ts` (20行)
**职责**: 定义两个 `InjectionKey<T>` 常量,解决 `LayoutScope ↔ IslandWrapper` 之间的循环依赖问题。
```typescript
export const renderFnKey = Symbol('renderFn') as InjectionKey<(nodes: AnalyzedNode[]) => string>
export const layoutScopeKey = Symbol('layoutScope') as InjectionKey<Component>
```
**为什么需要 Symbol DI**
```
FormRoot.tsx
├── import LayoutScope (定义在同文件)
├── import IslandWrapper (from core/IslandWrapper.tsx)
└── IslandWrapper 需要引用 LayoutScope → 循环依赖!
解决方案:
FormRoot: provide(layoutScopeKey, LayoutScope)
IslandWrapper: inject(layoutScopeKey) as Component
→ 运行时通过 DI 获取引用,编译时无循环
```
---
### 3.3 组件层
#### 3.3.1 `FormRoot.tsx` — 根容器 + LayoutScope
**路径**: `src/components/FormRoot.tsx` (333行)
此文件定义了两个核心组件:
##### LayoutScope — 通用递归渲染单元(同步一次性渲染)
**这是 v2 架构的核心创新**。每一层的渲染逻辑完全相同,所有业务组件同步一次性渲染:
```
LayoutScope({ nodes: AnalyzedNode[] })
├── computed: layoutHtml = generateLayoutHtml(nodes)
│ → 生成当前层的 HTML 骨架字符串
├── computed: businessNodes = collectBusinessNodes(nodes)
│ → 收集当前层需要 Teleport 的业务组件
├── onMounted:
│ containerRef.innerHTML = layoutHtml // 一次性写入 DOM
│ ready = true // 允许 Teleport 渲染
├── watch(layoutHtml):
│ containerRef.innerHTML = html // schema 变化时重建
└── render():
Fragment [
<div ref={containerRef} style="display:contents" />, // HTML 骨架容器
if (ready) ...businessNodes.map(node =>
<Teleport to={`[data-component-code="${node.componentCode}"]`}>
<IslandWrapper key={node.componentCode} node={node} />
</Teleport>
)
]
```
**`display:contents` 的作用**: 使容器 div 本身不产生盒模型,其 innerHTML 内容直接参与父元素的布局。这样 `el-row` 的 flex 布局不会被额外的 wrapper div 破坏。
**Teleport 同步渲染机制**:
1. `onMounted` 时通过 `innerHTML` 写入布局 HTML(含 `data-component-code` 占位 div
2. `ready` 设为 true,触发 re-render
3. render 函数为所有 businessNodes 创建 `<Teleport>`,同步一次性渲染到对应占位 div
##### FormRoot — 表单根容器
**Props**:
| Prop | 类型 | 来源 |
|------|------|------|
| schemas | LowCodeFormSchemaItem[] | useLowCodeFormCreate options |
| apis | FeFormApis | useLowCodeFormCreate options |
| configs | FeFormConfig | useLowCodeFormCreate options |
| pageType | number | useLowCodeFormCreate options |
| codeProtocol | CodeProtocol | useLowCodeFormCreate 创建 |
**Provide 上下文** (与 v1 完全一致):
| Key | 值 | 消费方 |
|-----|---|--------|
| `feFormApisKey` | props.apis | IslandWrapper, FormItemWrapper, 业务组件 |
| `feFormConfigKey` | props.configs | IslandWrapper, FormItemWrapper, 业务组件 |
| `feFormPageTypeKey` | props.pageType | IslandWrapper, FormItemWrapper |
| `feFormSandboxKey` | props.codeProtocol | 业务组件 (用户脚本执行) |
| `renderFnKey` (Symbol) | generateLayoutHtml | FormItemWrapper (icon HTML 注入) |
| `layoutScopeKey` (Symbol) | LayoutScope | IslandWrapper (容器 slot 递归) |
| `'lcdpFormContext'` | { labelSuffix } | form-designer FormItem.vue |
| `'PAGE_CONFIG_INFO_KEY'` | (inject from parent) | — |
**渲染结构**:
```html
<div id="rootCanvas" class="root-canvas ls-relative ls-z-0 ls-box-border ls-h-full [is-designer] [rootClassName] [routeName] [is-form-detail]">
<el-form :model="formApi.formState" :label-position :label-width :label-suffix ref="rootFormRefEl" class="root-canvas-form ls-h-full">
<el-row class="root-canvas-form-row ls-h-full lst-module_bg7 [rowClassName]" :style="componentStyle">
<LayoutScope :nodes="analyzedNodes" />
</el-row>
</el-form>
</div>
```
**Inject 依赖**:
- `'PAGE_CONFIG_INFO_KEY'` — 由 form-designer 的 PreviewForm 等上层组件 provide
- `$router` — 通过 `getCurrentInstance().appContext.config.globalProperties.$router` 安全获取
---
#### 3.3.2 `FormItemWrapper.tsx` — FormItem 轻量包装
**路径**: `src/components/FormItemWrapper.tsx` (124行)
**职责**: 为叶子业务组件提供 `<el-form-item>` 的标签和验证功能。
**Props**:
| Prop | 类型 | 说明 |
|------|------|------|
| schema | LowCodeFormSchemaItem | 当前字段的 schema |
| prefixIconNode | AnalyzedNode? | 标签前置 icon 节点 |
| suffixIconNode | AnalyzedNode? | 标签后置 icon 节点 |
**核心逻辑 — `formItemProps` 计算**:
```typescript
const formItemProps = computed(() => {
const fi = schema.metaProps.formItem; // 原始 formItem 配置
if (!fi) return null;
// 1. 调用 transformSchemaProperties 获取动态覆盖
// 返回: { 'metaProps.formItem.required': true, 'metaProps.formItem.rules': [...], ... }
const overrides = apis.runtimeApi.transformSchemaProperties(configs, schema, apis);
// 2. 惰性合并:只在存在覆盖时创建新对象
let mergedFi = fi;
if (overrides) {
for (const key in overrides) {
if (key.startsWith('metaProps.formItem.')) {
if (mergedFi === fi) mergedFi = { ...fi }; // 首次覆盖时浅拷贝
mergedFi[key.slice(18)] = overrides[key]; // 'metaProps.formItem.required' → 'required'
}
}
}
// 3. 组装最终 props
return { ...mergedFi, for, prop, __widget-common-class, __schema-root-class, __schema-code-root-class, schema };
});
```
**transformSchemaProperties 的重要性**: 在 v1 中,`required``rules``label` 等验证属性 **不存储在** `schema.metaProps.formItem` 中,而是由 `transformSchemaProperties` 在运行时动态计算注入。v2 必须在 FormItemWrapper 中执行此调用,否则表单验证不生效。
**Lazy 组件解析**: `getGlobalFormWrapper('form-item')` 在 render 函数中调用而非 setup 中,确保 form-designer 的 `registerWidget` 已执行完成。
**Icon 插槽处理**: 通过 `generateHtml()` (inject renderFnKey) 将 icon 节点转为 HTML 字符串,注入到 FormItem 的 `_formItemLabel_prefixIcon` / `_formItemLabel_suffixIcon` 插槽中。
---
### 3.4 工具层
#### 3.4.1 `colCss.ts` — Col 栅格类名计算
**路径**: `src/utils/colCss.ts` (42行)
`schema.metaProps.col` 配置转为 CSS 类名数组。**生成双前缀类名**以兼容 v1:
```typescript
// span: 12 → ['lc-col-12', 'el-col-12']
// offset: 2 → ['lc-col-offset-2', 'el-col-offset-2']
// sm: 6 → ['lc-col-sm-6', 'el-col-sm-6']
```
`lc-col-*` 由 v2 的 `col-grid.css` 定义;`el-col-*` 保留是为了兼容可能依赖 Element Plus col 类名的第三方样式。
#### 3.4.2 CSS 样式文件
| 文件 | 行数 | 内容 |
|------|------|------|
| `col-grid.css` | 392 | 24 列 flex 栅格系统,支持 span/offset/push/pull + 5 个响应式断点 |
| `root-canvas.css` | 39 | 根容器样式:滚动、设计态对齐、列表容器垂直排列等 7 条规则 |
| `flex-layout.css` | 25 | 设计态 flex-layout 列排列 + CSS 变量驱动 |
---
## 四、数据流与调用链路
### 4.1 完整渲染流程时序
```
1. 上层调用 useLowCodeFormCreate(options)
2. 创建 FormApi、CodeProtocol(复用 v1
3. 返回 [FeForm, formApi, codeProtocol]
4. 上层模板 <FeForm /> 触发 FormRoot render
5. FormRoot.setup():
├── provide 所有上下文 (apis, configs, pageType, sandbox, renderFn, layoutScope)
├── inject PAGE_CONFIG_INFO_KEY
├── computed: analyzedNodes = analyzeSchema(schemas) ← SchemaAnalyzer
├── computed: formProps, rowProps, componentStyle ← 从 rootSchema 提取
├── onMounted: setFormInstance(el-form ref)
└── render: #rootCanvas > el-form > el-row > LayoutScope(analyzedNodes)
6. LayoutScope.setup({ nodes: analyzedNodes }):
├── computed: layoutHtml = generateLayoutHtml(nodes) ← LayoutRenderer
├── computed: businessNodes = collectBusinessNodes(nodes) ← LayoutRenderer
├── onMounted: containerRef.innerHTML = layoutHtml ← DOM 写入
├── ready = true ← 触发 re-render
└── render: Fragment [containerDiv, ...businessNodes.map(Teleport[IslandWrapper])]
7. 每个 Teleport → IslandWrapper.setup({ node }):
├── inject: apis, configs, pageType, LayoutScopeComp
├── onBeforeMount: registerActionScript(schema)
└── render():
├── resolveComponent(schema) → Component
├── 构建 slotFns (若容器) → h(LayoutScopeComp, { nodes }) ← 递归到步骤 6
├── 构建 compProps (v-model, sharedProps, fieldPath)
├── baseVNode = h(Component, compProps, slotFns)
└── 若需 FormItem:
└── h(FormItemWrapper, { schema, icons }, { default: baseVNode })
8. FormItemWrapper.render():
├── computed: formItemProps (含 transformSchemaProperties 动态覆盖)
├── lazy: RenderFormItem = getGlobalFormWrapper('form-item')
└── h(RenderFormItem, formItemProps, { default, prefixIcon, suffixIcon })
```
### 4.2 递归渲染层级示意
以一个包含 GROUP_PANEL 的表单为例:
```
Schema 树:
PAGE_ROOT
├── SLOT (default)
│ ├── INPUT (fieldA)
│ ├── GROUP_PANEL
│ │ └── SLOT (default)
│ │ ├── SELECT (fieldB)
│ │ └── DATE_PICKER (fieldC)
│ └── TEXTAREA (fieldD)
v2 渲染层级:
Layer 0 (FormRoot → LayoutScope):
HTML 骨架: [占位:fieldA] [占位:GROUP_PANEL] [占位:fieldD]
Teleport × 3:
├── IslandWrapper(INPUT/fieldA) → FormItemWrapper → el-form-item > 输入框
├── IslandWrapper(GROUP_PANEL) → 容器组件,slot 内容 = LayoutScope(Layer 1)
└── IslandWrapper(TEXTAREA/fieldD) → FormItemWrapper → el-form-item > 文本域
Layer 1 (GROUP_PANEL slot → LayoutScope):
HTML 骨架: [占位:fieldB] [占位:fieldC]
Teleport × 2:
├── IslandWrapper(SELECT/fieldB) → FormItemWrapper → el-form-item > 下拉选择
└── IslandWrapper(DATE_PICKER/fieldC) → FormItemWrapper → el-form-item > 日期选择
```
### 4.3 模块间依赖关系图
```
index.ts
FeFormCreate.tsx
FormRoot.tsx ─────────────┐
╱ │ ╲ │
SchemaAnalyzer LayoutRenderer renderKeys.ts
│ │ │
colCss.ts │ │
│ │
IslandWrapper.tsx ────┘
FormItemWrapper.tsx
[form-designer 全局组件]
```
**外部依赖** (全部复用 v1):
```
@lingshu/lowcode-create:
├── FormApi, SandBox, UserDefine, useCodeProtocol (运行时)
├── feFormApisKey, feFormConfigKey, feFormPageTypeKey, feFormSandboxKey (DI keys)
├── getRegisteredComponent, getCurrentFieldKey, supportsVModel (工具)
├── getGlobalFormWrapper (全局组件注册表)
└── FormInnerSlotAreaEnum (插槽区域枚举)
@lingshu/types:
└── FormContext, PageConfig, PageModeEnum, PageOpenModeEnum, PageRenderSceneEnum
element-plus-cisdi:
└── ElForm, ElRow
```
---
## 五、v1 vs v2 架构对比
### 5.1 渲染链路对比
**v1 — 递归组件树**:
```
LowCodeFormRender (分帧调度)
└── LowCodeFormItem × N (每个 schema 节点)
├── Placeholder (renderType=Placeholder)
├── SlotOpe (type=SLOT, 设计态)
└── Operation (业务组件包装)
├── Col.vue (el-col 组件)
├── FormItem.vue (el-form-item 组件)
└── Component.vue (实际业务组件 + v-model)
```
每个字段 = `LowCodeFormItem` + `Operation` + `Col` + `FormItem` + `Component` = **5 层 Vue 组件**
**v2 — 岛屿架构**:
```
FormRoot
└── LayoutScope (innerHTML + Teleport)
└── IslandWrapper × N (每个业务组件)
└── FormItemWrapper (仅叶子组件)
└── 全局 FormItem.vue (form-designer 注册)
```
每个字段 = `IslandWrapper` + `FormItemWrapper` + 全局 FormItem = **3 层 Vue 组件**(布局层 0 组件)
### 5.2 关键指标对比
| 维度 | v1 | v2 | 改进 |
|------|----|----|------|
| 组件实例数 (50字段) | ~1,600+ | ~250 | **-85%** |
| Schema 解析 | 每个 LowCodeFormItem 独立解析 | 一次性前置分析 (cached computed) | N次 → 1次 |
| DOM 操作 | 递归逐个 mount | innerHTML 一次写入 + Teleport | 批量 vs 逐个 |
| 布局层组件 | Col(el-col) + 多层wrapper | 纯 CSS class + innerHTML | 零组件实例 |
| 分帧策略 | chunkSize 个 LowCodeFormItem/帧 | 同步渲染 (可选 IslandScheduler 分帧) | 首屏无分帧延迟 |
| 循环依赖 | 无(单向递归) | Symbol DI (renderKeys.ts) | 解耦 |
| API 兼容性 | — | 100% 签名一致 | 零改动切换 |
### 5.3 复用 vs 新建
| 模块 | 策略 |
|------|------|
| FormApi | **复用** v1 |
| CodeProtocol / SandBox | **复用** v1 |
| ComponentMap (getRegisteredComponent, getGlobalFormWrapper) | **复用** v1 |
| 类型定义 (LowCodeFormSchemaItem, FeFormApis, ...) | **复用** v1 |
| 全局 FormItem.vue / ColItem.vue | **复用** form-designer 注册的实现 |
| 渲染链路 (FormRoot, LayoutScope, IslandWrapper, ...) | **全新** v2 |
| 栅格系统 (col-grid.css) | **全新** v2 (替代 el-col 组件) |
| Schema 分析 (SchemaAnalyzer) | **全新** v2 |
| HTML 生成 (LayoutRenderer) | **全新** v2 |
---
## 六、关键设计决策
### 6.1 为何选择 innerHTML 而非 VNode 树?
**方案对比**:
| 方案 | 优点 | 缺点 |
|------|------|------|
| Vue VNode 树 | 响应式更新、组件化 | 每个布局节点是组件实例,内存开销大 |
| innerHTML + Teleport | 零组件实例、一次性 DOM 写入 | 失去响应式更新能力 |
**选择 innerHTML 的理由**:
- 布局 HTML 只在 schemas 变化时重建(极低频),不需要细粒度响应式
- 50 字段的表单减少 ~200 个 Col/FormItem 组件实例
- `display:contents` 保证 innerHTML 不破坏 flex 布局
### 6.2 为何 LayoutScope 递归而非全局扁平化?
全局扁平化(所有组件一次性 Teleport 到根层)会导致容器组件的 slot 内容无法正确传递。GROUP_PANEL 的 default slot 需要接收其内部组件作为子元素,而非同级 Teleport。
递归 LayoutScope 保证:
- 每层独立生成 HTML + Teleport
- 容器组件的 slot 内容在其内部的 LayoutScope 中渲染
- slot 内容通过 Vue 的 slot 机制正确传递给容器组件
#### 6.4 双前缀 Col 类名 (lc-col-* + el-col-*)
`computeColClasses` 为每个值同时生成 `lc-col-*``el-col-*` 两套类名:
- `lc-col-*`: v2 自定义栅格系统(col-grid.css
- `el-col-*`: 兼容 v1 可能存在的依赖 Element Plus col 类名的第三方样式
---
## 七、源码文件速查表
| 文件 | 路径 | 行数 | 核心职责 |
|------|------|------|---------|
| index.ts | `src/index.ts` | 12 | 包入口、CSS 导入、对外导出 |
| FeFormCreate.tsx | `src/composables/FeFormCreate.tsx` | 87 | useLowCodeFormCreate composable |
| SchemaAnalyzer.ts | `src/core/SchemaAnalyzer.ts` | 137 | Schema → AnalyzedNode 树(前置分析) |
| LayoutRenderer.tsx | `src/core/LayoutRenderer.tsx` | 117 | HTML 骨架生成 + 当前层业务组件收集 |
| IslandWrapper.tsx | `src/core/IslandWrapper.tsx` | 203 | 业务/容器组件统一渲染入口 |
| IslandScheduler.ts | `src/core/IslandScheduler.ts` | 120 | RAF 优先级队列分批挂载调度器 + 完成回调 |
| renderKeys.ts | `src/core/renderKeys.ts` | 20 | Symbol-based DI Key 定义 |
| FormRoot.tsx | `src/components/FormRoot.tsx` | 333 | LayoutScope 分批递归渲染 + FormRoot 根容器 |
| FormItemWrapper.tsx | `src/components/FormItemWrapper.tsx` | 124 | FormItem 轻量包装 + 动态属性注入 |
| colCss.ts | `src/utils/colCss.ts` | 42 | Col 栅格 CSS 类名计算 |
| col-grid.css | `src/styles/col-grid.css` | 392 | 24 列 CSS 栅格系统 |
| flex-layout.css | `src/styles/flex-layout.css` | 25 | Flex 布局样式 |
| root-canvas.css | `src/styles/root-canvas.css` | 39 | 根容器样式规则 |