渲染器方法详解

DSL 渲染器方法详解

基于当前项目 test/demo/src 源码,逐函数详细说明转换过程


一、类型系统 (src/core/types.ts)

1.1 JSExpression

1
2
3
4
5
interface JSExpression {
type: 'JSExpression';
id?: string;
value: string; // 如 "count + 1", "items.length", "visible ? 'show' : 'hide'"
}

作用: 表示一段需要在运行时动态求值的 JavaScript 表达式。
value 是字符串形式的表达式代码,渲染时通过 new Function 执行得到实际值。

转换示例:

1
2
3
输入: { type: 'JSExpression', value: 'count + 1' }
↓ parseExpression() 执行
输出: number(如 count=5 时结果为 6)

1.2 JSFunction

1
2
3
4
5
interface JSFunction {
type: 'JSFunction';
id?: string;
value: string; // 如 "function() { return this.count + 1 }"
}

作用: 表示一段需要在运行时动态生成的 JavaScript 函数。
value 是字符串形式的函数定义代码。

转换示例:

1
2
3
输入: { type: 'JSFunction', value: 'function handleClick() { this.count++ }' }
↓ parseFunction() 执行
输出: Function 对象,可直接调用

1.3 NodeSchema

1
2
3
4
5
6
7
8
9
10
11
12
interface NodeSchema {
id?: string; // 节点唯一标识,设计器用
name: string; // HTML 标签名或组件名,如 'div', 'button', 'MyComponent'
from?: string; // 组件来源路径
locked?: boolean; // 是否锁定(不可编辑)
invisible?: boolean; // 是否隐藏(不渲染)
props?: NodeProps; // 组件属性
events?: NodeEvents; // 事件绑定
directives?: NodeDirective[]; // Vue 指令
children?: string | JSExpression | NodeSchema[]; // 子节点
slot?: string | NodeSlot; // 插槽信息
}

作用: 描述页面中的每一个组件节点。children 可以是:

  • string — 纯文本内容,如 "Hello World"
  • JSExpression — 动态文本,如 { type: 'JSExpression', value: 'message' }
  • NodeSchema[] — 子节点数组,递归嵌套

1.4 BlockSchema

1
2
3
4
5
6
7
8
9
10
11
interface BlockSchema {
id?: string;
name: string; // 页面/区块名称
state?: Record<string, JSExpression | JSFunction>; // 响应式状态
computed?: Record<string, JSFunction>; // 计算属性
methods?: Record<string, JSFunction>; // 方法
watch?: any[]; // 侦听器
lifeCycles?: Record<string, JSFunction>; // 生命周期钩子
nodes?: NodeSchema[]; // 根节点列表
css?: string; // CSS 样式字符串
}

作用: 描述一个完整的页面或可复用区块,对应 Vue 的一个 SFC 组件。

1.5 NodeDirective

1
2
3
4
5
6
7
8
interface NodeDirective {
id?: string;
name: string | JSExpression; // 指令名:vIf, vFor, vShow, vModel, vBind, vHtml
arg?: string | JSExpression; // 指令参数:v-model:arg
modifiers?: Record<string, boolean>; // 修饰符
value?: JSExpression; // 指令绑定值
iterator?: { item: string; index: string }; // v-for 的迭代变量名
}

1.6 类型守卫函数

1
2
3
4
5
6
7
function isJSExpression(data: any): data is JSExpression {
return data && data.type === 'JSExpression';
}

function isJSFunction(data: any): data is JSFunction {
return typeof data === 'object' && data && data.type === 'JSFunction';
}

作用: 在运行时判断一个值是否为动态表达式/函数,用于递归遍历时区分静态值和动态值。


二、表达式解析 (src/core/renderer.ts)

2.1 parseExpression(str, self)

1
2
3
4
5
6
7
8
9
10
11
12
13
function parseExpression(str: JSExpression | JSFunction, self: any) {
// 1. 获取表达式字符串
let tarStr = (str.value || '').trim();

// 2. 将 this 替换为 __self(因为 new Function 内不能用 this 访问上下文)
tarStr = tarStr.replace(/this(\W|$)/g, (_a, b) => `__self${b}`);

// 3. 构建 with 语句,使 $scope 中的变量可以直接访问
const code = `with($scope || {}) { "use strict"; var __self = arguments[0]; return ${tarStr} }`;

// 4. 用 new Function 创建函数并执行,传入 self 作为上下文
return new Function('$scope', code)(self);
}

执行原理:

步骤 说明
str.value 原始表达式字符串,如 "count + 1"
this → __self 替换 this.count__self.count
with($scope) $scope 对象的属性可以直接用变量名访问
new Function('$scope', code)(self) 动态创建并立即执行函数

self 参数是什么: 即 Context 实例。Context 上有 statepropsmethods 等属性,
通过 with 语句,表达式中可以直接写 count 而不是 this.state.count

完整执行示例:

1
2
3
4
5
6
7
8
9
10
11
12
表达式: { type: 'JSExpression', value: 'count + 1' }
Context: { state: { count: 5 }, ... }

执行代码:
with($scope || {}) {
"use strict";
var __self = arguments[0];
return count + 1
}

$scope = Context实例,state 通过 with 展开为可访问变量
结果: 6

2.2 parseFunction(str, self)

1
2
3
4
5
6
7
function parseFunction(str: JSFunction, self: any) {
const fn = parseExpression(str, self); // 复用 parseExpression
if (typeof fn !== 'function') {
console.warn('parseFunction.error: not a function', str.value);
}
return fn as Function;
}

作用: 与 parseExpression 相同的执行机制,但期望返回值是一个 Function 对象。

示例:

1
2
3
4
5
输入: { type: 'JSFunction', value: 'function() { this.count++ }' }
↓ this → __self 替换
↓ with($scope) 包裹
↓ new Function 执行
输出: Function 对象,调用时 count 会自增

三、运行时上下文 — Context 类

3.1 Context 构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Context {
state: Record<string, any> = {}; // Vue reactive 状态
props: Record<string, any> = {}; // 组件 props
$refs: Record<string, any> = {}; // 模板 ref
$emit: any = null; // 触发事件
$slots: any = null; // 插槽
$watch: any = null; // 侦听器
// ... 其他 Vue 实例代理属性

constructor(private options: { dsl?: BlockSchema; attrs?: any }) {
if (options.dsl) this.__id = options.dsl.id || null;
if (options.attrs) Object.assign(this, options.attrs);
}
}

作用: Context 是 DSL 表达式执行的”宿主环境”,代理了 Vue 组件实例的所有能力。
DSL 中的 this.xxx 实际上就是访问 Context 上的属性。

3.2 ctx.setup(attrs, V) — 绑定 Vue 实例

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
setup(attrs: Record<string, any>, V: any = Vue) {
// 1. 获取当前 Vue 组件实例
const instance = V.getCurrentInstance();
if (!instance) return;

// 2. 重置缓存
this.__refs = {};
this.__refCaches = {};
this.$refs = {};

// 3. 代理 Vue 实例
this.__instance = instance.proxy;
Object.assign(this, instance.appContext.config.globalProperties);
Object.assign(this, attrs || {});

// 4. 代理 Vue 实例方法 ($emit, $nextTick, $watch 等)
CONTEXT_HOST.forEach((name) => {
this[name] = this.__instance?.[name];
});

// 5. 生命周期内自动更新代理
V.onMounted(() => { /* 重新绑定 CONTEXT_HOST */ });
V.onUnmounted(() => { /* 清理引用 */ });
V.onBeforeUpdate(() => { /* 重置缓存 */ });
}

调用时机: 在 createRenderersetup() 函数内调用,使 Context 与当前 Vue 组件实例绑定。

3.3 ctx.__parseExpression(code) / ctx.__parseFunction(code)

1
2
3
4
5
6
7
8
9
__parseExpression(code?: JSExpression | JSFunction) {
if (!code) return;
return parseExpression(code, this); // this 就是 self 参数
}

__parseFunction(code?: JSFunction) {
if (!code) return;
return parseFunction(code, this);
}

作用: Context 实例方法,将自身作为执行上下文传给 parseExpression/parseFunction。

3.4 ctx.__clone(context) — 克隆上下文

1
2
3
4
5
6
7
__clone(context: Record<string, any> = {}) {
const _context = { ...this.context, ...context };
const copy: any = { ..._context, context: _context };
copy.context.__proto__ = this.context;
copy.__proto__ = this;
return copy as Context;
}

作用: 创建 Context 的浅克隆,用于 v-for 循环。每次迭代需要独立的上下文来存储 itemindex 变量。

使用场景:

1
2
3
4
v-for="item in items"
↓ 每次迭代
ctx.__clone({ item: items[0], index: 0 }) // 子上下文 1
ctx.__clone({ item: items[1], index: 1 }) // 子上下文 2

3.5 ctx.__ref(id, ref) — 处理模板引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__ref(id: string | null = null, ref?: string | Function) {
if (!id) return undefined;
// 缓存 ref 函数避免重复创建
let refFunc = this.__refCaches[id];
if (refFunc) return refFunc;

refFunc = async (el: any) => {
await new Promise(r => setTimeout(r, 0)); // 等待 DOM 更新
let dom = el?.$el || el;
if (id) this.__refs[id] = el; // 存储到 Context.__refs
if (typeof ref === 'function') ref(el);
else if (ref) this.$refs[ref] = el; // 存储到 Vue $refs
};
this.__refCaches[id] = refFunc;
return refFunc;
}

作用: 为每个 DSL 节点创建 Vue 的 ref 回调函数,用于获取 DOM 元素引用。


四、节点渲染 — nodeRender() 核心流程

4.1 函数签名

1
2
3
4
5
6
7
8
function nodeRender(
dsl: NodeSchema, // 当前节点 Schema
ctx: Context, // 运行时上下文
V: any = Vue, // Vue 运行时引用
brothers: NodeSchema[], // 兄弟节点列表(用于 v-if/v-else 链)
isBranch: boolean, // 是否是 v-else-if/v-else 分支渲染
index: number // 节点在兄弟中的索引
): any // 返回 VNode | VNode[] | null

4.2 完整执行流程

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
nodeRender(dsl, ctx, V, brothers, isBranch, index)

├─ 1. 前置检查
│ ├─ dsl 为空或 invisible → 返回 null
│ └─ 非分支模式遇到 v-else-if/v-else → 返回 null(跳过)

├─ 2. 提取指令
│ └─ getDiretives(directives) → { vIf, vElseIf, vElse, vFor, vShow, vBind, vHtml, vModels }

├─ 3. 处理 v-if 条件链
│ ├─ vIf 条件为 false → 向后搜索兄弟节点的 v-else-if / v-else
│ ├─ 找到匹配的 v-else-if → 递归调用 nodeRender(brother, ctx, V, brothers, true)
│ └─ 找到 v-else → 递归调用 nodeRender(brother, ctx, V, brothers, true)

├─ 4. 定义内部 render(c, seq) 函数
│ │
│ ├─ 4a. 解析属性
│ │ └─ parseNodeProps(id, dsl.props, c)
│ │ ├─ deepParseNodeProps(props, c) // 递归处理 JSExpression
│ │ │ ├─ 遇到 JSExpression → c.__parseExpression() 求值
│ │ │ ├─ 遇到 JSFunction → c.__parseFunction() 生成函数
│ │ │ ├─ 遇到 Object → 递归处理每个值
│ │ │ └─ 遇到 Array → 递归处理每个元素
│ │ └─ 设置 ref 回调 c.__ref(id)
│ │
│ ├─ 4b. 解析事件
│ │ └─ parseNodeEvents(V, id, dsl.events, c)
│ │ ├─ 遍历每个事件 { name, handler: JSFunction }
│ │ ├─ c.__parseFunction(handler) → 得到实际函数
│ │ └─ 转为 Vue 格式: { onClick: fn, onInput: fn }
│ │
│ ├─ 4c. 处理特殊节点类型
│ │ └─ name === 'slot' → 渲染 Vue slot
│ │
│ ├─ 4d. 处理指令效果
│ │ ├─ vBind → Object.assign(props, 解析值) // 合并动态属性
│ │ ├─ vShow → 条件为 false 时设置 style.display = 'none'
│ │ ├─ vHtml → 设置 props.innerHTML = 解析值
│ │ └─ vModel → 双向绑定处理(区分原生/组件)
│ │
│ ├─ 4e. 处理 v-model 双向绑定
│ │ ├─ 原生 HTML 标签:
│ │ │ props.value = 解析值
│ │ │ props.onInput = (v) => { 变量 = v.target.value }
│ │ └─ Vue 组件:
│ │ props.modelValue = 解析值
│ │ props['onUpdate:modelValue'] = (v) => { 变量 = v }
│ │
│ ├─ 4f. 处理子节点
│ │ └─ childrenToSlots(V, dsl.children, c, dsl)
│ │ ├─ children 是 string → { default: () => text }
│ │ ├─ children 是 JSExpression → { default: () => 动态文本 }
│ │ └─ children 是 NodeSchema[] → createSlotsConfig() 分组
│ │ ├─ 按 slot 属性分组
│ │ └─ 每组递归调用 nodeRender()
│ │
│ └─ 4g. 创建 VNode
│ └─ Vue.createVNode(component, { key, data-vtj, ...props, ...events }, slots)

├─ 5. 处理 v-for 循环
│ └─ vForRender(directive, render, ctx)
│ ├─ 解析数组: ctx.__parseExpression(value)
│ ├─ 遍历每个元素:
│ │ ctx.__clone({ item, index }) → 创建子上下文
│ │ render(子上下文, 序号) → 生成 VNode
│ └─ 返回 VNode 数组

└─ 6. 返回结果
├─ 有 v-for → VNode 数组
└─ 无 v-for → 单个 VNode

4.3 getDiretives() — 指令分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getDiretives(directives: NodeDirective[] = []) {
// 将驼峰指令名转为标准格式:vIf → 'vIf', vFor → 'vFor'
const camelCase = (s: string) => s.replace(/([A-Z])/g, (_, c, i) => i === 0 ? c.toLowerCase() : c);

return {
vIf: directives.find(n => camelCase(n.name) === 'vIf'),
vElseIf: directives.find(n => camelCase(n.name) === 'vElseIf'),
vElse: directives.find(n => camelCase(n.name) === 'vElse'),
vFor: directives.find(n => camelCase(n.name) === 'vFor'),
vShow: directives.find(n => camelCase(n.name) === 'vShow'),
vBind: directives.find(n => camelCase(n.name) === 'vBind'),
vHtml: directives.find(n => camelCase(n.name) === 'vHtml'),
vModels: directives.filter(n => camelCase(n.name) === 'vModel'), // 注意是 filter,可以有多个
others: directives.filter(n => /* 其他自定义指令 */)
};
}

返回值: 将 directives 数组按类型分类,方便后续分别处理。

4.4 deepParseNodeProps() — 深度属性解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function deepParseNodeProps(props: any, ctx: Context): any {
// 递归终止条件
if (isJSExpression(props)) return ctx.__parseExpression(props); // 表达式 → 求值
if (isJSFunction(props)) return ctx.__parseFunction(props); // 函数 → 生成函数
if (Array.isArray(props)) return props.map(i => deepParseNodeProps(i, ctx)); // 数组 → 递归
if (typeof props === 'object' && props !== null) {
// 对象 → 递归处理每个属性值
return Object.keys(props).reduce((r, k) => {
r[k] = deepParseNodeProps(props[k], ctx);
return r;
}, {});
}
return props; // 原始值直接返回
}

递归逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
props: {
class: { type: 'JSExpression', value: 'active ? "active" : ""' },
style: { color: 'red', fontSize: { type: 'JSExpression', value: 'size + "px"' } },
items: [
{ type: 'JSExpression', value: 'list' },
'static'
]
}
↓ deepParseNodeProps
{
class: "active", // JSExpression 求值
style: { color: 'red', fontSize: '16px' }, // 嵌套递归
items: [['a','b'], 'static'] // 数组递归
}

4.5 parseNodeEvents() — 事件解析

1
2
3
4
5
6
7
8
9
10
11
function parseNodeEvents(V: any, _id: string, events: NodeEvents, ctx: Context) {
return Object.keys(events || {}).reduce((result, key) => {
const event = events[key];
const handler = ctx.__parseFunction(event.handler); // JSFunction → 真实函数
if (handler) {
// click → onClick, input → onInput
result['on' + key.charAt(0).toUpperCase() + key.slice(1)] = handler;
}
return result;
}, {});
}

转换规则:

1
2
3
DSL:  { click: { handler: JSFunction } }

Vue: { onClick: Function }

4.6 vForRender() — v-for 循环渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function vForRender(directive: NodeDirective, render: (ctx: Context, seq?: number) => any, ctx: Context) {
const { value, iterator } = directive;
const { item = 'item', index = 'index' } = iterator || {};

// 1. 解析循环数据源
let items = ctx.__parseExpression(value) || [];

// 2. 数字也可以循环: v-for="(item, index) in 3" → [1, 2, 3]
if (Number.isInteger(items)) items = new Array(items).fill(true).map((_, i) => i + 1);

if (!Array.isArray(items)) return [];

// 3. 遍历每个元素,克隆上下文并注入循环变量
return items.map((_item, _index) =>
render(ctx.__clone({ [item]: _item, [index]: _index }), _index)
);
}

执行示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
指令: { name: 'vFor', value: { type: 'JSExpression', value: 'items' }, iterator: { item: 'todo', index: 'i' } }
items = ['吃饭', '睡觉', '写代码']

↓ 第1次迭代
ctx.__clone({ todo: '吃饭', i: 0 }) → render(clone, 0) → VNode('li', {}, '吃饭')

↓ 第2次迭代
ctx.__clone({ todo: '睡觉', i: 1 }) → render(clone, 1) → VNode('li', {}, '睡觉')

↓ 第3次迭代
ctx.__clone({ todo: '写代码', i: 2 }) → render(clone, 2) → VNode('li', {}, '写代码')

结果: [VNode, VNode, VNode]

4.7 childrenToSlots() — 子节点转插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function childrenToSlots(V: any, children: any, ctx: Context, parent?: NodeSchema): any {
if (!children) return null;

// 纯文本 → 默认插槽
if (typeof children === 'string') return { default: () => children };

// 动态文本 → 默认插槽
if (isJSExpression(children)) return { default: () => toString(ctx.__parseExpression(children)) };

// 节点数组 → 按 slot 属性分组为具名插槽
if (Array.isArray(children) && children.length > 0) {
const slots = createSlotsConfig(children);
return Object.entries(slots).reduce((result, [name, { nodes }]) => {
result[name] = (scope: any) => {
const props = parent?.id && scope ? { [`scope_${parent.id}`]: scope } : {};
// 递归渲染每个子节点
return nodes.map((node, index) => nodeRender(node, ctx.__clone(props), V, nodes, false, index));
};
return result;
}, {});
}
return null;
}

createSlotsConfig() 分组逻辑:

1
2
3
4
5
children: [
{ name: 'div', slot: 'header', children: '标题' }, → slots.header
{ name: 'p', slot: 'default', children: '内容' }, → slots.default
{ name: 'span', slot: 'footer', children: '底部' } → slots.footer
]

五、状态初始化函数

5.1 createState() — 创建响应式状态

1
2
3
4
5
6
7
8
9
10
11
function createState(V: any, state: BlockSchema['state'], ctx: Context) {
return V.reactive(
Object.keys(state || {}).reduce((result, key) => {
let val = state[key];
if (isJSExpression(val)) val = ctx.__parseExpression(val); // 表达式求值
else if (isJSFunction(val)) val = ctx.__parseFunction(val); // 函数生成
result[key] = val;
return result;
}, {})
);
}

转换:

1
2
3
DSL state: { count: { type: 'JSExpression', value: '0' }, name: { type: 'JSExpression', value: '"Hello"' } }

Vue.reactive({ count: 0, name: 'Hello' })

5.2 createComputed() — 创建计算属性

1
2
3
4
5
6
function createComputed(V: any, computed: Record<string, JSFunction>, ctx: Context) {
return Object.entries(computed ?? {}).reduce((result, [k, v]) => {
result[k] = V.computed(ctx.__parseFunction(v) as any);
return result;
}, {});
}

转换:

1
2
3
DSL computed: { double: { type: 'JSFunction', value: 'function() { return this.count * 2 }' } }

{ double: Vue.computed(() => count * 2) }

5.3 createMethods() — 创建方法

1
2
3
4
5
6
function createMethods(methods: Record<string, JSFunction>, ctx: Context) {
return Object.entries(methods ?? {}).reduce((result, [k, v]) => {
result[k] = ctx.__parseFunction(v);
return result;
}, {});
}

转换:

1
2
3
DSL methods: { increment: { type: 'JSFunction', value: 'function() { this.count++ }' } }

{ increment: Function } // 调用时 count 自增

5.4 createLifeCycles() — 创建生命周期钩子

1
2
3
4
5
6
7
8
9
10
11
12
function createLifeCycles(lifeCycle: Record<string, JSFunction>, ctx: Context) {
return Object.entries(lifeCycle ?? {}).reduce((result, [k, v]) => {
const func = ctx.__parseFunction(v);
result[k] = async () => {
if (typeof func === 'function') {
await new Promise(r => setTimeout(r, 0)); // 延迟确保 Context 已绑定
await func();
}
};
return result;
}, {});
}

支持的钩子: onMounted, onUnmounted, onBeforeUpdate, onUpdated 等。
这些会展开为 defineComponent 的选项式钩子。

5.5 setWatches() — 设置侦听器

1
2
3
4
5
6
7
8
9
function setWatches(V: any, watches: any[], ctx: Context) {
watches.forEach((n) => {
V.watch(
ctx.__parseExpression(n.source), // 监听源
ctx.__parseFunction(n.handler), // 回调函数
{ deep: n.deep, immediate: n.immediate } // 选项
);
});
}

六、createRenderer() — 渲染器入口

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
export function createRenderer(options: CreateRendererOptions) {
const V = Vue;
const ctx = new Context({ dsl: options.dsl });

const renderer = V.defineComponent({
name: options.dsl?.name || 'DslRenderer',
props: {},

// async setup → 外层必须用 <Suspense> 包裹
async setup() {
ctx.state = createState(V, options.dsl?.state, ctx);
const computed = createComputed(V, options.dsl?.computed, ctx);
const methods = createMethods(options.dsl?.methods, ctx);
ctx.setup({ ...computed, ...methods }, V);
setWatches(V, options.dsl?.watch, ctx);
return { state: ctx.state, ...computed, ...methods };
},

...createLifeCycles(options.dsl?.lifeCycles, ctx),

render() {
if (!options.dsl?.nodes) return null;
const nodes = options.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 };
}

返回值:

  • renderer — 用 markRaw 标记的 Vue 组件(避免被 reactive 代理)
  • context — Context 实例(可用于外部操作)

七、通信协议 (src/canvas/protocol.ts)

7.1 消息格式

1
2
3
4
5
interface DesignerMessage {
channel: '__VTJ_DESIGNER__'; // 频道标识,过滤无关消息
type: MessageType; // 消息类型
payload?: any; // 数据载荷
}

7.2 消息类型

类型 方向 说明 payload
init Host → Canvas 初始化 DSL BlockSchema
updateDsl 双向 同步 DSL 变更 BlockSchema
select Host → Canvas 设置选中节点 nodeId | null
nodeClick Canvas → Host 用户点击节点 nodeId | null
dropNode Host → Canvas 拖拽放置组件 { schema, x, y }
ready Canvas → Host Canvas 就绪
selectedInfo Canvas → Host 新增节点选中信息 { id, node }

7.3 sendMessage(target, msg)

1
2
3
function sendMessage(target: Window, msg: DesignerMessage) {
target.postMessage(msg, '*'); // 使用 window.postMessage API
}

7.4 onMessage(fn) — 监听消息

1
2
3
4
5
6
7
8
9
function onMessage(fn: (msg: DesignerMessage) => void): () => void {
const handler = (e: MessageEvent) => {
if (e.data && e.data.channel === CHANNEL) {
fn(e.data); // 只处理本频道的消息
}
};
window.addEventListener('message', handler);
return () => window.removeEventListener('message', handler); // 返回取消函数
}

八、代码生成器 (src/coder/index.ts)

8.1 generateVueSFC(dsl) — DSL → Vue SFC 源码

将 BlockSchema 转为 .vue 单文件组件字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BlockSchema

├─ nodes → <template> 部分
│ └─ genNode() 递归生成 HTML 模板
│ ├─ props → HTML 属性
│ ├─ directives → v-if/v-for/v-model 等指令
│ ├─ events → @click/@input 等事件
│ └─ children → 递归子节点

├─ state → <script setup> reactive()
├─ computed → computed()
├─ methods → 普通函数

└─ css → <style scoped>

8.2 genNode(node, indent) — 节点转模板

1
2
3
NodeSchema: { name: 'button', props: { class: 'btn' }, children: '点击' }
↓ genNode
<button class="btn">点击</button>

指令转换:

1
2
3
4
5
6
7
8
9
10
11
{ name: 'vFor', value: 'items', iterator: { item: 'item', index: 'i' } }

v-for="(item, i) in items"

{ name: 'vIf', value: 'show' }

v-if="show"

{ name: 'vModel', value: 'text' }

v-model="text"

事件转换:

1
2
3
{ click: { handler: { type: 'JSFunction', value: 'function() { this.count++ }' } } }

@click="() { count++ }"

8.3 generateJSON(dsl) — DSL → 格式化 JSON

1
2
3
function generateJSON(dsl: BlockSchema): string {
return JSON.stringify(dsl, null, 2);
}

直接格式化输出,用于 DSL 查看和导入导出。


九、设计器集成

9.1 DesignerView → Canvas 交互流程

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
DesignerView (主页面)                      CanvasApp (iframe)
───────────────────── ───────────────────
1. iframe 加载 canvas.html
2. iframe load 事件
3. sendToCanvas('init', dsl) ─────────→ 收到 init → renderDsl()
4. createRenderer({ dsl })
5. <Suspense> 渲染
6. ←─────────── sendToHost('ready')
7. iframeReady = true

用户拖拽物料:
8. onDragStart(schema) 显示蓝色虚线遮罩
9. 鼠标移到画布上方松开
10. onMaskMouseUp()
11. sendToCanvas('dropNode', ...) ──────→ 收到 dropNode
12. addNode() → renderDsl()
13. ←─────────── sendToHost('updateDsl')
14. eng.setDSL(dsl)

用户点击节点:
15. onDocumentClick()
16. ←─────────── sendToHost('nodeClick', id)
17. selectedId = id
18. 右侧 PropsPanel 显示节点配置
19. 用户修改属性
20. eng.updateNodeProp() → syncDslToCanvas()
21. sendToCanvas('updateDsl') ─────────→ 收到 → renderDsl()

9.2 CanvasApp 内部渲染流程

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
CanvasApp.renderDsl()

├─ JSON.parse(JSON.stringify(dsl)) // 深拷贝去除 reactive 代理

├─ createRenderer({ dsl })
│ ├─ new Context({ dsl })
│ └─ defineComponent({ setup, render })

├─ renderer.value = result.renderer // 触发 Vue 重新渲染

└─ <Suspense>
└─ <component :is="renderer" />
├─ setup() 执行
│ ├─ createState() → reactive state
│ ├─ createComputed() → computed
│ ├─ createMethods() → methods
│ └─ ctx.setup() → 绑定 Vue 实例

└─ render() 执行
└─ nodes.map(n => nodeRender(n, ctx, V, nodes))
├─ 解析 props
├─ 解析 events
├─ 处理 directives
├─ 处理 children/slots
└─ createVNode() → VNode 树

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