34 KiB
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 设计目标
- 组件实例减少 85%:布局层零组件实例(纯 CSS + innerHTML)
- 同步渲染:骨架即时呈现,业务组件同步一次性挂载,渲染完成后触发事件通知
- API 完全兼容:
useLowCodeFormCreate()返回值与 v1 签名一致,上层零改动切换 - 模块复用:非渲染模块(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 组件实例、零响应式开销。 - 动态岛屿:业务组件(输入框、表格、按钮等)通过
<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.ts → FeFormCreate / 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 /> 使用),又避免了额外的组件实例开销。
模块复用: 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 接口:
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.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:
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 负责:
- 解析注册组件
- 构建 props(含 v-model 绑定)
- 构建命名插槽(容器组件)
- 决定是否需要 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 同步渲染机制:
onMounted时通过innerHTML写入布局 HTML(含data-component-code占位 div)ready设为 true,触发 re-render- 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 中,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:
// 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 | 根容器样式规则 |