共计 18318 个字符,预计需要花费 46 分钟才能阅读完成。
作者:vivo 互联网前端团队 -Wan Anwen、Hu Feng、Feng Wei、Xie Tao
进入互联网“下半场”,靠“人海战术”的研发模式曾经不再具备竞争力,如何通过技术升级晋升研发效力?前端通过 Babel 等编译技术倒退实现了工程化体系降级,如何进一步通过编译技术赋能前端开发?或者咱们 wepy 到 uniapp 编译的转换实际,能给你带来启发。
一、背景
随着小程序的呈现,借助微信的生态体系和海量用户,使服务以更加便捷形式的触达用户需要。基于此背景,团队很早布局 智能导购小程序(为 vivo 各个线下门店导购提供服务的用户经营工具)的开发。
晚期的小程序开发工程体系还不够健全,和当初的前端的工程体系相差较大,体现在对模块化,组件化以及高级 JavaScript 语法个性的撑持上。所以团队在做技术选型时,心愿克服原生小程序工程体系上的有余,通过比照最初抉择了腾讯出品的 wepy 作为整体的开发框架。
在我的项目的从 0 到 1 阶段,wepy 的确帮忙咱们实现了疾速的业务迭代,满足线下门店导购的需要。但随着工夫的推移,在技术上,社区逐渐积淀出以 uniapp 为代表的 Vue 栈体系和以 Taro 为代表的 React 栈跨端的体系,wepy 目前的社区活跃度比拟低。另外随着业务进入稳固阶段,除大量的 wepy 小程序,H5 我的项目和新的小程序都是基于 Vue 和 uniapp 来构建,团队也是心愿对立技术栈,实现更好的跨端开发能力,升高开发和保护老本,晋升研发效率。
二、思考
随着团队决定将智能导购小程序从 wepy 迁徙到 uniapp 的架构体系,咱们就须要思考,如何进行我的项目的安稳的迁徙,同时兼顾效率和品质?通过对以后的我的项目状态和技术背景进行剖析,团队梳理出 2 个准则 3 种迁徙思路。
2.1 渐进式迁徙
外围出发点,保障我的项目的平稳过渡,给团队更多的工夫,在迭代中逐渐的进行架构迁徙。心愿以此来升高迁徙中的危险和不可控的点。基于此,咱们思考两个计划:
计划一 交融两套架构体系
在目前的我的项目中引入和 uniapp 的我的项目体系,一个我的项目交融了 wepy 和 uniapp 的代码工程化治理,逐渐的将 wepy 的代码改成 uniapp 的代码,待迁徙实现删除 wepy 的目录。这种计划实现起来不是很简单,然而毛病是治理起来比较复杂,两套工程化管理机制,底层的编译机制,各种入口的配置文件等,治理起来比拟麻烦。另外团队每个人都须要消化 wepy 到 uniapp 的畛域常识迁徙,不仅仅是我的项目的迁徙也是常识体系的迁徙。
计划二 设计 wepy-webpack-loader
以 uniapp 为工程体系根底,外围思路是将现有 wepy 代码融入到 uniapp 的体系中来。咱们都晓得 uniapp 的底层依赖于 Vue 的 cli 的技术体系,最底层通过 webpack 实现对 Vue 单组件文件和其余资源文件的 bundle。
基于此,咱们能够开发一个 wepy 的 webpack 的 loader,wepy-loader 相似于 vue-loader 的能力,通过该 loader 对 wepy 文件进行编译打包,而后最终输入小程序代码。想法很简略,但咱们想要实现 wepy-loader 工作量还是比拟大的,须要对 wepy 的底层编译器进一步进行剖析拆解,剖析 wepy 的依赖关系,辨别是组件编译还是 page 编译等,且 wepy 底层编译器的代码比较复杂,实现老本较高。
2.2 整体性迁徙
构建一个编译器实现 wepy 到 uniapp 的主动代码转换。
通过对 wepy 和 uniapp 整体技术计划的梳理,加深了对两套架构差异性的认知和了解,尤其 wepy 下层语法和 Vue 的组件开发的代码上的差异性。基于团队对编译的认知,咱们认为借助 babel 等成熟编译技术是有能力实现这个转换的过程,另外,通过编译技术会极大的晋升整体的迁徙的效率。
2.3 计划比照
通过团队对计划的深刻探讨和技术预研,最终大家达成统一应用编译转换的形式(计划三)来进行本次的技术升级。最终,通过实现 wepy 到 uniapp 的编译转换器,使本来 25 人 / 天的工作量,6s 实现。
如下动图所示:
三、架构设计
3.1 wepy 和 uniapp 单文件组件转换
通过对 wepy 和 uniapp 的学习,充沛理解两者之间的差异性和相识点。wepy 的文件设计和 Vue 的单文件十分的类似,蕴含 template 和 script 和 style 的三局部组成。
如下图所示,
所以咱们将文件拆解为 script,template,style 款式三个局部,通过 transpiler 别离转换。同时这个过程次要是对 script 和 template 进行转换,款式和 Vue 能够放弃一致性最终借助 Vue 进行转换即可。
同时 wepy 还有本人的 runtime 运行时的依赖,为了确保我的项目对 wepy 做到最小化的依赖,不便后续齐全和 wepy 的依赖进行齐全解耦,咱们抽取了一个 wepy-adapter 模块,将原先对于 wepy 的依赖转换为对 wepy-adapter 的依赖。
整体转换设计,如下图所示:
3.2 编译器流水线构建
如上图所示,整个编译过程就是一条流水线的架构设计,在每个阶段实现不同的工作。次要流程如下:
3.2.1 我的项目资源剖析
不同的我的项目依赖资源不同的解决流程,扫描我的项目中的源码和资源文件进行分类,期待后续的不同的流水线解决。
动态资源文件(图片,款式文件等)不须要通过当中流水线的解决,中转指标 uniapp 我的项目的对应的目录。
3.2.2 AST 形象语法树转换
针对 wepy 的源文件(app,page,component 等)对 script,template 等局部,通过 parse 转换成绝对应的 AST 形象语法树,后续的代码转换都是基于对形象语法树的构造改良。
3.2.3 代码转换实现 – Transform code
依据 wepy 和 uniapp 的 Vue 的代码实现上的差别,通过对 ast 进行转换实现代码的转换。
3.2.4 代码生成 – code emitter
依据步骤三转换之后最终的 ast,进行对应的代码生成。
四、我的项目搭建
整体我的项目构造如下图所示:
4.1 单仓库的管理模式
应用 lerna 进行单仓库的模块化治理,不便进行模块的拆分和本地模块之间依赖援用。另外单仓库的益处在于,和我的项目相干的信息都能够在一个仓库中积淀下来,如文档,demo,issue 等。不过随着 lerna 社区不再进行保护,后续会将 lerna 迁徙到 pnpm 的 workspace 的计划进行治理。
4.2 外围模块
- wepy-adapter – wepy 运行期以来的最小化的 polyfill
- wepy-chameleon-cli – 命令行工具模块
- wepy-chameleon-transpiler – 外围的编译器模块,依照 one feature,one module 形式组织
4.3 自动化工作构建等
Makefile – *nix 世界的规范形式
4.4 scripts 自动化治理
shipit.ts 模块的主动公布等自动化能力
4.5 单元测试
- 采纳 Jest 作为根底的测试框架,应用 typescript 来作为测试用例的编写。
- 应用 @swc/jest 作为 ts 的转换器,晋升 ts 的编译速度。
- 当初社区的 vitest 间接提供了对 ts 的集成,借助 vite 带来更快的速度,打算迁徙中。
五、外围设计实现
5.1 wepy template 模版转换
5.1.1 差异性梳理
上面咱们能够先来大抵看一下 wepy 的模板语法和 uniapp 的模板语法的区别。
图:wepy 模板和 uni-app 模板
从上图能够看出,wepy 模板应用了原生微信小程序的 wxml 语法,并且在采纳相似 Vue 的组件引入机制的同时,保留了 wxml< import/ >、< include/ > 标签的能力。同时为了和 wxml 中循环渲染 dom 节点的语法做区别,引入了新的 < Repeat/ > 标签来渲染引入的子组件,而 uni-app 则是齐全应用 Vue 格调的语法来进行开发。
所以总结 wepy 和 uni-app 模板语法的次要区别有两点:
- wepy 应用了一些特定的标签用来导入或者复用其余 wxml 文件例如 < import > 和 < include >。
- wxml 应用了 xml 命名空间的形式来定义模板指令,并且对指令值的解决更像是应用模板引擎对特定格局的变量进行替换。
下表列举一些两者模板指令的对应转换关系。
此外,还有一些指令的细节须要解决,例如在 wepy 中 wx:key=”id” 指令会主动解析为 wx:key=”{{item.id}}”,这里便不再赘述。
5.1.2 外围转换设计
编译器对 template 转换次要就须要实现以下三个步骤:
- 解决 wepy 引入的非凡的标签例如。
- 将 wxml 中应用的指令、非凡标签等转换为 Vue 模板的语法。
- 收集引入的组件信息传递给上游的 wepy-page-transform 模块。
- wepy 非凡标签转换
首先咱们会解决 wepy 模板中的非凡标签 < import/ >、< include/ >,次要是将 wxml 的文件引入模式转换成 Vue 模板的组件引入模式,同时还须要收集引入的 wxml 的文件地址和展现的模板名称。因为 < include/ > 能够引入 wxml 文件中除了 < template/ > 和 < wxs/ > 的所有代码,为了保障转换后组件的复用性,咱们将引入的 xx.wxml 文件拆成了 xx.vue 和 xx-incl.vue 两个文件,应用 < import/ > 标签的会导入 xx.vue,而应用 < include/ > 标签的会导入 xx-incl.vue,转换 import 的外围代码实现如下:
transformImport() {
// 获取所有 import 标签
const imports = this.$('import')
for (let i = 0; i < imports.length; i++) {const node = imports.eq(i)
if (!node.is('import')) return
const importPath = node.attr('src')
// 收集引入的门路信息
this.importPath.push(importPath)
// 将文件名对立转换成短横线格调
let compName = TransformTemplate.toLine(path.basename(importPath, path.extname(importPath))
)
let template = node.next('template')
while (template.is('template')) {const next = template.next('template')
if (template.attr('is')) {const children = template.children()
// 生成新的组件标签例如
// <import src="components/list.wxml" />
// <template is="subList" /> => <list is="subList" />
const comp = this.$(`<${compName} />`)
.attr(template.attr())
.append(children)
comp.attr(TransformTemplate.toLine(this.compName), comp.attr('is'))
comp.removeAttr('is')
// 将以后标签替换为新生成的组件标签
template.replaceWith(comp)
}
template = next
}
node.remove()}
}
具体的 WXML 文件拆分计划请看 WXML 转换局部。
- wepy 属性转换
上文中曾经介绍了,wepy 模板中的属性应用了命名空间 + 模板字符串格调的动静属性,咱们须要将他们转换成 Vue 格调的属性。转换须要操作模板中的节点及其属性,这里咱们应用了 cheerio,疾速、灵便、类 jQuery 外围实现,能够利用 jQuery 的语法十分不便的对模板字符串进行解决。
上述流程中一个分支中的转换函数会解决相应的 wepy 属性,以保障后续能够很不便的对转换模块进行欠缺和批改。因为属性名称转换只是简略的做一下相应的映射,咱们重点剖析一下动静属性值的转换过程。
WXML 中应用双中括号来标记动静属性中的变量及 WXS 表达式,并且如果变量是 WXS 对象的话还能够省略对象的大括号例如
< view wx:for="{{list}}" > {{item}} < /view >、< template is="objectCombine" data="{{for: a, bar: b}}" >< /template >
所以当咱们取到双中括号中的值时会有以下两种状况:
- 失去 WXS 的表达式;
- 失去一个没有中括号包裹的 WXS 对象。此时咱们能够先对表达式尝试转换,如果有报错的话,给表达式包裹一层中括号再进行转换。思考到 WXS 的语法相似于 Javascript 的子集,咱们仍然应用 babel 对其进行解析并解决。
外围代码实现如下:
/**
*
* @param value 须要转换的属性值
*/
private transformValue(value: string): string {const exp = value.match(TransformTemplate.dbbraceRe)[1]
try {
let seq = false
traverse(parseSync(`(${exp})`), {enter(path) {// 因为 WXS 反对对象键值相等的缩写{{a,b,c}},故此处须要额定解决
if (path.isSequenceExpression()) {seq = true}
},
})
if (!seq) {return exp}
return `{${exp}}`
} catch (e) {return `{${exp}}`
}
}
到这里,咱们曾经可能解决 wepy 模板中绝大部分的动静属性值的转换。然而,上文也提及到了,wepy 采纳的是相似模板引擎的形式来解决动静属性的,即 WXML 反对这种动静属性 < view id=”item-{{index}}” >,如果这个 < view / > 标签应用了 wx:for 指令的话,id 属性会被编译成 item-0、item-1… 这个问题咱们也想了多种计划去解决,例如字符串拼接、正则解决等,然而都不能很好的笼罩全副场景,总会有非凡场景的呈现导致转换失败。
最终,咱们还是想到了模板引擎,Javascript 中也有相似于模板引擎的元素,那就是模板字符串。应用模板字符串,咱们仅仅须要把 WXML 中用来标记变量的双括号 {{}} 转换成 Javascript 中的 ${}即可。
5.2 Wepy App 转换
5.2.1 差异性梳理
wepy 的 App 小程序实例中次要蕴含小程序生命周期函数、config 配置对象、globalData 全局数据对象,以及其余自定义办法与属性。
外围代码实现如下:
import wepy from 'wepy'
// 在 page 中,通过 this.$parent 来拜访 app 实例
export default class MyAPP extends wepy.app {customData = {}
customFunction() {}
onLaunch() {}
onShow() {}
// 对应 app.json 文件
// build 编译时会依据 config 属性主动生成 app.json 文件
config = {}
globalData = {}}
uniapp 的 App.vue 能够定义小程序生命周期办法,globalData 全局数据对象,以及一些自定义办法,外围代码实现如下:
<script>
export default {
globalData: {text: 'text'}
onLaunch: function() {console.log('App Launch,app 启动')
},
onShow: function() {console.log('App Show,app 展示在前台')
},
onHide: function() {console.log('App Hide,app 不再展示在前台')
},
methods: {// .....}
}
<script>
5.2.2 外围转换设计
如图,外围转换设计流程:
- 对 app.py 进行 parse,拆分出 script 和 style 局部,对 script 局部应用 babel 进行 parse 生成 AST。
- 通过对 AST 剖析出,小程序的生命周期办法,globalData 全局数据,自定义办法等。
- 对于 AST 进行 uniapp 转换,生命周期办法和全局数据转成对象的办法和属性,对自定义办法转换到 method 内。
- 其中对 globalData 的拜访,要进行替换通过 getApp()进行拜访。
- 抽取 ast 中的 config 字段,输入到 app.json 配置文件。
- 抽取 wepy.config.js 中的 config 字段,传入 wepy 的 app 实例。
外围代码实现:
let APP_EVENT = ['onLaunch', 'onShow', 'onHide', 'onError', 'onPageNotFound']
//....
// 实现 wepy app 到 uniapp App.vue 的转换
t.program([...body.filter((node: t.Node) => !t.isExportDeclaration(node)),
// 插入 appClass
...appClass,
...body
.filter((node: t.Node) => t.isExportDeclaration(node))
.map((node: object) => {
// 对导出的 app 进行解决
if (t.isExportDeclaration(node)) {
// 提前 config 属性
const {appEvents, methods, props} = this.clzProperty
// 从新导出 vue style 的对象
return t.exportDefaultDeclaration(
t.objectExpression([
// mixins
...mixins,
// props
...Object.keys(props)
.filter((elem) => elem !== 'config')
.map((elem) =>
this.transformClassPropertyToObjectProperty(props[elem])
),
// app events
...appEvents.map((elem) =>
this.transformClassMethodToObjectMethod(elem)
),
// methods
t.objectProperty(t.identifier('methods'),
t.objectExpression([...methods.map((elem) =>
this.transformClassMethodToObjectMethod(elem)
),
])
),
])
)
}
return node
}),
])
// .....
5.2.3 痛点难点
在运行期,app.wpy 会继承 wepy.App 类,这样就会在运行期和 wepy.App 产生依赖关系,怎么最小化弱化这种关系。抽取 wepy 的最小化以来的 polyfill,随着业务中代码剔除对 wepy 的 api 调用,最终去除对 polyfill 的依赖。
5.3 wepy component 转换
对于 wepy component 的转换次要能够细化到对 component 中 template、script、style 三局部代码块的转换。
其中,style 局部因为曾经兼容 Vue 的标准,所以咱们无需做额定解决。而 template 模块次要是须要对 wepy template 中非凡的标签、属性、事件等内容进行解决,转化为适配 uni 的 template,上文做了具体的阐明。
咱们只须要专一于解决 script 模块的代码转换即可。从架构设计的思路来看,component script 的转换次要是是做以下两件事:
- 编译期可确定代码块的转换。
- 运行期动静注入代码的兼容。
wepy-component-transform 就是基于以上这两个规范设计进去的实现转换逻辑的模块。
5.3.1 差异性梳理
首先先解释一下什么是“编译期可确定代码块”,咱们来看一个 wepy 和 Vue 语法比照示例:
从直观上来说,这个 script 的模板的语法大抵和 Vue 语法相似,这意味着咱们解析进去的 AST 构造和 Vue 文件对应的 AST 构造上相似,基于这一点来看编译转换的工作量大抵有底了。
从细节来看,wpy 文件 script 模块中的 API 语法和 Vue 中有申明及应用上的不同,其中蕴含:
- wepy 本身的包依赖注入及运行时依赖
- props/data/methods 申明形式不同
- 生命周期钩子不同
- 事件公布 / 订阅的注册和监听机制不同。
- …. 等等
为了确定这个第 5 点等等还存在哪些应用场景,咱们须要对 wepy 本身的逻辑和玩法有一个详尽的理解和相熟,通过在团队内组织的 wepy 源码走读,再联合 wepy 理论生产我的项目中的代码互相印鉴,咱们最终才将 wepy 语法逻辑与 uni-app Vue 语法逻辑的异同梳理分明。
5.3.2 外围转换设计
咱们简略梳理一下 wepy-component-transform 这个模块的构造,能够分为以下三个局部:
- 预处理 wepy component script 代码 AST 节点局部
- 构建 Vue AST
- 通过 generate 吐出代码
1. 预处理 AST
基于前文转换设计这一节咱们晓得,wepy 变色龙的转换器中对代码的 AST 解析次要依赖 babel AST 三板斧(traverse、types、generate)来实现,通过剖析各个差别点代码语句转换后的 AST 节点,就能够通过 traverse 中的钩子来进行节点的前置解决,这里安利一下 https://astexplorer.net/,咱们能够通过它疾速剖析代码块 AST 节点、模仿场景及验证转换逻辑:
预处理 AST,目标是提前将 wepy 源码中的代码块解析为 AST Node 节点后,按语法进行归集到预置的 clzProperty 对象中,其中:
- props 对象用来盛放 ClassProperty 语法的 ast 节点
- notCompatibleMethods 数组用来盛放非生命周期函数白名单内的函数 AST 节点。
- appEvents 数组用来盛放生命周期函数白名单内的函数 AST 节点。
- listenEvents 数组用来盛放 公布 / 订阅事件注册的函数 AST 节点。
外围代码实现如下所示:
import {NodePath, traverse, types} from '@babel/core'
this.clzProperty = {props: {},
notCompatibleMethods: [],
appEvents: [],
listenEvents: []}
traverse() {ClassProperty: (path) => {
const name = path.node.key.name
this.clzPropertyprops[name] = path.node
},
ClassMethod: (path) => {
const methodName = path.node.key.name
// 判断是否存在于生命周期白名单内
const isCompEvent = TOTAL_EVENT.includes(methodName)
if (isCompEvent) {this.clzProperty.appEvents.push(path.node)
} else {this.clzProperty.notCompatibleMethods.push(path.node)
}
},
ObjectMethod: (path: any) => {if (path.parentPath?.container?.key?.name === 'events') {this.clzProperty.listenEvents.push(path.node)
}
}
}
这里要留神一点,因为对 wepy 来说,实际上 page 也属于 component 的一种实现,所以两者的 event 会有肯定的重合,而且因为 wepy 中生命周期和 Vue 生命周期的差异性,咱们须要对如 attached、detached、ready 等钩子做一些 hack。
2. 构建 Vue AST
buildCompVueAst 函数即为 构建 Vue AST 局部。从直观上来看,这个函数只做了一件事,即用 types.program 从新生成一个 AST 节点构造,而后将原有的 wepy 语法转换为 vue 语法。然而实际上咱们还须要解决许多额定的兼容逻辑,简略列举一下:
- created 重叠问题
- methods 中函数的收集
- events 中函数的调用解决
created 重叠问题次要是为了解决 created/attached/onLoad/onReady 这 4 个生命周期函数都会转换为 created 导致的多次重复申明问题。咱们须要针对若存在 created 重叠问题时,将其余钩子中的代码块取出并 push 到第一个 created 钩子函数外部。代码示例如下:
const body = this.ast.program.body
const {appEvents, notCompatibleMethods, props, listenEvents} =
this.clzProperty
// 解决多个 created 生命周期重叠问题
const createIndexs: number[] = []
const sameList = ['created', 'attached', 'onLoad', 'onReady']
appEvents.forEach((node, index) => {
const name: string = node.key.name
if (sameList.includes(name)) {createIndexs.push(index)
}
})
if (createIndexs.length > 1) {
// 取出源节点内代码块
const originIndex = createIndexs[0]
const originNode = appEvents[originIndex]
const originBodyNode = originNode.body.body
// 留下的残余节点须要取出其代码块并塞入源节点中
// 塞入实现后删除残余节点
createIndexs.splice(0, 1)
createIndexs.forEach((index) => {const targetNode = appEvents[index]
const targetBodyNode = targetNode.body.body
// 将源节点内代码块塞入指标节点中
originBodyNode.push(...targetBodyNode)
// 删除源节点
appEvents.splice(index, 1)
})
}
因为 wepy 中非 methods 中函数的特殊性,所以咱们须要在转换时将独立申明的函数、events 中的函数都抽离进去再 push 到 methods 中,伪代码逻辑如下所示:
buildCompVueAst() {
const body = this.ast.program.body
return t.program([...body.map((node) => {
return t.exportDefaultDeclaration(
t.objectExpression([...Object.keys(props)
.map((elem) => {if (elem === 'methods') {const node = props[elem]
// 1.events 内函数插入 methods 中
// 2. 与生命周期平级的函数抽离进去插入 methods 中
node.value.properties.push(
...listenEvents,
...notCompatibleMethods
)
}
return props[elem]
})
])
)
})
])
}
events 中函数的调用解决次要是为了抹平 wepy 中公布订阅事件调用和 Vue 调用的差异性。在 wepy 中,事件的注册通过在 events 中申明函数,事件的调用通过 this.$emit 来触发。而 vue 中咱们采纳的是 EventBus 计划来兼容 wepy 中的写法,即手动为 events 中的函数创立 this.$on 模式的调用,并将其代码块按程序塞入 created 中来初始化。
首先咱们要判断文件中是否已有 created 函数,若存在,则获取其对应的代码块并调用 forEachListenEvents 函数将 events 中的监听都 push 进去。
若不存在,则初始化一个空的 created 容器,并调用 forEachListenEvents 函数。外围代码实现如下所示:
buildCompVueAst() {const obp = [] as types.ObjectMethod[]
// 获取 class 属性和办法
const body = node.declaration.body.body
const targetNodeArray = body.filter(child =>
child.key.name === 'created'
)
if (targetNodeArray.length > 0) {let createdNode = targetNodeArray[0]
this.forEachListenEvents(createdNode)
} else {
const targetNode = t.objectMethod(
'method',
t.identifier('created'),
[],
t.blockStatement([])
)
this.forEachListenEvents(targetNode)
if (targetNode.body && targetNode.body.body.length > 0) {obp.push(targetNode)
}
}
return obp
}
forEachListenEvents 函数次要是通过 wepy 中 申明的 events 事件名和入参,借助 babel types 手动创立对应的 AST Node,最终生成对应的形如 this.eventBus.on(“canceldeposit”, this.canceldeposit) 模式的监听,其中,this.canceldeposit 为原有 events 中的事件被移入 methods 后的函数,相干伪代码实现如下所示:
// 依据 events 中的 methods 构建事件监听的调用
// 并塞入 created 中
forEachListenEvents(targetNode: types.ObjectMethod) {this.clzProperty.listenEvents.forEach((item) => {
const methodsNode: any = item
// 形如 this.$on('test', ()=>{})
if (methodsNode?.key?.name) {
// 创立 this 表达式
const thisEx = t.thisExpression()
// 创立 $on 表达式
const ide = t.identifier('$eventBus.$on')
// 合并 this.$on 表达式
const om = t.memberExpression(thisEx, ide)
// 创立事件名称参数节点
const eventNameIde = t.stringLiteral(methodsNode.key.name.toString().trim())
// 获取办法体内代码内容节点
const meNode = t.memberExpression(t.thisExpression(),
t.identifier(methodsNode.key.name.toString().trim())
)
const ceNode = t.callExpression(om, [eventNameIde, meNode])
const esNode = t.expressionStatement(ceNode)
// 将合成后的代码插入到 created 中
targetNode.body.body.push(esNode)
}
})
}
3.emitter vue 代码生成
构建完 Vue AST 之后,咱们能够调用 generate 函数生成源码字符串:
transform() {const ast = this.buildCompVueAst()
const compVue = this.genCode(ast)
return {compVue, wxs: this.buildWxs() }
}
5.4 Wepy page 转换
5.4.1 差异性梳理
下面的章节曾经给大家剖析了 template、component 的代码转换逻辑,这一节次要带大家一起看下如何转换 page 文件。page 转换的逻辑即如何实现 wepy 的 page.wpy 模块转换为 uniapp 的 page.vue 模块。
首先咱们来看下 wepy 的 page 小程序实例:
<script>
import wepy from 'wepy';
import Counter from '../components/counter';
export default class Page extends wepy.page {config = {};
components = {counter1: Counter};
data = {};
methods = {};
events = {};
onLoad() {};
// Other properties
}
</script>
<template lang="wxml">
<view>
</view>
<counter1></counter1>
</template>
<style lang="less">
/** less **/
</style>
能够看到,wepy 的 page 类也是通过继承来实现的,页面文件 page.wpy 中所申明的页面实例继承自 wepy.page 类,该类的次要属性介绍如下:
5.4.2 外围转换设计
基于 page 的 api 个性以及实现计划,具体的转换设计思路如下:
5.4.3 痛点难点
1. 非阻塞异步与异步
在进行批量 pages 转换时,须要同时对 pages.json 进行读取、批改、再批改的操作,这就波及到应用阻塞 IO/ 异步 IO 来解决文件的读写,当应用异步 IO 时,会发动多个过程同时解决 pages.json, 每个读取实现后独自解决对应的内容,数据不是串行批改,最终导致最终批改的内容不合乎预期,因而在遇到并行处配置文件时,须要应用阻塞式 io 来读取文件,保障最终数据的唯一性,具体代码如下:
// merge pageConfig to app config
const rawPagesJson = fs.readFileSync(path.join(dest, 'src/pages.json'))
// 数据操作
fs.writeFileSync(path.join(dest, 'src', 'pages.json'),
prettJson(pagesJson)
)
2. 简单的事件机制
在转换过程中,咱们也碰到一个比拟大的痛点:page.wepy 继承至 wepy.page,wepy.page 代码较简单,须要将明确局部独自抽离进去。例如说 events 中组件间数据传递:$broadcast
、$emit
、$invoke
,$broadcast
、$invoke
须要相熟其应用场景,转换为 Vue 中公共办法。
5.5 Wepy WXML 转换
template 转换章节中提到了 wepy 模板中能够间接引入 wxml 文件,然而 uni-app 应用的 Vue 模板不反对间接引入 wxml,故咱们须要将 wxml 文件解决为 uniapp 能够引入的 Vue 文件。咱们先来看一下 wepy 中引入的 wxml 文件的大抵构造。
<template name="foo">
<view class="foo-content">
<text class="text1">{{item.text1}}</text>
<image class="pic" src="{{pic.url}}" mode="aspectFill"></image>
</view>
</template>
<template name="bar">
<view class="bar-content">
<image class="bar" src="{{pic.url}}" mode="aspectFill"></image>
<text class="text2">{{item.text2}}</text>
</view>
</template>
<view class="footer">
this is footer
</view>
<!-- index.wepy -->
<!-- 引入文件 -->
<import src="somePath/fooBar.wxml" />
<!-- 确定展现的 template 及传入属性 -->
<script is="foo" data="{{item, pic}}" />
<!-- or, 此时仅会展现 <template/> 以外的内容即 footer -->
<include src="somePath/fooBar.wxml">
5.5.1 差异性梳理
从下面的代码能够看出,一个 WXML 文件中反对多个不同 name 属性的 < template/ > 标签,并且反对通过在引入设置 data 来传入属性。从下面的示例模板中咱们能够剖析出,除了须要将 wepy 应用的 WXML 语法转换成 vue 模板语法外(这里的转换交给了 template 模块来解决),咱们还须要解决以下的问题。
- 确定引入组件时的传参格局
- 确定组件中传入对象的属性有哪些
- 解决 < import/ > 和 < include/ > 引入的文件时的状况
5.5.2 外围转换设计
1. 确定引入组件时的传入属性形式
首先须要将 wepy 组件引入模式改成 Vue 的组件引入形式。以下面的代码为例,行将 < import/ >、< script/ > 对的引入模式改写成 < component-name / > 引入形式。咱们会在转换开始前对代码进行扫描,收集模板中的引入文件信息,传递给 wepy-page-transform 模块解决,在转换后的 Vue 组件的 < script/ > 中进行引入。并且将 < script is=”foo” data=”{{item, pic}}” / > 转换为 < FooBar is=”foo” :data=(待定)/ >。这里就须要确定属性传递的形式。
从下面的代码中能够看到,在 WXML 文件的 < template/ > 会主动应用传入的 data 属性作为隐式的命名空间,从而不须要应用 data.item 来获取 item 属性。这里很天然的就会想到原来的 < script is=”foo” data=”{{item, pic}}” / > 能够转换成 < FooBar compName=”foo” :key1=”val1″ :key2=”val2″ … / >。
其中,key1,val1,key2,val2 等为原 data 属性对象中的键值对,compName 用来指定展现的局部。这样解决的益处是,引入的 WXML 文件中应用相应的传入的属性就不须要做额定的批改,并且比拟合乎咱们个别引入 Vue 组件时传入属性的形式。
尽管这种计划能够较少的改变 WXML 文件中的模板,然而因为传入的对象可能会在运行期间进行批改,咱们在编译期间比拟难以确定传入的 data 对象中的键值对。思考到实现的工夫老本及难易水平,咱们没有抉择这种计划。
目前咱们所采纳的计划是不去扭转原有的属性传入形式,行将组件引入标签转换为 < FooBar compName=”foo” :data=”{item, pic}” / >。从而省去剖析传入对象在运行时的变动。这里就引出了第二个问题,如何确定组件中传入的参数有哪些。
2. 确定组件中的传入的对象属性
因为 Vue 的模板中不会主动应用传入的对象作为命名空间,咱们须要手动的找到以后待转换的模板中所应用到的所有的变量。相应的代码如下:
searchVars() {
const self = this
const domList = this.$('template *')
// 获取 wxml 文件中 template 节点下的所有 text 节点
const text = domList.text()
const dbbraceRe = new RegExp(TransformTemplate.dbbraceRe, 'g')
let ivar
// 拿到所有被 {{}} 包裹的动静表达式
while ((ivar = dbbraceRe.exec(text))) {addVar(ivar[1])
}
// 遍历所有节点的属性,获取所有的动静属性
for (let i = 0; i < domList.length; i++) {const dom = domList.eq(i)
const attrs = Object.keys(dom.attr())
for (let attr of attrs) {const value = dom.attr(attr)
if (!TransformTemplate.dbbraceRe.test(value)) continue
const exp = value.match(TransformTemplate.dbbraceRe)[1]
try {addVar(exp)
} catch (e) {addVar(`{${exp}}`)
}
}
}
function addVar(exp: string) {traverse(parseSync(`(${exp})`), { // 利用 babel 剖析表达式中的所有变量
Identifier(path) {
if (path.parentPath.isMemberExpression() &&
!path.parentPath.node.computed &&
path.parentPath.node.property === path.node
)
return
self.vars.add(path.node.name) // 收集变量
},
})
}
}
收集到所有的变量信息后,模板中的所有变量后面须要加上传入的对象名称,例如 item.hp\_title 须要转换成 data.item.hp\_title。思考到模板的简洁性和后续的易维护性,咱们把转换对立放到 < script/ > 的 computed 字段中对立解决即可:
<template>
<!--...-->
</template>
<script>
export default {props: ['data', 'compName'],
computed: {item() {return data.item},
pic() {return data.pic}
}
}
</script>
3. 解决 < import/ > 和 < include/ > 两种引入形式
wepy 模板有两种引入组件的形式,一种是应用 < import/ >< script/ > 标签对进行引入,还有一种是应用 < include/ > 进行引入,< include/ > 会引入 WXML 文件中除了 < template/ > 和 < wxs/ > 的其余标签。这里的解决形式就比较简单,咱们把 < include/ > 会引入的局部独自抽取进去,生成 TItem-incl.vue 文件,这样即保障了生成代码的可复用性,也升高 < import/ > 标签引入的局部生成的 TItem.vue 文件中的逻辑复杂度。生成的两个文件的构造如下:
<!--TItem.vue-->
<template>
<view>
<template v-if="compName =='foo'">
<view class="foo">
<!--...-->
</view>
</template>
<template v-if="compName =='bar'">
<view class="bar">
<!--...-->
</view>
</template>
</view>
</template>
<script>
export default {props: ['compName', 'data'],
computed: {item() {return this.data.item},
pic() {return this.data.pic}
}
}
</script>
<!--TItem-incl.vue-->
<template>
<view>
<view class="footer">
this is footer
</view>
</view>
</template>
六、阶段性成绩
截止到目前,司内的企微导购小程序我的项目通过接入变色龙编译器曾经顺利的从 wepy 迁徙到了 uniApp 架构,本来预计须要 25 人 / 天 的迁徙工作量在应用了编译器转换后缩短到了 10s。这不仅仅只是进步了迁徙的效率,也升高了迁徙中的常识迁徙老本,给后续业务上的疾速迭代奠定的扎实的根底。
迁徙后的企微导购小程序我的项目经测试阶段验证业务性能 0 bug,目前曾经顺利上线。后续咱们也会继续收集其余相似的业务诉求,帮忙业务兄弟们低成本实现迁徙。
七、总结
研发能效的晋升是个永恒的话题,此次咱们从编译这个角度登程,和大家分享了从 wepy 到 uniapp 的架构降级摸索的过程,通过构建代码转换的编译器来晋升整体的架构降级效率,通过编译器消化底层的畛域和常识的差异性,获得了不错的成果。
当然,咱们目前也有还不够欠缺的中央,如:编译器脚手架不足对于局部个性颗粒度更细的管制、代码编译转换过程中日志的输入更敌对等等。后续咱们也有打算将 wepy 变色龙编译器在社区开源共建,届时欢送大家一起参加进来。
现阶段编译在前端的应用场景越来越多,或者咱们真的进入了 Compiler is our framework 的时代。