本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
前言
之前对公司七八年的老项目进行了升级,将 vue2 升级到 vue3,并输出了一篇文章,传送门
但它存在很多问题,具体来说:
- 可读性巨差
以下边对 filters 的处理举例,你很难一眼看出来它到底在做什么,光是正则就要脑子宕机好一会儿
- 识别不准确
以下边对 methods 的处理举例,针对 methods 不存在的情况就没办法处理,必须手动在. vue 文件中增加占位符
之所以当时能接受,是因为项目中的大多数页面基本都有该属性配置,要改动的点特别的少
- 不智能
在拒绝 gogocode,vue2 升级 vue3,看这里一文中笔者也说了,目标是半自动化。以项目中使用到的 render 函数、slot 插槽举例
项目中有多少呢?说出来也许吓你一跳
公司项目大部分是以 h 函数引用的,有 718 个
而 slot 有 1112 个
这些当时基本是手动一个一个改的,虽然也有通过正则替换的,但并不可靠,当时也是给我搞的挺 tnn 的
安装与使用
// 安装yarn add patch-vue3// 使用const patchVue3 = require('patch-vue3').default;new patchVue3({ // 配置项,详见文档 identifier: {...}, config:{...}}),效果预览
测试源码在example/test.vue下,笔者此处仅展示结果
- template 部分
- script 部分
在 methods 中,黄色是注入的部分,红色是转换的部分
render 语法中,黄色是对 props 的处理,红色是对事件绑定的处理
目标
实现一个 webpack 插件,对于 vue2 和 vue3 差异的部分,实现一键转换
正文
我始终认为,思路大于开发,因此,本文之分享核心实现思路,细节概不涉及
首先,要选一个打包工具,并确定输出,笔者这里选择 cjs 和 esm 两种输出格式,
由于 webpack 的 loader 需要是字符串形式,且需要指向打包后的最终地址,因此,需要设计成双出口
下一步就是来确定实现方式,想要对代码进行转换,无非先定位,后重写
重写的方式无二,只能基于字符串 rewrite
定位要不就是正则匹配,要不就是 ast,显然前文已经证明了前者的不可行,故选择 ast
那问题就变成了如何 ast 化?
- 解析 sfc
通过@vue/compiler-sfc可以拿到. vue 文件的基本信息,这包括了 script 和 template 部门的源码
- 解析转换 script
使用ast-kit提供的babelParse接口
- 解析转换 template
使用vue-template-compiler提供的compiler接口
接着,我们来简单设计下整个应用程序的风格
首先,定义 ast 基类,它负责对 ast 树做解析或遍历等操作
在具体处理 script 或 template 时就可以基于它做扩展
处理 script
- 思路
由于在 vue2 中的 script 代码,本质上是按属性分类的,所以我们要搞一个批量自动触发调用的机制,而不是一堆 if else 做判断然后分发处理
要想不改变原有代码的写法,最好的方法是将语法的变动层注入到 methods 中,这就保证 methods 必须要在最后一步被程序触发,对应在源码中,它必须在配置项的最后一个,显然这不可能要求开发者这么做,也违背了 “无感” 原则
所以,第一步就是做一些格式化处理
这包括代码格式化,这样操作,能减少对逗号存在性处理的心智负担
还有就是关于 methods 的位置处理,它应该总是在最后,即使原本没有
最后是关于 render 函数的导入的处理,需要将其收集并从源码中剔除,并等待最后重新注入
当每一段处理程序执行的时候,只需要基于 ast 的标记进行识别并分发给具体的处理函数重写就可以了
- 重难点
1 - 处理顺序
在处理的时候要特别注意处理顺序,因为字符串是基于magic-string包的,该包会把处理过的字符串位置进行标记,已经处理过的再次处理会报错
因此,在每次处理前,需要进行下 reverse,按从后往前的顺序
(ps:关于顺序的处理涉及很多,并非简单的数组反转,感兴趣的可以看下源码)
2 - 更新 ast 节点
在处理 render 时,由于函数中有可能仍有需要处理的语法
这样就涉及到了递归,需要对函数体内的语法先行处理,再回过头来继续处理 on 对应的部分
这就会产生节点的不一致,因此,还需要对节点进行更新
3 - 避免重复处理
由于walkAST本质上是一次深度遍历,默认情况下,他会对每一个节点依次访问一遍,那就有可能处理过的节点被二次处理
笔者一开始是在全局维护了 repaired 数组来进行标记,后来觉得不够优雅,就去大致翻了下源码,可以像如下这样做,调用 ast 树上的 remove 接口就可以了
处理 template
说实话,这个可坑死我了!!!
在一开始阶段,笔者是基于@vue/compiler-dom进行的 ast 化,实现过程很顺利
在正式向项目里接入时候却不停报错,看了报错后才意识到,可能是解析包的问题,因为它报的错误信息与源码毫无关系
遂,转为vue-template-compiler
但vue-template-compiler依旧很坑,它虽然解析正常,也有 ast tree。但结构却与正常认知的 ast 大不相同
具体来说
它没有节点在源码中的对应位置信息
为此,需要自己去拉取对应的 html 结构
组件的 slot 是挂载在当前节点的
为此,需要自己手动实现 traverseNode
还有一点,由于对应的 html 结构是自己实现的,它只能拉取最顶层的 html 部分,对于子 html 结构是无能为力的。至少,在当前版本中是这样
为此,就不能使用magic-string包了,因为没法保证先子后父,从后向前,故,需要基于原生 js 实现。为了代码结构的一致性,得模拟一个
剩下的,就和 script 差不多,都是找到指定的标记,然后分发做处理
预期与展望
以下是一些尚未添加的功能,准备发布成 npm 包,到时候看有没有人用吧,有人用,就搞一下,没人用,就当笔记在这里记一下这样子。就......,梦想是要有的😂
- 支持 import 导入
虽然笔者打包了 esm 和 cjs 两种,但是在引入 webpack 的 loader 时却使用的是__dirname 语法,这在 es 模块下大概是不支持的
- 添加 vite 支持
应该有一部分人是基于 vite 跑的 vue2 项目,后续可以进行下支持,并且这也很容易
- 使用 typescript 重构
尽管笔者一直在更新 TypeScript 的专栏,但早就过了技术至上的年纪,能不复杂化就尽量简单些,但如果你觉得使用 ts 很酷,那我也可以让它变身
- 增加插件机制
插件化这个话题之前是聊过的,并且笔者将不止聊一次,后续的 vite 技术揭秘、还原与实战专栏中也会 1:1 解析并实现插件功能
因为它真的很香,也在一定程度上会降低包作者的维护成本
- 增加 write 配置
大概有不少人是希望将转换结果生成文件的,且不说每次运行补丁都会耗费时间,就单说日报这一块儿
你是写我研究了 vue2 和 vue3 的文档,详细对比并罗列了差异点,还通过创建 demo 进行了效果比对,最后逐个攻破,改动了三千八百八十八行代码
还是写我就 npm install 一下,调了个包,完事儿
你自己说,哪一种写出来更显得辛苦一些
- 优化解析流程
目前的解析流程我个人感觉是有问题的,虽然我也不是很能说出来到底问题出在哪,似乎每一步都挺合理的,但我不是尤雨奚,所以我的代码具有隐藏 bug,它一定不够好