(给前端大全加星标,提升前端技能)
转自:大转转FE/张所勇
为什么需要 wepy 转 VUE
“转转二手”是我司用 wepy 开发的功能与 APP 相似度非常高的小程序,实现了大量的功能性页面,而新业务 H5 项目在开发过程中有时也经常需要一些公共页面和功能,但新项目又有自己的独特点,这些页面需求重新开发成本很高,但如果把小程序代码转换成 VUE 就会容易的多,因此需要这样一个转换工具。
本文将通过实战带你体验 HTML、css、JavaScript 的 AST 解析和转换过程
如果你看完觉得有用,请点个赞~
AST 概览
AST 全称是叫抽象语法树,网络上有很多对 AST 的概念阐述和 demo,其实可以跟 XML 类比,目前很多流行的语言都可以通过 AST 解析成一颗语法树,也可以认为是一个 JSON,这些语言包括且不限于:CSS、HTML、JavaScript、PHP、Java、SQL 等,举一个简单的例子:
var a = 1;
这句简单的 JavaScript 代码通过 AST 将被解析成一颗“有点复杂”的语法树:
这句话从语法层面分析是一次变量声明和赋值,所以父节点是一个 type 为 VariableDeclaration(变量声明)的类型节点,声明的内容又包括两部分,标识符:a 和 初始值:1
这就是一个简单的 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版
class="userCard">
class="basic">
class="avatar">
"{
{info.portrait}}">
class="info">
class="name">{
{info.nickName}}
class="label" wx:if="{
{info.label}}">
class="label-text" wx:for="{
{info.label}}">{
{item}}
class="onsale">在售宝贝{
{sellingCount}}
class="follow " @tap="follow">{
{isFollow ? '取消关注' : '关注'}}
template>
rel="stylesheet/less" scoped>
.userCard {
position:relative;
background: #FFFFFF;
box-shadow: 0 0 10rpx 0 rgba(162,167,182,0.31);
border-radius: 3rpx;
padding:20rpx;
position: relative;
}
/* css太多了,省略其他内容 */
import wepy from 'wepy'
export default class UserCard extends wepy.component {
props = {
info:{
type:Object,
default:{}
}
}
data = {
isFollow: false,
}
methods = {
async follow() {
await someHttpRequest() //请求某个接口
this.isFollow = !this.isFollow
this.$apply()
}
}
computed = {
sellingCount(){
return this.info.sellingCount || 1
}
}
onLoad(){
this.$log('view')
}
}
//VUE版
class="userCard">
class="basic">
class="avatar">
"info.portrait">
class="info">
class="name">{
{info.nickName}}
class="label" v-if="info.label">
class="label-text" v-for="(item,key) in info.label">{
{item}}
class="onsale">在售宝贝{
{sellingCount}}
class="follow " @click="follow">{
{isFollow ? '取消关注' : '关注'}}
template>
rel="stylesheet/less" scoped>
.userCard {
position:relative;
background: #FFFFFF;
box-shadow: 0 0 10rpx 0 rgba(162,167,182,0.31);
border-radius: 3*@px;
padding:20*@px;
position: relative;
}
/* css太多了,省略其他内容 */
export default {
props : {
info:{
type:Object,
default:{}
}
}
data(){
return {
isFollow: false,
}
}
methods : {
async follow() {
await someHttpRequest() //请求某个接口
this.isFollow = !this.isFollow
}
}
computed : {
sellingCount(){
return this.info.sellingCount || 1
}