写了个webpack插件,1722行代码,无感升级到vue3

程序员他爱做梦 2024-04-30 21:31:10
前言

之前对公司七八年的老项目进行了升级,将vue2升级到vue3,并输出了一篇文章,传送门

但它存在很多问题,具体来说:

可读性巨差

以下边对filters的处理举例,你很难一眼看出来它到底在做什么,光是正则就要脑子宕机好一会儿

识别不准确

以下边对methods的处理举例,针对methods不存在的情况就没办法处理,必须手动在.vue文件中增加占位符

之所以当时能接受,是因为项目中的大多数页面基本都有该属性配置,要改动的点特别的少

不智能

在拒绝gogocode,vue2升级vue3,看这里一文中笔者也说了,目标是半自动化。以项目中使用到的render函数、slot插槽举例

项目中有多少呢?说出来也许吓你一跳

公司项目大部分是以h函数引用的,有718个

而slot有1112个

这些当时基本是手动一个一个改的,虽然也有通过正则替换的,但并不可靠,当时也是给我搞的挺tnn的

注意事项不支持非.vue文件中的相关语法转换不支持jsx写法转换(即options API中配置的render函数)安装与使用// 安装(尚未发布)yarn add patch-vue3// 使用const patchVue3 = require('patch-vue3').default;// 作为webpack插件使用new patchVue3(PatchVue3Options),

interface PatchVue3Options { identifier?: { // ui库 uiLib?: string; // render函数渲染的ui组件 uiComponents?: string[]; // 挂载的eventBus名称,默认值为'$bus' eventBus?: string; // 挂载的$children名称,默认值为'$children' mountChildren?: string; }; config?: { // ui库的前缀,比如element的el-、iview的i- uiLibPrefix?:string; // 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) => 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; // 文件id id:string;}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,它一定不够好

作者:Sir_苏链接:https://juejin.cn/post/7359083109912412186

0 阅读:56

程序员他爱做梦

简介:感谢大家的关注