likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 5

前言

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

  • 可读性巨差

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

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

  • 识别不准确

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

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

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

  • 不智能

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

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

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

而slot有1112

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

这些当时基本是手动一个一个改的,虽然也有通过正则替换的,但并不可靠,当时也是给我搞的挺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部分

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

  • script部分

在methods中,黄色是注入的部分,红色是转换的部分

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

render语法中,黄色是对props的处理,红色是对事件绑定的处理

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

目标

实现一个webpack插件,对于vue2和vue3差异的部分,实现一键转换

正文

我始终认为,思路大于开发,因此,本文之分享核心实现思路,细节概不涉及

首先,要选一个打包工具,并确定输出,笔者这里选择cjs和esm两种输出格式,

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

由于webpack的loader需要是字符串形式,且需要指向打包后的最终地址,因此,需要设计成双出口

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

下一步就是来确定实现方式,想要对代码进行转换,无非先定位,后重写

重写的方式无二,只能基于字符串rewrite

定位要不就是正则匹配,要不就是ast,显然前文已经证明了前者的不可行,故选择ast

那问题就变成了如何ast化?

  • 解析sfc

通过@vue/compiler-sfc可以拿到.vue文件的基本信息,这包括了script和template部门的源码

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

  • 解析转换script

使用ast-kit提供的babelParse接口

  • 解析转换template

使用vue-template-compiler提供的compiler接口

接着,我们来简单设计下整个应用程序的风格

首先,定义ast基类,它负责对ast树做解析或遍历等操作

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

在具体处理script或template时就可以基于它做扩展

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

处理script

  • 思路

由于在vue2中的script代码,本质上是按属性分类的,所以我们要搞一个批量自动触发调用的机制,而不是一堆if else做判断然后分发处理

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

要想不改变原有代码的写法,最好的方法是将语法的变动层注入到methods中,这就保证methods必须要在最后一步被程序触发,对应在源码中,它必须在配置项的最后一个,显然这不可能要求开发者这么做,也违背了“无感”原则

所以,第一步就是做一些格式化处理

这包括代码格式化,这样操作,能减少对逗号存在性处理的心智负担

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

还有就是关于methods的位置处理,它应该总是在最后,即使原本没有

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

最后是关于render函数的导入的处理,需要将其收集并从源码中剔除,并等待最后重新注入

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

当每一段处理程序执行的时候,只需要基于ast的标记进行识别并分发给具体的处理函数重写就可以了

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

  • 重难点

1-处理顺序

在处理的时候要特别注意处理顺序,因为字符串是基于magic-string包的,该包会把处理过的字符串位置进行标记,已经处理过的再次处理会报错

因此,在每次处理前,需要进行下reverse,按从后往前的顺序

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

(ps:关于顺序的处理涉及很多,并非简单的数组反转,感兴趣的可以看下源码)

2-更新ast节点

在处理render时,由于函数中有可能仍有需要处理的语法

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

这样就涉及到了递归,需要对函数体内的语法先行处理,再回过头来继续处理on对应的部分

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

这就会产生节点的不一致,因此,还需要对节点进行更新

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

3-避免重复处理

由于walkAST本质上是一次深度遍历,默认情况下,他会对每一个节点依次访问一遍,那就有可能处理过的节点被二次处理

笔者一开始是在全局维护了repaired数组来进行标记,后来觉得不够优雅,就去大致翻了下源码,可以像如下这样做,调用ast树上的remove接口就可以了

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

4-支持hook回调

插件只能处理通用的部分,对于特立独行的点,不能也不应该在plugin中处理,比如下边这种

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

这时候就需要能将控制权交给用户

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

这显然无法控制用户按怎样的顺序处理节点,因此需要做无限递归,只要用户hook执行一次,就重新dfs一次,以保证不影响patch-vue3包自身的补丁处理

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

但这同时又引发了新的问题,那就是当前次递归结束后回到上一次递归,会造成同一个节点被多次处理,所以还要进行下过滤

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

处理template

说实话,这个可坑死我了!!!

在一开始阶段,笔者是基于@vue/compiler-dom进行的ast化,实现过程很顺利

在正式向项目里接入时候却不停报错,看了报错后才意识到,可能是解析包的问题,因为它报的错误信息与源码毫无关系

遂,转为vue-template-compiler

vue-template-compiler依旧很坑,它虽然解析正常,也有ast tree。但结构却与正常认知的ast大不相同

具体来说

它没有节点在源码中的对应位置信息

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

为此,需要自己去拉取对应的html结构

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

组件的slot是挂载在当前节点的

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

为此,需要自己手动实现traverseNode

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

还有一点,由于对应的html结构是自己实现的,它只能拉取最顶层的html部分,对于子html结构是无能为力的。至少,在当前版本中是这样

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

为此,就不能使用magic-string包了,因为没法保证先子后父,从后向前,故,需要基于原生js实现。为了代码结构的一致性,得模拟一个

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

剩下的,就和script差不多,都是找到指定的标记,然后分发做处理

预期与展望

以下是一些尚未添加的功能,准备发布成npm包,到时候看有没有人用吧,有人用,就搞一下,没人用,就当笔记在这里记一下这样子。就......,梦想是要有的😂

  • 支持import导入

虽然笔者打包了esm和cjs两种,但是在引入webpack的loader时却使用的是__dirname语法,这在es模块下大概是不支持的

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

  • 添加vite支持

应该有一部分人是基于vite跑的vue2项目,后续可以进行下支持,并且这也很容易

  • 使用typescript重构
  • 增加write配置

大概有不少人是希望将转换结果生成文件的,且不说每次运行补丁都会耗费时间,就单说日报这一块儿

你是写我研究了vue2和vue3的文档,详细对比并罗列了差异点,还通过创建demo进行了效果比对,最后逐个攻破,改动了三千八百八十八行代码

还是写我就npm install一下,调了个包,完事儿

你自己说,哪一种写出来更显得辛苦一些

  • 优化解析流程

目前的解析流程我个人感觉是有问题的,虽然我也不是很能说出来到底问题出在哪,似乎每一步都挺合理的,但我不是尤雨奚,所以我的代码具有隐藏bug,它一定不够好

转载自:https://juejin.cn/post/7359083109912412186
评论
请登录