DSL 渲染为 Vue3 页面组件 — 完整流程
整体流程概览
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| BlockSchema (JSON DSL) │ ▼ createRenderer() │ ├─ 1. 创建 Context 上下文对象 │ ├─ 2. 定义 Vue 组件 (defineComponent) │ ├─ setup(): 初始化 state / computed / methods / watch │ └─ render(): 递归调用 nodeRender() 生成 VNode 树 │ └─ 3. 返回 { renderer, context } │ ▼ <Suspense><component :is="renderer" /></Suspense> │ ▼ 页面渲染完成
|
Step 1: DSL 数据结构定义
代码路径: src/core/types.ts
DSL 使用 JSON 描述页面,核心类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| interface BlockSchema { id?: string; name: string; state?: Record<string, JSExpression>; computed?: Record<string, JSFunction>; methods?: Record<string, JSFunction>; watch?: any[]; lifeCycles?: Record<string, JSFunction>; nodes?: NodeSchema[]; css?: string; }
interface NodeSchema { id?: string; name: string; props?: NodeProps; events?: NodeEvents; directives?: NodeDirective[]; children?: string | JSExpression | NodeSchema[]; }
interface JSExpression { type: 'JSExpression'; value: string; }
interface JSFunction { type: 'JSFunction'; value: string; }
|
Step 2: 创建渲染器 — createRenderer()
代码路径: src/core/renderer.ts → createRenderer()
重点方法: createRenderer(options)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| export function createRenderer(options: CreateRendererOptions) { const V = Vue; const ctx = new Context({ dsl: options.dsl });
const renderer = V.defineComponent({ name: dsl.name || 'DslRenderer',
async setup() { ctx.state = createState(V, dsl.state, ctx); const computed = createComputed(V, dsl.computed, ctx); const methods = createMethods(dsl.methods, ctx); ctx.setup({ ...computed, ...methods }, V); setWatches(V, dsl.watch, ctx); return { state: ctx.state, ...computed, ...methods }; },
...createLifeCycles(dsl.lifeCycles, ctx),
render() { const nodes = dsl.nodes || []; if (nodes.length === 1) return nodeRender(nodes[0], ctx, V, nodes); return V.createVNode('div', {}, nodes.map(child => nodeRender(child, ctx, V, nodes)) ); } });
return { renderer: V.markRaw(renderer), context: ctx }; }
|
调用方式 (在 CanvasApp.vue / PreviewView.vue 中):
1 2 3 4 5 6 7
| const result = createRenderer({ dsl: blockSchema }); renderer.value = result.renderer;
<Suspense> <component :is="renderer" /> </Suspense>
|
Step 3: 运行时上下文 — Context 类
代码路径: src/core/renderer.ts → class Context
Context 是 DSL 表达式执行的运行环境,代理了 Vue 实例的所有能力:
1 2 3 4 5 6 7 8 9 10 11
| Context ├── state → Vue reactive 状态 ├── props → 组件 props ├── $refs → 模板引用 ├── $emit → 事件触发 ├── $slots → 插槽 ├── $watch → 侦听器 ├── __parseExpression() → 执行 JSExpression ├── __parseFunction() → 执行 JSFunction ├── __clone() → 克隆上下文(用于 v-for 循环变量) └── __ref() → 处理模板 ref 引用
|
重点方法:
ctx.setup(attrs, V) — 在 Vue setup() 中调用,绑定 getCurrentInstance() 到 ctx
ctx.__parseExpression(code) — 将 JSExpression 字符串转为实际值
ctx.__parseFunction(code) — 将 JSFunction 字符串转为可执行函数
ctx.__clone(context) — 创建子上下文(v-for 中每个迭代项需要独立上下文)
Step 4: 表达式解析 — parseExpression / parseFunction
代码路径: src/core/renderer.ts → parseExpression(), parseFunction()
重点方法: parseExpression(str, self)
1 2 3 4 5 6 7 8
| function parseExpression(str: JSExpression, self: any) { let tarStr = str.value.replace(/this(\W|$)/g, `__self$1`); const code = `with($scope || {}) { "use strict"; var __self = arguments[0]; return ${tarStr} }`; return new Function('$scope', code)(self); }
|
原理: 利用 new Function + with 语句,让 DSL 中的表达式字符串可以访问 Context 上的 state/props/methods 等变量。
示例:
1 2 3
| JSExpression: { type: 'JSExpression', value: 'count + 1' } ↓ parseExpression 结果: state.count + 1 的实际值
|
Step 5: 节点渲染 — nodeRender() 递归
代码路径: src/core/renderer.ts → nodeRender()
重点方法: nodeRender(dsl, ctx, V, brothers, isBranch, index)
这是整个渲染器最核心的函数,将一个 NodeSchema 递归转为 Vue VNode:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| nodeRender(dsl, ctx) │ ├─ 1. 处理指令 (v-if / v-else-if / v-else) │ 条件不满足 → 查找兄弟节点的 v-else-if / v-else │ ├─ 2. 内部 render(c, seq) 函数: │ ├─ parseNodeProps() → 解析属性(递归处理 JSExpression) │ ├─ parseNodeEvents() → 解析事件(JSFunction → onXxx) │ ├─ 处理 v-bind → 合并动态属性 │ ├─ 处理 v-show → 控制 display: none │ ├─ 处理 v-html → 设置 innerHTML │ ├─ 处理 v-model → 双向绑定(区分原生/组件) │ ├─ childrenToSlots() → 子节点转为插槽 │ └─ Vue.createVNode() → 创建 VNode │ ├─ 3. 处理 v-for → vForRender() 循环渲染 │ 每次迭代 clone Context,注入 item/index 变量 │ └─ 4. 返回 VNode 或 VNode 数组
|
Step 6: 辅助函数详解
6.1 属性解析 — deepParseNodeProps()
代码路径: src/core/renderer.ts → deepParseNodeProps()
递归遍历 props 对象,将所有 JSExpression/JSFunction 替换为实际值:
1 2 3
| props: { class: { type: 'JSExpression', value: 'active ? "active" : ""' } } ↓ deepParseNodeProps props: { class: "active" } // 运行时求值结果
|
6.2 事件解析 — parseNodeEvents()
代码路径: src/core/renderer.ts → parseNodeEvents()
将 DSL 事件定义转为 Vue 的 onXxx 格式:
1 2 3
| events: { click: { handler: { type: 'JSFunction', value: 'function() { ... }' } } } ↓ parseNodeEvents { onClick: [Function] }
|
6.3 指令处理 — getDiretives()
代码路径: src/core/renderer.ts → getDiretives()
将 directives 数组分类为 vIf/vFor/vShow/vModel 等,方便后续分别处理。
6.4 v-for 渲染 — vForRender()
代码路径: src/core/renderer.ts → vForRender()
1 2 3 4 5 6 7 8
| { directives: [{ name: 'vFor', value: { type: 'JSExpression', value: 'items' }, iterator: { item: 'item', index: 'idx' } }] }
|
6.5 子节点转插槽 — childrenToSlots()
代码路径: src/core/renderer.ts → childrenToSlots(), createSlotsConfig()
将 NodeSchema 的 children 转为 Vue 插槽对象:
1 2 3 4 5 6 7 8 9
| children: [ { name: 'div', slot: 'header', children: '标题' }, { name: 'div', slot: 'default', children: '内容' } ] ↓ childrenToSlots { header: () => [VNode], default: () => [VNode] }
|
6.6 状态初始化 — createState() / createComputed() / createMethods()
代码路径: src/core/renderer.ts
1 2 3 4
| BlockSchema.state → createState() → Vue.reactive({ ... }) BlockSchema.computed → createComputed() → { key: Vue.computed(fn) } BlockSchema.methods → createMethods() → { key: Function } BlockSchema.watch → setWatches() → Vue.watch(source, handler)
|
Step 7: 设计器中的渲染集成
7.1 iframe 画布渲染
代码路径: src/canvas/CanvasApp.vue
1 2 3 4 5 6 7 8 9
| DesignerView (主页面) │ postMessage('init', dsl) ▼ CanvasApp.vue (iframe 内) │ 收到 init → renderDsl() │ ├─ createRenderer({ dsl }) │ └─ renderer.value = result.renderer ▼ <Suspense><component :is="renderer" /></Suspense>
|
7.2 预览页渲染
代码路径: src/views/PreviewView.vue
1 2 3 4 5 6 7
| sessionStorage.getItem('vtj_preview_dsl') │ JSON.parse ▼ createRenderer({ dsl }) │ ▼ <Suspense><component :is="renderer" /></Suspense>
|
代码文件索引
| 文件 |
说明 |
核心方法 |
src/core/types.ts |
DSL 类型定义 |
BlockSchema, NodeSchema, JSExpression, JSFunction |
src/core/renderer.ts |
渲染器核心 |
createRenderer(), nodeRender(), Context |
|
表达式解析 |
parseExpression(), parseFunction() |
|
属性处理 |
deepParseNodeProps(), parseNodeProps(), parseNodeEvents() |
|
指令处理 |
getDiretives(), vForRender() |
|
插槽处理 |
childrenToSlots(), createSlotsConfig() |
|
状态初始化 |
createState(), createComputed(), createMethods(), setWatches(), createLifeCycles() |
src/canvas/CanvasApp.vue |
iframe 画布应用 |
renderDsl(), addNode(), onDocumentClick() |
src/canvas/protocol.ts |
postMessage 通信 |
sendMessage(), onMessage() |
src/views/PreviewView.vue |
预览页 |
createRenderer() + <Suspense> |
src/views/DesignerView.vue |
设计器主页面 |
sendToCanvas(), onMaskMouseUp() |
src/engine/Engine.ts |
设计器引擎 |
DSL CRUD, 撤销/重做, 节点操作 |
src/materials/index.ts |
物料定义 |
createNodeSchema() |
src/coder/index.ts |
代码生成 |
generateVueSFC(), generateJSON() |
完整数据流图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| 用户拖拽物料 │ ▼ DesignerView.onDragStart() ──→ dragSchema = schema │ (鼠标松开在画布遮罩上) ▼ DesignerView.onMaskMouseUp() │ postMessage('dropNode', { schema, x, y }) ▼ CanvasApp 收到 dropNode │ addNode(parentId, schema) │ ├─ dsl.nodes.push(node) │ ├─ renderDsl() → createRenderer({ dsl }) │ │ ├─ new Context({ dsl }) │ │ ├─ defineComponent({ │ │ │ setup: createState + createComputed + createMethods │ │ │ render: nodes.map(n => nodeRender(n, ctx)) │ │ │ ├─ parseNodeProps() 解析属性 │ │ │ ├─ parseNodeEvents() 解析事件 │ │ │ ├─ getDiretives() 处理指令 │ │ │ ├─ childrenToSlots() 处理子节点 │ │ │ └─ Vue.createVNode() 创建 VNode │ │ │ }) │ │ └─ markRaw(renderer) │ └─ renderer.value = result.renderer ▼ <Suspense><component :is="renderer" /></Suspense> │ ▼ 页面渲染出组件 │ postMessage('updateDsl', dsl) ←── 同步回主页面 ▼ DesignerView 收到 updateDsl │ eng.setDSL(dsl) ←── 更新引擎状态 ▼ 右侧 PropsPanel 显示选中节点配置
|
Next: 关于低代码相关的理解