# 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、ComponentMap)100% 复用 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 组件实例、零响应式开销**。 - **动态岛屿**:业务组件(输入框、表格、按钮等)通过 `` 精准挂载到 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( options: UseLowCodeFormCreateOptions ): [ReturnType, 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) │ └── 内部渲染 │ └── 6. 返回 [FeForm, formApi, codeProtocol] ``` **关键设计决策**: `FeForm` 是一个 `FunctionalComponent`(无状态无实例),仅作为 props 透传层将所有参数传给 `FormRoot`。这样既保持了 v1 的 API 兼容性(上层通过 `` 使用),又避免了额外的组件实例开销。 **模块复用**: `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 →
│ ├── FLEX_LAYOUT → 生成 flex 容器 HTML: │ ┌── 若有 colClasses:
│ │
│ │ {递归 children HTML} │ │
│ └──
│ └── 其他(业务/容器组件)→ 生成占位符: ┌── 若有 colClasses:
└──
``` **关键点**: - `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` 常量,解决 `LayoutScope ↔ IslandWrapper` 之间的循环依赖问题。 ```typescript export const renderFnKey = Symbol('renderFn') as InjectionKey<(nodes: AnalyzedNode[]) => string> export const layoutScopeKey = Symbol('layoutScope') as InjectionKey ``` **为什么需要 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 [
, // HTML 骨架容器 if (ready) ...businessNodes.map(node => ) ] ``` **`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 创建 ``,同步一次性渲染到对应占位 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
``` **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行) **职责**: 为叶子业务组件提供 `` 的标签和验证功能。 **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. 上层模板 触发 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 | 根容器样式规则 |