揭秘 VTJ 代码转换系统:Vue SFC 与 DSL 的双向转换实践

0 阅读8分钟

从 Vue 源码到可视化编辑,再到生成干净代码的背后引擎

在现代低代码/可视化开发工具中,如何让开发者既能够使用熟悉的 Vue 单文件组件(SFC)编写代码,又能在可视化设计器中拖拽编辑,同时保证生成的代码整洁、可维护?VTJ 的代码转换系统正是解决这一核心问题的关键基础设施。本文将深入剖析这一系统的架构设计、核心流程与实现细节,带你了解 Vue SFC 与内部 DSL 之间双向转换的技术内幕。

为什么需要代码转换系统?

VTJ 是一个面向 Vue3 的可视化开发平台,其核心能力之一是将现有的 Vue 代码转换为可拖拽编辑的“设计态”模型(DSL),同时能将设计模型重新生成为干净的“运行态”Vue SFC。这一过程需要解决几个关键挑战:

  • 解析任意 Vue SFC:支持 <script setup>、TypeScript、多种 CSS 预处理器,并处理复杂的模板指令(v-ifv-forv-model 等)。
  • 保持代码语义:在转换过程中不丢失原有逻辑,且生成的代码符合 Vue 最佳实践。
  • 运行时隔离:将原始代码中的变量引用转换为可在设计器沙箱中正确执行的表达式。
  • 自动修复常见问题:对不规范的代码进行验证和修复,确保可视化编辑的稳定性。

下面,我们将从整体架构出发,逐步拆解转换系统的各个模块。

系统整体架构

转换系统由两条核心管道组成:解析管道(Vue SFC → DSL)和生成管道(DSL → Vue SFC)。下图展示了完整的双向转换流程及关键组件:

flowchart TD
    UserVue["用户编写的 Vue SFC"]
    AIVue["AI 生成的 Vue 代码"]
    VisualDSL["可视化设计产生的 NodeSchema"]
    
    Validator["ComponentValidator<br>packages/parser/src/tools/validator.ts"]
    AutoFixer["AutoFixer<br>packages/parser/src/tools/fixer.ts"]
    
    ParseVue["parseVue()<br>packages/parser/src/vue/index.ts"]
    ParseSFC["parseSFC()<br>@vue/compiler-sfc"]
    ParseTemplate["parseTemplate()<br>packages/parser/src/vue/template.ts"]
    ParseScripts["parseScripts()<br>packages/parser/src/vue/scripts.ts"]
    ParseStyle["parseStyle()<br>packages/parser/src/vue/style.ts"]
    PatchCode["patchCode()<br>packages/parser/src/vue/utils.ts"]
    
    BlockSchema["BlockSchema<br>@vtj/core"]
    NodeSchema["NodeSchema<br>组件树"]
    BlockState["BlockState<br>响应式数据"]
    Methods["Methods<br>JSFunction"]
    Computed["Computed<br>JSFunction"]
    
    Generator["generator()<br>@vtj/coder"]
    Formatter["tsFormatter()<br>packages/coder/src/formatters.ts"]
    CleanVue["干净的 Vue SFC<br>零污染代码"]
    
    UserVue --> Validator
    AIVue --> Validator
    AutoFixer --> ParseVue
    ParseTemplate --> NodeSchema
    ParseScripts --> BlockState
    ParseScripts --> Methods
    ParseScripts --> Computed
    ParseStyle --> BlockSchema
    NodeSchema --> PatchCode
    BlockState --> PatchCode
    Methods --> PatchCode
    Computed --> PatchCode
    PatchCode --> BlockSchema
    VisualDSL --> BlockSchema
    BlockSchema --> Generator
    Generator --> Formatter
    Formatter --> CleanVue

    subgraph InputSources ["输入源"]
        UserVue
        AIVue
        VisualDSL
    end

    subgraph ValidationLayer ["验证层"]
        Validator
        AutoFixer
    end

    subgraph ParserPipeline ["解析管道"]
        ParseVue
        ParseSFC
        ParseTemplate
        ParseScripts
        ParseStyle
        PatchCode
    end

    subgraph DSLLayer ["DSL 层"]
        BlockSchema
        NodeSchema
        BlockState
        Methods
        Computed
    end

    subgraph GeneratorPipeline ["生成管道"]
        Generator
        Formatter
    end

    subgraph Output ["输出"]
        CleanVue
    end

双向转换的核心组件

转换系统被划分为多个功能模块,每个模块承担明确的职责。下表列出了主要组件及其在代码库中的位置和作用:

组件位置作用
parseVue()packages/parser/src/vue/index.ts L24-L140Vue → DSL 的主入口,协调解析过程
parseSFC()packages/parser/src/shared/utils.ts L9-L20使用 @vue/compiler-sfc 拆解 SFC 为模板、脚本、样式
parseTemplate()packages/parser/src/vue/template.ts L53-L81将模板 AST 转换为 NodeSchema
parseScripts()packages/parser/src/vue/scripts.ts L55-L145通过 Babel 解析脚本,提取组件选项(状态、方法、计算属性等)
parseStyle()packages/parser/src/vue/style.ts处理 CSS/SCSS,提取样式规则
patchCode()packages/parser/src/vue/utils.ts L501-L545调用 replacer 对表达式进行上下文转换
replacer()packages/parser/src/vue/utils.ts L198-L499核心字符串替换引擎,智能修改变量引用
ComponentValidatorpackages/parser/src/tools/validator.ts L12-L166验证 Vue SFC 的结构和语法,检测潜在问题
AutoFixerpackages/parser/src/tools/fixer.ts L6-L155自动修复常见错误(图标名称、状态前缀等)
generator()@vtj/coderBlockSchema 生成 Vue SFC 代码
tsFormatter()packages/coder/src/formatters.ts L57-L70格式化生成的 TypeScript/JavaScript 代码

接下来,我们将深入解析管道,看看 Vue SFC 是如何一步步变成可编辑的 DSL 的。

解析管道:Vue SFC → DSL

解析管道的核心任务是将 Vue 单文件组件转换为结构化的 BlockSchema 对象,该对象完整描述了组件的模板结构、逻辑状态、方法、计算属性、样式等,并且可以被可视化设计器直接操作。

下图展示了解析管道的完整流程:

flowchart TD
    VueSFC["Vue SFC 源代码"]
    SFCParser["parseSFC()<br>@vue/compiler-sfc"]
    TemplateStr["template: string"]
    ScriptStr["script: string"]
    StylesStr["styles: string[]"]

    CompileTemplate["compileTemplate()<br>@vue/compiler-sfc"]
    TemplateAST["TemplateChildNode[]"]
    TransformNode["transformNode()<br>递归遍历"]
    NodeSchemaArray["NodeSchema[]"]
    Slots["BlockSlot[]"]
    Context["Record<string, Set<string>><br>v-for/插槽上下文变量"]

    BabelParser["parseScript()<br>@babel/parser"]
    ScriptAST["Babel AST"]
    TraverseAST["traverseAST()<br>@babel/traverse"]
    ExtractSetup["提取 setup() 中的 state"]
    ExtractMethods["提取 methods 对象"]
    ExtractComputed["提取 computed 对象"]
    ExtractLifecycles["提取生命周期钩子"]
    ScriptResult["ParseScriptsResult<br>state, methods, computed, ..."]

    PatchCodeFn["patchCode()<br>转换变量引用"]
    ReplacerFn["replacer()<br>上下文感知替换"]
    WalkDSL["walkDsl()<br>遍历所有表达式"]
    ContextMapping["上下文变量映射<br>key → this.context.key"]
    ComputedMapping["计算属性映射<br>key → this.key.value"]
    LibMapping["库映射<br>key → this.$libs.Library.key"]

    PostCSS["postcss"]
    SASSParser["sass"]
    CSSRules["CSSRules<br>样式规则"]

    BlockSchemaOutput["BlockSchema<br>{id, name, nodes, state, methods, computed, ...}"]

    VueSFC --> SFCParser
    SFCParser --> TemplateStr
    SFCParser --> ScriptStr
    SFCParser --> StylesStr

    TemplateStr --> CompileTemplate
    CompileTemplate --> TemplateAST
    TemplateAST --> TransformNode
    TransformNode --> NodeSchemaArray
    TransformNode --> Slots
    TransformNode --> Context

    ScriptStr --> BabelParser
    BabelParser --> ScriptAST
    ScriptAST --> TraverseAST
    TraverseAST --> ExtractSetup
    TraverseAST --> ExtractMethods
    TraverseAST --> ExtractComputed
    TraverseAST --> ExtractLifecycles
    ExtractSetup --> ScriptResult
    ExtractMethods --> ScriptResult
    ExtractComputed --> ScriptResult
    ExtractLifecycles --> ScriptResult

    NodeSchemaArray --> WalkDSL
    ScriptResult --> WalkDSL
    WalkDSL --> PatchCodeFn
    PatchCodeFn --> ReplacerFn
    ReplacerFn --> ContextMapping
    ReplacerFn --> ComputedMapping
    ReplacerFn --> LibMapping

    StylesStr --> PostCSS
    StylesStr --> SASSParser
    PostCSS --> CSSRules
    SASSParser --> CSSRules

    ContextMapping --> BlockSchemaOutput
    ComputedMapping --> BlockSchemaOutput
    LibMapping --> BlockSchemaOutput
    Slots --> BlockSchemaOutput
    Context --> BlockSchemaOutput
    CSSRules --> BlockSchemaOutput
    NodeSchemaArray --> BlockSchemaOutput
    ScriptResult --> BlockSchemaOutput

1. SFC 结构解析

parseSFC() 是对 @vue/compiler-sfc 的简单封装,将 Vue SFC 字符串解析为 SFCDescriptor,并提取出模板、脚本和样式部分的纯文本内容。

flowchart TD
    Input["Vue SFC 字符串"]
    Parse["parse()<br>@vue/compiler-sfc"]
    Descriptor["SFCDescriptor"]
    Template["template.content"]
    Script["script.content 或 scriptSetup.content"]
    Styles["styles[].content"]

    Input --> Parse
    Parse --> Descriptor
    Descriptor --> Template
    Descriptor --> Script
    Descriptor --> Styles

返回结果包含三个核心字段:

字段类型描述
templatestring<template> 标签内的 HTML 模板
scriptstring<script><script setup> 内的 JavaScript/TypeScript 代码
stylesstring[]<style> 标签内的 CSS/SCSS 内容数组(支持多样式块)
errorsany[]编译过程中的错误信息

2. 模板解析:从 HTML 到 NodeSchema 树

parseTemplate() 接收模板字符串,使用 @vue/compiler-sfccompileTemplate 生成 AST,然后递归遍历 AST 节点,将每个元素/文本/插值转换为 NodeSchema 对象。

关键函数详解:

  • getProps() L101-L163:提取静态属性、动态绑定的 v-bind,并特殊处理 classstyle(支持对象/数组语法)。
  • getEvents() L165-L212:提取 v-on@ 事件,处理事件修饰符(.stop.prevent 等),生成 NodeEvents
  • getDirectives() L214-L324:解析 v-ifv-else-ifv-elsev-forv-modelv-showv-html 以及自定义指令,生成对应的 NodeDirective
  • pickContext() L364-L383:收集 v-for 中定义的迭代变量(如 itemindex)和插槽作用域参数,用于后续的代码修补。
  • formatTagName() utils.ts L595-L603:将 HTML 标签名转换为 PascalCase(如 el-buttonElButton),以匹配组件库的命名规范。

3. 脚本解析:提取组件逻辑

parseScripts() 是脚本处理的核心,它通过 Babel 解析代码 AST,遍历并提取组件定义中的各种选项。

解析步骤

  1. 使用 @babel/parser 将脚本代码解析为 AST。
  2. 遍历 AST,找到 export defaultdefineComponent 调用。
  3. 从组件选项对象中提取:
    • name:组件名称
    • setup 函数:进一步分析其内部的 reactive 调用,提取响应式状态
    • methods:提取普通方法(名称不匹配事件处理器正则的)
    • computed:提取计算属性
    • watch:提取侦听器
    • props:属性定义(支持数组和对象形式)
    • emits:通过分析 this.$emit 调用收集事件名
    • 生命周期钩子:onMountedonCreated
    • 数据源定义:API 调用或模拟数据

关键提取函数

函数目的输出类型
getState()从 setup 中提取 reactive({...}) 定义的响应式变量BlockState
getMethods()提取常规方法(排除事件处理器)Record<string, JSFunction>
getEventHandlers()提取名称匹配 /_[\w]{5,}$/ 的方法(通常用作事件回调)Record<string, JSFunction>
getWatchers()提取名称以 watcher_ 开头的方法(作为计算属性)Record<string, JSFunction>
getWatches()处理 watch 选项,生成带有 deep/immediate 标志的 BlockWatchBlockWatch[]
getLifeCycles()提取生命周期钩子Record<string, JSFunction>
processProps()解析 props 定义Array<string | BlockProp>
processEmits()通过遍历 this.$emit() 调用收集 emit 事件BlockEmit[]
getDataSources()提取 API 和模拟数据源定义Record<string, DataSourceSchema>

4. 代码修补:让表达式在运行时正确执行

解析管道中最具挑战性的部分是将原始 Vue 代码中的变量引用转换为能够在设计器沙箱中正确求值的形式。例如,模板中的 {{ count }} 在设计器中可能对应 this.context.countthis.state.count,具体取决于变量来源。这就是 replacer() 函数的职责。

replacer() 是一个上下文感知的字符串替换引擎,它根据 ExpressionOptions 中提供的上下文信息,智能地修改变量引用。其核心规则包括:

  • 跳过字符串字面量:单引号、双引号、反引号内的内容不替换,但模板字符串中的 ${} 表达式除外。
  • 跳过属性访问obj.key 中的 key 不替换(除非整个表达式是 key)。
  • 跳过声明constletvarfunction 后面的标识符不替换。
  • 跳过对象键{ key: value } 中的 key 不替换(计算属性 [key] 中的 key 会替换)。
  • 跳过函数参数function(key) {}(key) => {} 中的参数 key 不替换。
  • 跳过正则表达式字面量/regex/ 内的内容不替换。
  • 尊重单词边界:仅替换完整的标识符,避免部分匹配。
  • 跳过扩展运算符:检测 ...key,但 key 本身会被替换。

replacer 内部使用状态机跟踪当前是否处于字符串、模板表达式或正则表达式内部:

const state = {
  inString: false,
  quoteChar: "",
  inTemplateExpr: 0,
  inRegex: false,
  regexDepth: 0,
};

根据 ExpressionOptions 中的配置,replacer 执行四种类型的映射:

映射类型示例转换结果
上下文变量item.namethis.context.item.name
计算属性fullNamethis.fullName.value
库导入ElButtonthis.$libs.ElementPlus.ElButton
Vue 成员$emitthis.$emit

5. 样式解析

样式部分相对简单:根据样式语言(CSS、SCSS、Less 等)选择合适的解析器(PostCSS、Sass),将样式文本解析为规则对象,最终存储到 BlockSchema.css 字段中。

验证与自动修复:保证输入质量

在解析之前,所有输入的 Vue SFC 代码都会经过 ComponentValidator 的检查,并通过 AutoFixer 进行自动修复。这一层确保进入解析管道的代码是符合平台预期的,避免因格式问题导致解析失败或生成错误。

flowchart TD
    InputCode["Vue SFC 代码"]
    Validator["ComponentValidator"]
    CheckStructure["isCompleteSFC()<br>是否存在 template、script、style"]
    CheckSyntax["checkSyntax()<br>Babel 语法检查"]
    CheckSetup["checkSetup()<br>setup 是否恰好 3 条语句"]
    CheckComments["hasUnchangedComment()<br>是否包含 '不变' 标记"]
    CheckVantIcons["checkVantIcons()<br>Vant 图标名称是否合法"]
    CheckVtjIcons["checkVtjIcons()<br>@vtj/icons 导入是否合法"]
    ValidationResult["ValidationResult<br>包含错误列表和非法图标"]

    AutoFixer["AutoFixer"]
    FixVantIcons["fixVantIcons()<br>替换为默认图标"]
    FixVtjIcons["fixVtjIcons()<br>更新导入,移除非法图标"]
    FixStatePrefix["checkAndFixStatePrefix()<br>为响应式属性添加 state. 前缀"]
    ReconstructSFC["reconstructSFC()<br>重新组装 SFC"]
    ValidCode["有效的 Vue SFC"]

    InputCode --> Validator
    Validator --> CheckStructure
    Validator --> CheckSyntax
    Validator --> CheckSetup
    Validator --> CheckComments
    Validator --> CheckVantIcons
    Validator --> CheckVtjIcons
    CheckStructure --> ValidationResult
    CheckSyntax --> ValidationResult
    CheckSetup --> ValidationResult
    CheckComments --> ValidationResult
    CheckVantIcons --> ValidationResult
    CheckVtjIcons --> ValidationResult

    ValidationResult --> AutoFixer
    AutoFixer --> FixVantIcons
    AutoFixer --> FixVtjIcons
    AutoFixer --> FixStatePrefix
    FixVantIcons --> ReconstructSFC
    FixVtjIcons --> ReconstructSFC
    FixStatePrefix --> ReconstructSFC
    ReconstructSFC --> ValidCode

ComponentValidator 检查项

检查方法目的
SFC 结构isCompleteSFC()确保代码包含 template、script、style 三部分
语法checkSyntax()使用 Babel 解析脚本,捕捉语法错误
Setup 语句数checkSetup()验证 setup 函数恰好有 3 条语句(provider、state、return)
未更改标记hasUnchangedComment()检测代码中是否含有“不变”注释,表示该部分尚未完成
Vant 图标checkVantIcons()检查 <van-icon name="..."> 中的图标名是否在允许列表中
VTJ 图标checkVtjIcons()检查从 @vtj/icons 导入的图标是否存在于图标库中

AutoFixer 自动修复

  1. 修复 Vant 图标:将模板中非法的 Vant 图标名称替换为 defaultVantIcon
  2. 修复 VTJ 图标:更新脚本中的导入语句,移除非法图标导入,并添加默认图标导入。
  3. 修复状态前缀:这是最复杂的修复之一。它会扫描模板中的所有表达式(插值、指令值、绑定值等),为缺失 state. 前缀的响应式属性自动添加前缀。例如:
    • {{ name }}{{ state.name }}
    • :prop="value":prop="state.value"
    • v-if="loading"v-if="state.loading"
    • v-for="item in items"v-for="item in state.items"
    • v-model="text"v-model="state.text"
    • @click="count++"@click="state.count++"

该修复器通过解析模板 AST,结合脚本中提取的响应式变量列表,精准地插入 state. 前缀,同时避免误改(例如已带 state. 或属于局部变量的情况)。

关键数据结构

转换系统围绕几个核心数据结构展开,它们构成了 Vue 与 DSL 之间的桥梁。

BlockSchema

完整的组件 DSL 表示:

interface BlockSchema {
  id: string;               // 唯一标识
  name: string;             // 组件名称
  nodes: NodeSchema[];      // 组件树
  state?: BlockState;       // 响应式状态
  props?: Array<string | BlockProp>; // 属性定义
  methods?: Record<string, JSFunction>; // 方法
  computed?: Record<string, JSFunction>; // 计算属性
  lifeCycles?: Record<string, JSFunction>; // 生命周期钩子
  watch?: BlockWatch[];     // 侦听器
  dataSources?: Record<string, DataSourceSchema>; // 数据源
  slots?: BlockSlot[];      // 插槽定义
  emits?: BlockEmit[];      // 发射的事件
  expose?: string[];        // 暴露的成员
  inject?: BlockInject[];   // 注入的依赖
  css?: string;             // 编译后的 CSS
}

NodeSchema

表示树中的一个组件或原生元素:

interface NodeSchema {
  id?: string;                       // 节点标识
  name: string;                       // 标签名(组件使用 PascalCase)
  from?: NodeFrom;                    // 导入来源
  props?: NodeProps;                  // 属性和属性绑定
  events?: NodeEvents;                // 事件处理器
  directives?: NodeDirective[];       // Vue 指令
  children?: NodeSchema[] | JSExpression | string; // 子节点或文本内容
  slot?: BlockSlot | string;          // 所属插槽信息
}

JSFunction 与 JSExpression

代码片段被包装为带有类型标记的对象,便于在 DSL 中序列化和反序列化:

interface JSFunction {
  type: "JSFunction";
  value: string;  // 函数代码,如 "(param) => { ... }"
}

interface JSExpression {
  type: "JSExpression";
  value: string;  // 表达式代码,如 "data.value"
}

ExpressionOptions

代码修补系统的配置参数:

interface ExpressionOptions {
  platform: PlatformType;          // 'web' | 'uniapp' | 'h5'
  context: Record<string, Set<string>>; // 节点 ID → 该节点作用域内的上下文变量
  computed: string[];              // 计算属性名称列表
  libs: Record<string, string>;    // 导入名称 → 库名称(如 ElButton → ElementPlus)
  members: string[];               // 需要添加 'this.' 前缀的成员(如 $emit)
}

与项目模型的集成

解析得到的 BlockSchema 并不会直接使用,而是被包装在 BlockModel 类中,后者提供了更多的实用方法,如验证、节点查找、依赖管理等。多个 BlockModel 组成 ProjectModel,形成完整的项目级模型。

flowchart TD
    ParseVue["parseVue()"]
    BlockSchema["BlockSchema"]
    BlockModel["new BlockModel(dsl)"]
    DSL["dsl = model.toDsl()"]
    ProjectModel["ProjectModel.blocks[]"]

    ParseVue --> BlockSchema
    BlockSchema --> BlockModel
    BlockModel --> DSL
    DSL --> ProjectModel

BlockModel 位于 @vtj/core,其核心功能包括:

  • 对 DSL 进行校验和归一化
  • 提供 toDsl() 方法重新生成纯对象
  • 管理节点间的父子关系
  • 处理组件依赖(如自动导入)

使用示例

以下是一个简单的使用示例,展示如何将 Vue SFC 解析为 DSL,并随后重新生成代码:

import { parseVue } from '@vtj/parser';
import { generator } from '@vtj/coder';

// 假设有一个 Vue SFC 字符串
const vueCode = `
<template>
  <div>
    <h1>{{ title }}</h1>
    <el-button @click="handleClick">Click me</el-button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
const title = ref('Hello VTJ');
const handleClick = () => {
  title.value = 'Clicked!';
};
</script>

<style scoped>
h1 { color: red; }
</style>
`;

// 1. 解析为 DSL
const result = await parseVue({
  project: projectSchema, // 项目级信息
  id: 'comp-1',
  name: 'MyComponent',
  source: vueCode
});

console.log(result.state); // { title: { type: 'JSExpression', value: 'ref("Hello VTJ")' } }
console.log(result.methods); // { handleClick: { type: 'JSFunction', value: '() => { title.value = "Clicked!"; }' } }

// 2. 经过可视化编辑后,可能修改了 result 的某些字段
// ...

// 3. 重新生成 Vue SFC
const generatedCode = await generator(result, { platform: 'web' });
console.log(generatedCode);
// 输出格式化后的 Vue SFC,与原始代码结构一致,但可能经过了规范化处理

总结与展望

VTJ 的代码转换系统通过分层、模块化的设计,成功实现了 Vue SFC 与内部 DSL 之间的无损双向转换。其核心亮点包括:

  • 深度集成 Vue 编译器:直接复用 @vue/compiler-sfc 和 Babel,确保与 Vue 生态的兼容性。
  • 智能代码修补:通过上下文感知的 replacer 引擎,解决了变量作用域和运行时刻隔离的难题。
  • 健壮的验证与修复:在解析前进行多层检查,并自动修复常见问题,提升平台容错性。
  • 清晰的数据结构BlockSchema 完整覆盖 Vue 组件的所有方面,为可视化编辑提供了坚实基础。

未来,随着 VTJ 支持更多平台(如 uniapp、小程序),转换系统也将不断扩展,增加对应平台的特定处理逻辑,并优化代码生成的性能和可读性。

如果你对可视化开发或低代码引擎感兴趣,欢迎深入研究 VTJ 的源码,也期待你为社区贡献想法和代码!

开源仓库gitee.com/newgateway/…