关于低代码DSL转vue3组件的实现

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
// 页面级 Schema
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; // 样式
}

// 节点 Schema
interface NodeSchema {
id?: string;
name: string; // HTML 标签名或组件名 (如 'div', 'button')
props?: NodeProps; // 属性 (含 style, class)
events?: NodeEvents; // 事件绑定
directives?: NodeDirective[]; // 指令 (v-if, v-for, v-model...)
children?: string | JSExpression | NodeSchema[]; // 子节点
}

// 动态表达式 — 在运行时通过 new Function 求值
interface JSExpression {
type: 'JSExpression';
value: string; // 如 "count + 1"
}

// 动态函数 — 在运行时通过 new Function 生成函数
interface JSFunction {
type: 'JSFunction';
value: string; // 如 "function() { return this.count + 1 }"
}

Step 2: 创建渲染器 — createRenderer()

代码路径: src/core/renderer.tscreateRenderer()

重点方法: 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 });

// 核心:定义一个 Vue 组件
const renderer = V.defineComponent({
name: dsl.name || 'DslRenderer',

// setup 是 async(所以外层需要 <Suspense>)
async setup() {
// 2a. 创建响应式 state
ctx.state = createState(V, dsl.state, ctx);
// 2b. 创建 computed
const computed = createComputed(V, dsl.computed, ctx);
// 2c. 创建 methods
const methods = createMethods(dsl.methods, ctx);
// 2d. 绑定 Vue 实例到 Context
ctx.setup({ ...computed, ...methods }, V);
// 2e. 设置 watch
setWatches(V, dsl.watch, ctx);
// 暴露给模板
return { state: ctx.state, ...computed, ...methods };
},

// 生命周期钩子
...createLifeCycles(dsl.lifeCycles, ctx),

// 渲染函数:递归遍历 nodes 生成 VNode
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,因为 setup 是 async)
<Suspense>
<component :is="renderer" />
</Suspense>

Step 3: 运行时上下文 — Context 类

代码路径: src/core/renderer.tsclass 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.tsparseExpression(), parseFunction()

重点方法: parseExpression(str, self)

1
2
3
4
5
6
7
8
function parseExpression(str: JSExpression, self: any) {
// 1. 替换 this 为 __self
let tarStr = str.value.replace(/this(\W|$)/g, `__self$1`);
// 2. 用 with 语句构建作用域
const code = `with($scope || {}) { "use strict"; var __self = arguments[0]; return ${tarStr} }`;
// 3. new Function 动态执行
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.tsnodeRender()

重点方法: 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.tsdeepParseNodeProps()

递归遍历 props 对象,将所有 JSExpression/JSFunction 替换为实际值:

1
2
3
props: { class: { type: 'JSExpression', value: 'active ? "active" : ""' } }
↓ deepParseNodeProps
props: { class: "active" } // 运行时求值结果

6.2 事件解析 — parseNodeEvents()

代码路径: src/core/renderer.tsparseNodeEvents()

将 DSL 事件定义转为 Vue 的 onXxx 格式:

1
2
3
events: { click: { handler: { type: 'JSFunction', value: 'function() { ... }' } } }
↓ parseNodeEvents
{ onClick: [Function] }

6.3 指令处理 — getDiretives()

代码路径: src/core/renderer.tsgetDiretives()

将 directives 数组分类为 vIf/vFor/vShow/vModel 等,方便后续分别处理。

6.4 v-for 渲染 — vForRender()

代码路径: src/core/renderer.tsvForRender()

1
2
3
4
5
6
7
8
// 示例 DSL:
{ directives: [{ name: 'vFor', value: { type: 'JSExpression', value: 'items' }, iterator: { item: 'item', index: 'idx' } }] }

// 执行流程:
// 1. ctx.__parseExpression(value) → 获取数组 [1, 2, 3]
// 2. 遍历数组,每次 ctx.__clone({ item: val, idx: i }) 创建子上下文
// 3. 对每个子上下文调用 render(c, seq) 生成 VNode
// 4. 返回 VNode 数组

6.5 子节点转插槽 — childrenToSlots()

代码路径: src/core/renderer.tschildrenToSlots(), 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 显示选中节点配置

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器