从 Wepy 到 UniApp 变形记( 五 )

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 的转换主要是是做以下两件事:

  1. 编译期可确定代码块的转换 。
  2. 运行期动态注入代码的兼容 。
wepy-component-transform 就是基于以上这两个标准设计出来的实现转换逻辑的模块 。
5.3.1 差异性梳理首先先解释一下什么是“编译期可确定代码块”,我们来看一个 wepy 和 Vue 语法对比示例:
从 Wepy 到 UniApp 变形记

文章插图
从直观上来说,这个 script 的模板的语法大致和 Vue 语法类似,这意味着我们解析出来的 AST 结构和 Vue 文件对应的 AST 结构上类似,基于这一点来看编译转换的工作量大致有底了 。
从细节来看,wpy 文件script 模块中的 API 语法和 Vue 中有声明及使用上的不同,其中包含:
  1. wepy 自身的包依赖注入及运行时依赖
  2. props/data/methods 声明方式不同
  3. 生命周期钩子不同
  4. 事件发布/订阅的注册和监听机制不同 。
  5. ....等等
为了确定这个第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 节点、模拟场景及验证转换逻辑:
从 Wepy 到 UniApp 变形记

文章插图
预处理 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.namethis.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)}}}

经验总结扩展阅读