背景
在面对大型且高度组件化的项目时,传统的开发模式——即边预览边手动修改代码,往往会因项目结构的复杂性而显得效率低下,尤其是对于新加入项目或对项目结构不够熟悉的开发者而言,从UI界面逆向定位到具体代码实现并作出修改的过程尤为耗时且挑战重重。为了解决这一问题,提升开发效率与团队协作的流畅度,我们构想了一种更为智能和直观的开发辅助方案,旨在通过UI直接映射到代码组件,并简化代码编辑过程,将源码AST与UI做绑定, 直接通过UI界面修改属性或内容,并自动同步到代码文件中,实现一种直观且高效的可视化编程体验,并通过这种方式来维护以及新建项目。
传统低代码方案带来的问题
在深入探讨基于源码的可视化编程技术方案之前,我们先来分析一下传统低代码方案所带来的问题吧。
传统低代码方案通过自定义的私有协议与可视化视图做绑定,将代码逻辑抽象成私有描述,使用 JSON 配置来生成页面,后续只能通过维护JSON schema来维护项目。
功能和组件通常有一定的限制,无法完全满足所有复杂的、高度定制化的开发需求。因为是私有协议,所以定位问题也会比较困难。
公有协议
在摒弃私有协议,追求广泛兼容与协同工作的背景下,软件开发社区积极拥抱公有协议和标准。其中,抽象语法树(AST)作为解析JavaScript(及其他编程语言)代码并转化为结构化数据的核心技术,已成为一个公认的公有协议。AST还为开发者提供了一种统一、标准化的方式来理解和操作源代码。借助AST我们可以非常容易的对代码进行描述以及代码和协议之间的互转。
VUE AST Transform
AST的生成通常涵盖代码的词法分析和语法分析阶段:
-
词法分析:将源代码分解成一个个有意义的单元(tokens),如关键字、标识符、运算符等。
-
语法分析:根据语言的语法规则,将tokens组织成AST。在这个过程中,会构建出表示源代码结构的树状结构,其中每个节点对应源代码中的一个构造。
如下所示:
AST Transform通常包括以下几个步骤:
-
解析(Parsing):将源代码转换为AST
-
转换(Transformation):遍历AST并对其进行修改
-
生成(Generation):将修改后的AST转换回源代码文本
关键词说明
-
VariableDeclaration 变量声明
-
Expression:表达式,MemberExpression、BinaryExpression、UnaryExpression、AssignmentExpression、CallExpression、BindExpression、NewExpression 等...
-
Statement:语句,由 Expression 组成
-
...
可以通过访问https://astexplorer.net在线调试理解更多关键词类型。
解析
js可以通过babel进行ast的解析,那vue文件该怎么解析成ast呢,你首先想到的肯定是@vue/compiler-sfc这个包,我们来看一下效果。
可以看到descriptor字段里分别用template,script/scriptSetup,styles来描述vue文件的三个代码区块,所以可以将descriptor看成是vue的ast,理论上来讲我们可以通过修改各个区块的ast节点,最后再通过将descriptor转回vue代码即可。很可惜的是官方设计@vue/compiler-sfc这个包主要是为了将vue代码编译为可以在浏览器运行的render function,并没有提供transform的能力,即使修改了ast,也无法将ast转成vue-sfc代码。
解决方案
为了实现对vue-sfc代码的AST修改并重新生成vue-sfc代码,我们可以采取一种分而治之的策略,针对代码中的不同区块(template, script/scriptSetup, styles)使用不同的工具或库进行处理。通过上述vue ast的数据结构可以看到每个区块都有content字段表示当前区块的源代码,那是不是可以把template和script交给各自的解析器呢?(style本文不做解析),script区块的代码我们可以直接交给生态比较成熟的babel来处理,那template呢,我们可以想一下template 默认的 lang 属性是什么?是不是html,那是不是只要找到对应的html解析器就可以了。
流程图
template区块的处理
通过对比多个html解析器,最终选择了node-html-parser来对template区块的代码来做ast的解析和处理。
对比其他解析器,这个包对ast的处理实现了HTMLElement 接口,使用起来非常方便,就像处理dom节点一样简单。
注意:有一个坑,解析修饰符会有问题,仓库issu中有解决方法,需要自己改一下源码打补丁
更多使用方法可参考WebAPI HTMLElement 接口,例如:
关联组件
组件在渲染的时候会被编译成template中的内容,那如果我需要在点击某一个组件的时候获取到该组件的props该怎么做呢?
可以使用node-html-parser给每个标签节点新增一个唯一标识,比如在每个标签的class中新增一个有固定前缀的随机唯一id,点击的时候通过获取该id,
然后通过node-html-parser提供的querySelector(".${id}")获取。
script区块的处理
下文使用到的babel工具库:
-
@babel/parser(对源代码进行AST解析)
-
@babel-traverse (遍历ast修改对应节点)
-
@babel/generator(ast转成源代码)
-
@babel/types(用于处理ast 节点的 lodash 式工具库)
-
@babel/template(通过字符串模板来创建ast)
一些简单的案例
获取ref
获取computed
computed的获取和ref其实大同小异
比如我们新增这样一段代码
获取生命周期
修改ref的值
修改onMounted回调里的执行语句
@babel/traverse提供了replaceWith,replaceWithSourceString等replace方法来修改AST
方式1:
可以直接把表达式字符串传递给replaceWithSourceString来对AST进行修改
方式2(推荐):
replaceWithSourceString传递声明表达式语句会报错,推荐使用@babel/template创建ast,减少容错,然后传递给replaceWith
将
改成
style区块的处理
那么同理style区块的代码也可以使用对应的解析器来处理,可以参考一些比较成熟的loader,如css-loader,postcss-loader等
生成vue-sfc代码
实现思路
1.将通过node-html-parser修改过的templateAST重新生成template代码,使用prettier格式化
2.将通过babel修改的scriptAST重新生成代码,使用prettier格式化,
3.最后将格式化后的template代码,js代码和没处理过的style区块的代码进行重组来达到生成vue-sfc代码的目的。
代码实现
效果展示
按住⌥option可进行编辑模式,hover选中组件,点击可以对组件进行可视化修改
修改 → 同步源码
重新渲染视图
流程图
未来展望
目前还在探索阶段,一些功能还处于开发阶段,未来将支持组件的拖拽,样式修改,全局状态,接口的管理,打通AI,git仓库,CICD,内部物料库组件等等
-End-
作者丨Gengar