为什么需要 wepy 转 VUE
“转转二手”是我司用 wepy 开发的功能与 APP 相似度非常高的小程序,实现了大量的功能性页面,而新业务 H5 项目在开发过程中有时也经常需要一些公共页面和功能,但新项目又有自己的独特点,这些页面需求重新开发成本很高,但如果把小程序代码转换成 VUE 就会容易的多,因此需要这样一个转换工具。
本文将通过实战带你体验 HTML、css、JavaScript 的 AST 解析和转换过程
如果你看完觉得有用,请点个赞~
AST 概览
AST 全称是叫抽象语法树,网络上有很多对 AST 的概念阐述和 demo,其实可以跟 XML 类比,目前很多流行的语言都可以通过 AST 解析成一颗语法树,也可以认为是一个 JSON,这些语言包括且不限于:CSS、HTML、JavaScript、PHP、Java、SQL 等,举一个简单的例子:
var
这句简单的 JavaScript 代码通过 AST 将被解析成一颗“有点复杂”的语法树:
![0c48f2dce5e4768f2c5bac5b473048ef.png](https://i-blog.csdnimg.cn/blog_migrate/5f4beea0057a333078d0d2d271e83e37.jpeg)
这句话从语法层面分析是一次变量声明和赋值,所以父节点是一个 type 为 VariableDeclaration(变量声明)的类型节点,声明的内容又包括两部分,标识符:a 和 初始值:1
![e60bf84d7061b2514c03ea718f6cb5c9.png](https://i-blog.csdnimg.cn/blog_migrate/f4fa728759e8815fab82ea556d4b8bea.jpeg)
这就是一个简单的 AST 转换,你可以通过 astexplorer(https://astexplorer.net/)可视化的测试更多代码。
AST 有什么用
AST 可以将代码转换成 JSON 语法树,基于语法树可以进行代码转换、替换等很多操作,其实 AST 应用非常广泛,我们开发当中使用的 less/sass、eslint、TypeScript 等很多插件都是基于 AST 实现的。
本文的需求如果用文本替换的方式也可能可以实现,不过需要用到大量正则,且出错风险很高,如果用 AST 就能轻松完成这件事。
AST 原理
AST 处理代码一版分为以下两个步骤:
词法分析
词法分析会把你的代码进行大拆分,会根据你写的每一个字符进行拆分(会舍去注释、空白符等无用内容),然后把有效代码拆分成一个个 token。
语法分析
接下来 AST 会根据特定的“规则”把这些 token 加以处理和包装,这些规则每个解析器都不同,但做的事情大体相同,包括:
- 把每个 token 对应到解析器内置的语法规则中,比如上文提到的 var a = 1;这段代码将被解析成 VariableDeclaration 类型。
- 根据代码本身的语法结构,将 tokens 组装成树状结构。
各种 AST 解析器
每种语言都有很多解析器,使用方式和生成的结果各不相同,开发者可以根据需要选择合适的解析器。
JavaScript
- 最知名的当属 babylon,因为他是 babel 的御用解析器,一般 JavaScript 的 AST 这个库比较常用
- acron:babylon 就是从这个库 fork 来的
HTML
- htmlparser2:比较常用
- parse5:不太好用,还需要配合 jsdom 这个类库
CSS
- cssom、csstree 等
- less/sass
XML
- XmlParser
wepy 转 VUE 工具
接下来我们开始实战了,这个需求我们用到的技术有:
- node
- commander:用来写命令行相关命令调用
- fs-extra:fs 类库的升级版,主要提高了 node 文件操作的便利性,并且提供了 Promise 封装
- XmlParser:解析 XML
- htmlparser2:解析 HTML
- less:解析 css(我们所有项目统一都是 less,所以直接解析 less 就可以了)
- babylon:解析 JavaScript
- @babel/types:js 的类型库,用于查找、校验、生成相应的代码树节点
- @babel/traverse:方便对 JavaScript 的语法树进行各种形式的遍历
- @babel/template:将你处理好的语法树打印到一个固定模板里
- @babel/generator:生成处理好的 JavaScript 文本内容
转换目标
我们先看一段简单的 wepy 和 VUE 的代码对比:
//wepy版
//VUE版
转换代码实现
我们先写个读取文件的入口方法
const
在 fileHandle 函数中,我们可以得到代码的文本内容,首先我们将对其进行 XML 解析,把 template、css、JavaScript 拆分成三部分。 有同学可能问为什么不直接正则匹配出来,因为开发者的代码可能有很多风格,比如有两部分 style,可能有很多意外情况是使用正则考虑不到的,这也是使用 AST 的意义。
//首先需要完成Xml解析及路径定义:
不同节点的处理逻辑,定义在一个叫做 typesHandler 的对象里面存放,接下来我们看下不同类型代码片段的处理逻辑
因篇幅有限,本文只列举一部分代码转换的目标,实际上要比这些更复杂
接下来我们对代码进行转换:
模板处理
转换目标
- 模板标签转换:把 view 转换成 div,把 image 标签转换成 img
- 模板逻辑判断:wx:if="{{info.label}}" 转换成 v-if="info.label"
- 模板循环:wx:for="{{info.label}}" 转换成 v-for="(item,key) in info.label"
- 事件绑定:@tap="follow" 转换成 @click="follow"
核心流程
- 首先把拿到的目标文本解析成语法树,然后进行各项转换,最后把语法树转换成文本写入到文件
let
- TemplateParser 是我封装的一个简单的模板 AST 处理类库,(因为使用了 htmlparser2 类库,该类库的调用方式有点麻烦),我们看下代码:
const
- 3、接下来我们看下具体替换过程:
//html标签替换规则,可以添加更多
css 处理
转换目标
- 将 image 替换为 img
- 将单位 rpx 转换成 *@px
核心过程
- 1、我们要先对拿到的 css 文本代码进行反转义处理,因为在解析 xml 过程中,css 中的特殊符号已经被转义了,这个处理逻辑很简单,只是字符串替换逻辑,因此封装在 utils 工具方法里,本文不赘述。
let
- 2、根据节点属性中的 type 来判断是 less 还是普通 css
if
- 3、less 内容的处理:使用 less.render()方法可以将 less 转换成 css;如果是 css,直接对 styleText 进行处理就可以了
less
- 4、将 image 选择器换成 img,这里也需要替换更多标签,比如 text、icon、scroll-view 等,篇幅原因不赘述
const
- 5、将 rpx 替换为*@px
replacedStyleText
- 6、将转换好的代码写入文件
replacedStyleText
JavaScript 转换
转换目标
- 去除 wepy 引用
- 转换成 vue 的对象写法
- 去除无用代码:this.$apply()
- 生命周期对应
核心过程
在了解如何转换之前,我们先简单了解下 JavaScript 转换的基本流程:
![ce011d81eb931c71f90248e1365cccb3.png](https://i-blog.csdnimg.cn/blog_migrate/c6abdc88722e7380e1cec04ac7a25db0.jpeg)
借用其他作者一张图片,可以看出转换过程分为解析->转换->生成 这三个步骤。
具体如下:
- 1、先把 xml 节点通过 toString 转换成文本
v
- 2、再进行反转义(否则会报错的哦)
let
- 3、接下来初始化一个解析器
let
这个解析器里封装了什么呢,看代码:
const
值得注意的是:babylon 的 plugins 配置有很多,如何配置取决于你的代码里面使用了哪些高级语法,具体可以参见文档或者根据报错提示处理
- 4、在解析之前可以先通过 beforeParse 方法去除掉一些无用代码(这些代码通常比较固定,直接通过字符串替换掉更方便)
javascriptContent
- 5、再把文本解析成 AST
javascriptParser
- 6、通过 AST 遍历整个树,进行各种代码转换
let
componentConverter 是转换的方法封装,转换过程略复杂,我们先了解几个概念。
假如我们拿到了 AST 对象,我们需要先对他进行遍历,如何遍历呢,这样一个复杂的 JSON 结构如果我们用循环或者递归的方式去遍历,那无疑会非常复杂,所以我们就借助了 babel 里的traverse这个工具,文档:babel-traverse(https://babeljs.io/docs/en/babel-traverse)。
- traverse 接受两个参数:AST 对象和 vistor 对象
- vistor 就是配置遍历方式的对象
- 主要有两种:
- 树状遍历:主要通过在节点的进入时机 enter 和离开 exit 时机进行遍历处理,进入节点之后再判断是什么类型的节点做对应的处理
const
- 按类型遍历:traverse 帮你找到对应类型的所有节点
const
本文代码主要使用了树状遍历的方式,代码如下:
const
本文的各种 vistor 主要做一个事,把各种类属性和方法收集起来,基类代码:
class
这里还需要补充讲下@babel/types这个类库,它主要是提供了 JavaScript 的 AST 中各种节点类型的检测、改造、生成方法,举例:
//类型检测
通过上面的处理,我们已经把 wepy 里面的各种类属性和方法收集好了,接下来我们看如何生成 vue 写法的代码
- 7、把转换好的 AST 树放到预先定义好的 template 模板中
convertedJavascript
看下 componentTemplateBuilder 这个方法如何定义:
const
这里就用到了@babel/template这个类库,主要作用是可以把你的代码数据组装到一个新的模板里,模板如下:
const
*生命周期需要进行对应关系处理,略复杂,本文不赘述
- 8、把模板转换成文本内容并写入到文件中
let
这里用到了@babel/generate类库,主要作用是把 AST 语法树生成文本格式
上述过程的代码实现总体流程
const
上面就是 wepy 转 VUE 工具的核心代码实现流程了
通过这个例子希望大家能了解到如何通过 AST 的方式进行精准的代码处理或者语法转换
如何做成命令行工具
既然我们已经实现了这个转换工具,那接下来我们希望给开发者提供一个命令行工具,主要有两个部分:
注册命令
- 1、在项目的 package.json 里面配置 bin 部分
{
- 2、写好代码后,npm publish 上去
- 3、开发者安装了你的插件后就可以在命令行以fancy xxxx的形式直接调用命令了
编写命令调用代码
#!/usr/bin/env node
convert 命令对应的代码:
const
fileHandle 这块的代码最开始已经讲过了,忘记的同学可以从头再看一遍,你就可以整个串起来这个工具的整体实现逻辑了
结语
至此本文就讲完了如何通过 AST 写一个 wepy 转 VUE 的命令行工具,希望对你有所收获。
最重要的事: 我司 转转 正在招聘前端高级开发工程师数名,有兴趣来转转跟我一起搞事情的,请发简历到zhangsuoyong@zhuanzhuan.com
转载请注明来源及作者:张所勇@转转