838 lines
34 KiB
Markdown
838 lines
34 KiB
Markdown
# 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 组件实例、零响应式开销**。
|
||
- **动态岛屿**:业务组件(输入框、表格、按钮等)通过 `<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 | 根容器样式规则 |
|