DSL 到 Vue 代码生成
DSL 到 Vue 代码生成系统将 VTJ 基于块的 DSL 模式转换为生产就绪的 Vue 3 组件。此转换管道弥合了可视化设计器输出与可执行代码之间的差距,实现了与标准 Vue 开发工作流的无缝集成,同时保留了设计器的意图并保持了类型安全。
架构概览
代码生成架构遵循四阶段管道,通过收集、解析、模板编译和格式化阶段处理 DSL 模式。这种模块化设计允许在保持统一转换逻辑的同时进行特定于平台的变体(web、h5、uniapp)。
该系统利用收集器-收集者模式,在转换之前提取元数据,从而实现上下文感知的代码生成,解析依赖关系,跟踪组件使用情况,并维护正确的导入语句。
核心生成管道
生成器函数入口点
主要的生成函数接受灵活的输入配置,支持传统的参数传递和现代的选项对象。这种双重兼容性既确保了向后兼容性,又为新实现提供了更清晰的 API。
typescript
export async function generator(
_dsl: BlockSchema | GeneratorOptions,
_componentMap: Map<string, MaterialDescription> = new Map(),
_dependencies: Dependencie[] = [],
_platform: PlatformType = "web",
_formatterDisabled?: boolean,
) {
const maybeOptions: any = _dsl;
const options: GeneratorOptions =
typeof maybeOptions.dsl === "object" && arguments.length === 1
? maybeOptions
: {
dsl: _dsl,
componentMap: _componentMap,
dependencies: _dependencies,
platform: _platform,
formatterDisabled: _formatterDisabled,
};
const {
dsl,
componentMap = new Map(),
dependencies = [],
platform = "web",
formatterDisabled = false,
ts = true,
scss = false,
} = options;
const collecter = new Collecter(cloneDeep(dsl), dependencies);
const token = parser(collecter, componentMap, platform);
const script = scriptCompiled(token);
const vue = vueCompiled({
template: token.template || `\n<!--组件模版内容-->\n`,
css:
(await cssFormatter(token.css, formatterDisabled)) ||
`\n/* 组件样式内容 */\n`,
script: await tsFormatter(script, formatterDisabled),
style: await cssFormatter(token.style, formatterDisabled),
scriptLang: ts ? "ts" : "js",
styleLang: scss ? "scss" : "css",
});
return await vueFormatter(vue, formatterDisabled).catch((e) => {
e.content = vue;
return Promise.reject(e);
});
}
生成器编排四个关键操作:用于隔离的 DSL 深度克隆、元数据收集、语义解析、模板编译和代码格式化。每个阶段都会生成中间产物,供后续转换使用。
收集阶段
Collecter 类遍历 DSL 树以提取代码生成所需的关键元数据。这包括库依赖、导入语句、上下文引用、URL 模式和块插件引用。
typescript
export class Collecter {
imports: Record<string, Set<string>> = {};
context: Record<string, Set<string>> = {};
style: Record<string, string> = {};
urlSchemas: Record<string, NodeFromUrlSchema> = {};
blockPlugins: Record<string, NodeFromPlugin> = {};
members: Set<string> = new Set();
constructor(
public dsl: BlockSchema,
public dependencies: Dependencie[],
) {
this.collectLibrary();
this.walk(dsl);
}
private walk(dsl: BlockSchema) {
this.walkNodes(dsl);
this.collectContext(dsl);
this.collectStyle(dsl);
}
private walkNodes(dsl: BlockSchema) {
if (dsl.nodes) {
dsl.nodes.forEach((node) => {
this.collectUrlSchema(node);
this.collectBlockPlugin(node);
if (node.children) {
if (Array.isArray(node.children)) {
node.children.forEach((child) => {
this.walkNodes({ nodes: [child] } as BlockSchema);
});
} else if (typeof node.children !== "string") {
// 处理 JSExpression 子节点
}
}
});
}
}
}
收集器使用正则表达式模式(例如 this.$libs.ElementPlus.ElButton)识别外部库引用,提取库名称和特定组件路径。这些引用被存储以供后续生成导入语句使用,同时从生成的代码中移除以防止运行时错误。
解析阶段
解析器将 DSL 元素转换为统一的 token 结构,作为模板编译的基础。每个 DSL 方面------状态、props、方法、事件、指令------都有专门的解析器函数。
解析器入口点协调这些单独的解析器:
typescript
export function parser(
collecter: Collecter,
componentMap: Map<string, MaterialDescription>,
platform: PlatformType = "web",
): Token {
const { dsl } = collecter;
const computedKeys = Object.keys(dsl.computed || {});
const lifeCycles = parseFunctionMap(dsl.lifeCycles, computedKeys);
const computed = parseFunctionMap(dsl.computed, computedKeys);
const watch = parseWatch(dsl.watch, computedKeys);
const dataSources = parseDataSources(dsl.dataSources);
const { methods, nodes, components, importBlocks, directives } =
parseTemplate(
dsl.nodes || [],
componentMap,
computedKeys,
collecter.context,
);
const mergeComputed = [...computed, ...watch.computed];
const mergeMethods = parseFunctionMap(
{
...methods,
...(dsl.methods || {}),
},
computedKeys,
);
const blocksImport = importBlocks.map((n: any) => {
return `import ${n.name} from './${n.id}.vue';`;
});
let { imports, uniComponents } = parseImports(
componentMap,
components,
blocksImport,
collecter.imports,
platform,
);
const asyncComponents = Object.keys({
...collecter.urlSchemas,
...collecter.blockPlugins,
});
const urlSchemas = parseUrlSchemas(collecter.urlSchemas);
const blockPlugins = parseBlockPlugins(collecter.blockPlugins);
return {
id: dsl.id as string,
version: dsl.__VERSION__ as string,
name: dsl.name,
state: parseState(dsl.state).join(","),
inject: parseInject(dsl.inject).join(","),
props: parseProps(dsl.props).join(","),
emits: parseEmits(dsl.emits).join(","),
expose:
dsl.expose && dsl.expose.length ? JSON.stringify(dsl.expose || []) : "",
watch: watch.watches.join(","),
lifeCycles: lifeCycles.join(","),
computed: mergeComputed.join(","),
methods: [...dataSources, ...mergeMethods].join(","),
imports: "\n" + imports.join("\n"),
components: skipUniComponents(components, uniComponents).join(","),
directives: directives.join(","),
returns: collecter.members.join(","),
template: nodes.join("\n"),
css: dsl.css || "",
style: parseStyle(collecter.style),
urlSchemas: urlSchemas.join("\n"),
blockPlugins: blockPlugins.join("\n"),
asyncComponents: asyncComponents.join(","),
uniComponents,
renderer: platform === "uniapp" ? "@vtj/uni-app" : "@vtj/renderer",
};
}
模板解析
模板解析是最复杂的转换,它将节点模式转换为 Vue 模板语法,同时处理 props、事件、指令和嵌套子节点。
typescript
export function parseTemplate(
children: NodeSchema[],
componentMap: Map<string, MaterialDescription>,
computedKeys: string[] = [],
context: Record<string, Set<string>> = {},
parent?: NodeSchema,
) {
const nodes: string[] = [];
let methods: Record<string, JSFunction> = {};
let components: string[] = [];
const defineDirectives: string[] = [];
let importBlocks: { id: string; name: string }[] = [];
const slots = groupBySlot(children);
slots.forEach((item) => {
const contents: string[] = [];
for (const child of item.children) {
let { id, name, invisible, from } = child;
if (invisible) {
continue;
}
// 收集组件名称
const component = getComponentName(name, componentMap, from);
if (component) {
components.push(component);
}
// 收集块引用
if (isFromSchema(from)) {
importBlocks.push({ id: from.id, name });
}
// 收集 props 和 events
const { props, events, handlers } = parsePropsAndEvents(
child,
id as string,
child.props,
child.events,
context,
computedKeys,
);
const directives = parseDirectives(
child.directives,
computedKeys,
defineDirectives,
).join(" ");
const nodeChildren = child.children
? parseNodeChildren(
child.children,
computedKeys,
componentMap,
context,
child,
)
: "";
Object.assign(methods, handlers);
let childContent = "";
if (typeof nodeChildren === "string") {
childContent = nodeChildren;
} else {
childContent = (nodeChildren?.nodes || []).join("\n");
Object.assign(methods, nodeChildren?.methods || {});
components = components.concat(nodeChildren?.components || []);
importBlocks = importBlocks.concat(nodeChildren?.importBlocks || []);
}
const tagName = ["@dcloudio/uni-h5", "@dcloudio/uni-ui"].includes(
(from || componentMap.get(name)?.package) as string,
)
? kebabCase(name)
: isFromUrlSchema(from) || isFromPlugin(from)
? "component"
: name;
contents.push(
NO_END_TAGS.includes(tagName)
? `<${tagName} ${directives} ${props} ${events} />`
: `<${tagName} ${directives} ${props} ${events}>${childContent ? "\n" + childContent.trim() : ""}</${tagName}>`,
);
}
const node = wrapSlot(item.slot, contents.join("\n"), parent?.id);
nodes.push(node);
});
return {
nodes,
methods,
directives: directivesRegister(defineDirectives),
components: dedupArray(components) as string[],
importBlocks: dedupArray<{ id: string; name: string }>(importBlocks, "id"),
};
}
模板解析器会自动处理来自 @dcloudio 包的 uniapp 组件的组件名称短横线命名转换,同时为标准 web 组件保留大驼峰命名。这确保了与特定于平台的命名约定的兼容性。
事件和指令处理
事件在转换时具有上下文感知能力,当表达式直接引用 this.* 时生成内联处理程序,或者在需要上下文数据时创建包装方法:
typescript
function bindNodeEvents(
id: string,
events: NodeEvents = {},
context: Record<string, Set<string>> = {},
) {
const handlers: Record<string, JSFunction> = {};
const nodeContext = Array.from(context[id] || new Set([]));
const eventParams = nodeContext.length
? `({${nodeContext.join(", ")}}, args)`
: "";
const binders = Object.entries(events).map(([name, value]) => {
const isExp = value.handler.value.startsWith("this.");
const binder = isExp
? replaceThis(value.handler.value)
: `${camelCase(name)}_${id}${eventParams}`;
if (!isExp) {
handlers[binder] = nodeContext.length
? {
type: "JSFunction",
value: `{
return (${value.handler.value}).apply(this, args);
}`,
}
: value.handler;
}
return bindEvent(name, value, binder, nodeContext, isExp);
});
return {
binders,
handlers,
};
}
指令会被自动解析和注册,内置指令将得到特殊处理:
typescript
export const BUILT_IN_DIRECTIVES = [
"show",
"if",
"else-if",
"else",
"for",
"model",
"on",
"bind",
"text",
"html",
"once",
"pre",
"cloak",
"slot",
"is",
];
来源: template.ts
状态和 Props 转换
状态属性被转换为 Vue 3 响应式引用,并自动移除 this. 前缀以使代码更整洁:
typescript
export function parseState(state: BlockState = {}) {
return Object.entries(state).map(([key, val]) => {
const value = parseValue(val);
return `${key}: ref(${value})`;
});
}
Props 被转换为带有类型推断的 Vue 组件 prop 定义:
typescript
export function parseProps(props: Array<string | BlockProp> = []) {
return toArray(props).map((item) => {
const prop = typeof item === "string" ? { name: item } : item;
const { name, defaultValue, type = "String", required = false } = prop;
const defVal = parseValue(defaultValue, true, false);
const result = [`${name}: { type: ${type}, required: ${required} }`];
if (defaultValue !== undefined) {
result[0] = `${name}: { type: ${type}, required: ${required}, default: ${defVal} }`;
}
return result.join(",");
});
}
模板编译阶段
生成的 token 对象被注入到模板字符串中,以生成最终的脚本和 Vue 组件结构。
脚本模板
typescript
const scriptTemplate = `
// @ts-nocheck
<%= imports %>
import { useProvider } from '<%= renderer %>';
export default defineComponent({
name: '<%= name %>',
<% if(inject) { %> inject: { <%= inject %>}, <% } %>
<% if(components) { %> components: { <%= components %> }, <% } %>
<% if(directives) { %> directives: { <%= directives %> }, <% } %>
props: { <%= props %> },
emits: [<%= emits %>],
<% if(expose) { %> expose: <%= expose %>, <% } %>
setup(props, { expose }) {
const { state, methods, computed, lifeCycles, watch } = useProvider();
const { <%= returns %> } = methods;
return {
<%= returns %>,
...toRefs(state)
};
}
});
`;
Vue 模板
typescript
const vueTemplate = `
<template>
<%= template %>
</template>
<script lang="<%= scriptLang %>">
<%= script %>
</script>
<style lang="<%= styleLang %>" scoped>
<%= css %>
<%= style %>
</style>
`;
模板编译使用 @vtj/base 的模板函数进行高效的字符串插值,并进行正确的转义。
代码格式化阶段
最后阶段应用 Prettier 格式化以确保所有生成文件的代码风格一致。单独的格式化器处理不同的代码类型:
typescript
export async function vueFormatter(content: string, disabled?: boolean) {
if (disabled) return content;
return format(content, {
...prettierOptions,
parser: "vue",
plugins: [htmlParser, babelParser, cssParser, estree],
});
}
export async function tsFormatter(content: string, disabled?: boolean) {
if (disabled) return content;
return format(content, {
...prettierOptions,
parser: "babel-ts",
plugins: [babelParser, estree],
});
}
export async function cssFormatter(content: string, disabled?: boolean) {
if (disabled) return content;
return format(content, {
...prettierOptions,
parser: "css",
plugins: [cssParser],
});
}
格式化器选项强制执行一致的格式化规则:
- 箭头函数括号:始终
- 括号间距:启用
- 行宽:80 个字符
- 单引号:JSX 启用
- 行尾:LF
DSL 到 Vue 映射
转换遵循 DSL 模式元素与 Vue 组件结构之间的系统映射规则:
| DSL 元素 | Vue 输出 | 示例转换 |
|---|---|---|
state 属性 |
ref() 响应式引用 |
list: { type: 'JSExpression', value: '[]' } → list: ref([]) |
computed |
计算属性 | total: { type: 'JSFunction', value: '() => this.state.count * 2' } → total: computed(() => state.count * 2) |
methods |
方法对象 | fetchData: { type: 'JSFunction', value: 'async () => {}' } → fetchData: async () => {} |
lifeCycles |
生命周期钩子 | mounted: { type: 'JSFunction', value: '() => {}' } → mounted: () => {} |
nodes |
Vue 模板 HTML | 将节点树转换为类 JSX 的 Vue 模板语法 |
events |
事件处理程序 | @click 绑定与包装方法以进行上下文访问 |
directives |
Vue 指令 | v-if, v-for, v-model 带有正确的语法 |
props |
组件 props | props: { title: { type: String, required: true } } |
watch |
监听选项 | watch: { count: { immediate: true, deep: false, handler: '() => {}' } } |
特定于平台的变体
生成器支持三个平台,并具有特定于平台的适配:
Web 平台
- 使用
@vtj/renderer作为运行时提供程序 - 保留大驼峰组件名称
- 完整的 Element Plus、Ant Design Vue 和 Vant 支持
- 启用 CSS 作用域样式
H5 平台
- 移动端优化的组件选择
- 触摸事件处理优化
- 响应式视口配置
UniApp 平台
- 使用
@vtj/uni-app作为运行时提供程序 - uni-app 组件的短横线命名转换
- 特定于平台的条件导入
- 支持使用
rpx单位进行响应式布局
平台检测发生在生成时,而不是运行时,确保仅包含平台相关的导入和依赖项,从而优化包大小。
值转换的实用函数
转换依赖于处理不同值类型和上下文替换的实用函数:
typescript
export function parseValue(
val: unknown,
stringify: boolean = true,
noThis: boolean = true,
computedKeys: string[] = [],
) {
let value = isJSCode(val)
? val.value.trim().replace(/;$/, "")
: stringify
? JSON.stringify(val)
: val;
value = replaceComputedValue(value as string, computedKeys);
return noThis
? replaceThis(replaceContext(value as string))
: replaceContext(value as string);
}
export function replaceThis(content: string) {
return content.replace(new RegExp("this.", "g"), "");
}
export function replaceContext(content: string) {
return content.replace(/this\.context\??\./g, "");
}
export function replaceComputedValue(
content: string = "",
keys: string[] = [],
) {
let result = content;
for (const key of keys) {
result = result.replace(
new RegExp(`this.${key}.value`, "g"),
`this.${key}`,
);
}
return result;
}
完整生成工作流
导入管理
系统根据组件使用情况和依赖项智能地管理导入:
typescript
export function parseImports(
componentMap: Map<string, MaterialDescription>,
components: string[] = [],
importBlocks: string[] = [],
collectImports: Record<string, Set<string>> = {},
platform: PlatformType = "web",
) {
const imports: string[] = [];
// 处理来自收集器的库导入
for (const [library, members] of Object.entries(collectImports)) {
if (library === "Vue") continue;
const memberArray = Array.from(members);
if (memberArray.length > 0) {
imports.push(`import { ${memberArray.join(", ")} } from '${library}';`);
}
}
// 处理组件导入
components.forEach((name) => {
const material = componentMap.get(name);
if (material) {
const { library, package: pkg } = material;
if (library) {
imports.push(`import { ${name} } from '${library}';`);
} else if (pkg && pkg.startsWith("uni-")) {
// UniApp 组件使用运行时注册
} else {
imports.push(`import { ${name} } from '${pkg}';`);
}
}
});
// 添加块导入
imports.push(...importBlocks);
return { imports, uniComponents: [] };
}
配置选项
GeneratorOptions 接口提供对代码生成的全面控制:
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
dsl |
BlockSchema |
必需 | 用于生成代码的 DSL 模式 |
componentMap |
Map<string, MaterialDescription> |
new Map() |
用于导入解析的组件元数据 |
dependencies |
Dependencie[] |
[] |
用于库导入的外部依赖项 |
platform |
PlatformType |
'web' |
目标平台:'web'、'h5' 或 'uniapp' |
formatterDisabled |
boolean |
false |
跳过 Prettier 格式化以加快生成速度 |
ts |
boolean |
true |
生成 TypeScript 代码(JavaScript 则为 false) |
scss |
boolean |
false |
使用 SCSS 作为样式(CSS 则为 false) |
错误处理和验证
生成器在每个阶段都包含内置的错误处理。格式化阶段在保留生成内容用于调试的同时捕获错误:
typescript
return await vueFormatter(vue, formatterDisabled).catch((e) => {
e.content = vue;
return Promise.reject(e);
});
这确保了即使格式化失败(例如由于语法无效),未格式化的生成代码也可用于检查和调试。
与设计器集成
代码生成系统旨在与 VTJ 的可视化设计器无缝集成。当设计器在可视化编辑器中保存更改时,DSL 会自动重新生成,并可作为生产就绪的 Vue 组件导出。这种往返工作流实现了:
- 具有即时代码生成的可视化开发
- 与设计器同步的手动代码编辑
- 通过版本控制的 DSL 文件进行团队协作
- 从单个 DSL 源生成多个目标
有关从 Vue 源代码到 DSL 的反向转换的更多信息,请参阅 Vue 源代码到 DSL 解析。
高级用法
自定义组件映射集成
要在生成的代码中使用自定义组件,请提供具有正确元数据的组件映射:
typescript
const componentMap = new Map([
[
"MyButton",
{
name: "MyButton",
package: "@my-company/ui",
library: "MyUI",
categoryId: "components",
props: [{ name: "label", label: "Label", setters: "InputSetter" }],
},
],
]);
const code = await generator({
dsl: blockSchema,
componentMap,
platform: "web",
});
处理数据源
DSL 中定义的数据源被转换为响应式数据获取方法:
typescript
export const dataSources = {
userApi: {
type: "JSFunction",
value: `async () => {
return await fetch('/api/user').then(res => res.json());
}`,
},
};
生成为:
typescript
const userApi: () => Promise<any> = async () => {
return await fetch("/api/user").then((res) => res.json());
};
监听配置
监听选项在转换时会正确处理 deep 和 immediate:
typescript
export const watch = {
count: {
type: "JSFunction",
value: "(val, oldVal) => { console.log(val, oldVal); }",
deep: false,
immediate: true,
},
};
DSL 到 Vue 代码生成系统提供了一个强大、生产就绪的转换管道,在保持设计器意图的同时生成干净、可维护的 Vue 代码。模块化架构允许进行特定于平台的调整,并易于扩展以满足自定义需求。
后续步骤
- 探索 Vue 源代码到 DSL 解析 以了解反向转换
- 查看 模板编译和 AST 转换 以获取更深入的编译详细信息
- 了解关于 内置组件库 的组件集成
- 查看 项目结构和文件组织 以了解生成输出的位置
参考资料
开源项目仓库:gitee.com/newgateway/...