前言
之前对公司七八年的老项目进行了升级,将vue2升级到vue3,并输出了一篇文章,传送门
但它存在很多问题,具体来说:
- 可读性巨差
以下边对filters的处理举例,你很难一眼看出来它到底在做什么,光是正则就要脑子宕机好一会儿
- 识别不准确
以下边对methods的处理举例,针对methods不存在的情况就没办法处理,必须手动在.vue文件中增加占位符
之所以当时能接受,是因为项目中的大多数页面基本都有该属性配置,要改动的点特别的少
- 不智能
在拒绝gogocode,vue2升级vue3,看这里一文中笔者也说了,目标是半自动化。以项目中使用到的render函数、slot插槽举例
项目中有多少呢?说出来也许吓你一跳
公司项目大部分是以h函数引用的,有718个
而slot有1112个
这些当时基本是手动一个一个改的,虽然也有通过正则替换的,但并不可靠,当时也是给我搞的挺tnn的
注意事项
-
不支持非.vue文件中的相关语法转换
-
不支持jsx写法转换(即options API中配置的render函数)
安装与使用
js
// 安装(尚未发布)
yarn add patch-vue3
// 使用
const patchVue3 = require('patch-vue3').default;
// 作为webpack插件使用
new patchVue3(PatchVue3Options),
ts
interface PatchVue3Options {
identifier?: {
// ui库
uiLib?: string;
// render函数渲染的ui组件
uiComponents?: string[];
// 函数式slot调用
scopedSlots?: string;
// 挂载的eventBus名称,默认值为'$bus'
eventBus?: string;
// 挂载的$children名称,默认值为'$children'
mountChildren?: string;
};
config?: {
// eventBus的引用路径,默认引入路径为webpack配置的别名key+'/util/patch',该模块需要导出名称为bus的对象
busImportPath?: string;
// 是否启用别名,启用后,查找并应用webpack配置项中的第number个alias key,默认为0
alias?: number;
// 当非setup标签、非setup函数、非jsx render、非多根节点时,又想要sfc文件跳过本插件处理时指定,默认为refuse-patch
skipTag?: string;
// prettier配置文件地址,默认为根目录下的.prettierrc
prettierrc?: string;
// 全局过滤器,当前sfc找不到filter配置时降级使用
globalFilters?: string[];
};
hooks?: {
// 文件开始被处理时的回调
"patch:start"?: (id: string, code: string) => void;
// 文件处理完成时的回调
"patch:end"?: (id: string, code: string) => void;
// 处理script时的回调
"patch:scriptNode"?: (node: AstNode, ctx: ScriptCtx) => Boolean | void;
// 处理template时的回调
"patch:templateNode"?: (node: AstNode, ctx: TemplateCtx) => void;
};
}
interface Ctx {
// 遍历节点
dfs: (node: AstNode, cb: (node: Node) => void) => void;
// 模版源码
getSource: () => string;
// 保存更新后的源码
save: (code: string) => void;
}
interface ScriptCtx & Ctx {
// 获取某一段script code
loadScript:(code:string,start:number,flag:[string,string])=>string;
}
interface TemplateCtx & Ctx {
// 获取tag标签
loadTag: (
code: string,
attr: string,
config?: {
lastIndex: number;
tagName: string;
}
) => string;
}
效果预览
测试源码在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接口就可以了
4-支持hook回调
插件只能处理通用的部分,对于特立独行的点,不能也不应该在plugin中处理,比如下边这种
这时候就需要能将控制权交给用户
这显然无法控制用户按怎样的顺序处理节点,因此需要做无限递归,只要用户hook执行一次,就重新dfs一次,以保证不影响patch-vue3包自身的补丁处理
但这同时又引发了新的问题,那就是当前次递归结束后回到上一次递归,会造成同一个节点被多次处理,所以还要进行下过滤
处理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很酷,那我也可以让它变身
- 增加write配置
大概有不少人是希望将转换结果生成文件的,且不说每次运行补丁都会耗费时间,就单说日报这一块儿
你是写我研究了vue2和vue3的文档,详细对比并罗列了差异点,还通过创建demo进行了效果比对,最后逐个攻破,改动了三千八百八十八行代码
还是写我就npm install一下,调了个包,完事儿
你自己说,哪一种写出来更显得辛苦一些
- 优化解析流程
目前的解析流程我个人感觉是有问题的,虽然我也不是很能说出来到底问题出在哪,似乎每一步都挺合理的,但我不是尤雨奚,所以我的代码具有隐藏bug,它一定不够好