Files
2026-05-23 14:05:22 +08:00

34 KiB
Raw Permalink Blame History

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 包后样式自动生效。

// 样式副作用
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.tsFeFormCreate / IslandScheduler(渲染完成检测) / SchemaAnalyzer(类型)


3.1.2 FeFormCreate.tsx — useLowCodeFormCreate Composable

路径: src/composables/FeFormCreate.tsx (87行)

v2 的唯一对外入口函数,API 签名与 v1 完全一致,上层代码无需修改:

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 /> 使用),又避免了额外的组件实例开销。

模块复用: FormApiSandBoxUserDefineuseCodeProtocol 全部复用 @lingshu/lowcode-create v1 的实现。


3.2 核心渲染引擎

3.2.1 SchemaAnalyzer.ts — Schema 前置分析器

路径: src/core/SchemaAnalyzer.ts (137行)

职责: 将原始 LowCodeFormSchemaItem[] 数组递归转换为 AnalyzedNode[] 树,一次性完成分类、优先级标注、CSS 类名计算。

AnalyzedNode 接口:

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):

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.tsxcomputed(() => 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:

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 组件分批挂载以避免长任务阻塞主线程。

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 之间的循环依赖问题。

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) | — |

渲染结构:

<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 计算:

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 中,requiredruleslabel 等验证属性 不存储在 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

// 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 根容器样式规则