公司的项目大部分都是 2B 类型的中后台 PC 端项目,且业务都非常庞大。常年累月的维护下,使用 webpack dev 速度变得非常慢。
因为最近的工作主要是负责一些前端的基础设施建设,各个业务线的产品都有一定的接触。有一个项目在我的电脑上面 npm run dev 居然要 6 分钟之久,实在是难以感到愉悦。加上公司内部已经新项目使用 vite 作为构建工具,所以挑选了一个自己比较熟悉的老项目(vue-cli)进行 vite 迁移。并将过程进行了记录。
项目背景
先简单说明一下项目的情况:
内容:2B 的 PC 端中后台管理系统,是一个双入口的应用(登录页一个入口,管理系统一个入口)。
构建工具:vue-cli-service
技术栈:vue 2.6 + Axios + Vuex + Vue-Router + sf-vue-componet(公司内部组件库) + ElementUI
维护时长:3 年+,(实际上应该远远超过五年,这个项目是以一个已经维护了很久的项目为基础进行开发的,而当前最早的一条 commit 记录是 3 年前,但是这个基础项目已经开发很久时间了)
模块数量:6000(npm run serve 时显示的模块数量)
业务代码量:19.3W 行
兼容性要求:IE11
项目的体量在公司内部只能算是中等,之所以挑选这个项目进行处理是因为对这个业务比较熟悉。话不多说,开搞。
思路分析
本次的目的不是直接用 vite 替换 vue-cli,而是想同时保留 vite 和 vue-cli。除去影响开发体验也主要是集中在 dev 这一块的原因外,还有以下两点:
产品需要兼容 IE11,尚不清楚 vite 在此方面有多少坑。步子迈太大了,可能扯着胯。
rollup 打包机制毕竟和 webpack 还是不同,且业务也比较复杂。修改完成之后也很难保证所有业务都能测试到位。
webpack 和 vite 本质上都是入口文件 + 依赖分析 + 模块转换。vite 是 使用的时候进行处理(vite 针对第三方库有同样会进行预处理),webpack 则是先全部处理完毕。当浏览器天然支持 ES module 之后,实时处理变得可能。这也是 vite 启动速度快的原因。
所以,从理论上来说,从 Vue-cli 迁移到 vite 是可行的。
开始迁移
step1 分析 vue.config.js
首先,我们先进行 vue.config.js 进行分析。主要是把一些插件,和比较特殊的配置整理出来。然后再去 vite 官网和社区查看,看是匹配的配置或者插件。把其中不匹配的部分进行解决,估计迁移也就差不多了。
分析完成之后主要两点无法匹配的情况
1 | // vue.config.js |
chainWebpack 中的配置主要是为了解决两个入口资源互相加载的问题,即访问登录页时,会预加载管理系统资源。由于这个是打包后在生产环境中才会用到的内容,且这次迁移无需关系打包的内容,所以暂时不用处理。
transpileDependencies 主要是让 node_modules 中的文件也进行 babel 的处理,而我们在开发时可以使用最新的浏览器,完全可以不用 babel,所以这个问题也不用处理。
https://cli.vuejs.org/zh/config/#transpiledependencies
step2 安装依赖,添加 scripts
一般情况下,会需要用到以下的 npm 包。
- vite
- vite-plugin-vue2:vite 支持 vue2 的插件(默认情况下,vite 并不支持 vue)
- vite-plugin-eslint(可选):vite eslint 插件,用于在 run dev 时,实时校验 eslint
- vite-plugin-legacy(可选):处理浏览器兼容性,本次这里不需要。
- vite-plugin-mock(可选):mock 数据
1 | yarn add vite vite-plugin-vue2 -D # 这里只安装了必要的vite 与 vite vue2插件 |
然后添加 package.json 中 添加 scripts 命令。
1 | // package.json |
step3 编写 vite.config.js 文件
因为原本的 vue.config.js 不是特别复杂,网上也有其他迁移的文章讲解如何迁移 webpack 配置,所以我这里直接粘贴结果。
1 | const { createVuePlugin } = require('vite-plugin-vue2') |
从上面的结果来看其实只用写三个部分的内容:
- serve: 开发时的运行端口,以及数据请求的反向代理的规则
- alias: 别名
- plugins: 引入 vite-plugin-vue2 用于加载 vue 文件。
关于多入口
在项目介绍的时候说到,项目是一个多入口的文件。实际上在 vite 的开发环境中,多入口无需配置。启动文件之后直接访问对应的 html 资源即可。
关于 wp2vite
https://github.com/tnfe/wp2vite (一个前端项目转换工具,可以让 webpack 项目支持 vite)
wp2vite 能够将根据你当前的 webpack 配置生成 vite 配置(支持 vue-cli),如果不想手写 vite.config.js 可以使用该工具进行自动转换。我是在撰写本文整理资料时,才发现有此工具。
wp2vite 转换的结果并非完全正确,存在以下问题。
- alias 添加了一些莫名其妙的别名 (也有可能是 Feature :smile:)
- proxy 配置 rewrite 不正确
以上两点,各位同学在参考的时候注意一下就行。
step 4 调整入口文件
与 vue-cli 有区别的是,vite 默认只能将 html 作为入口文件,所以需要对 HTML 内容进行调整。
别忘记给 script 标签添加 type=”module”
1 | <!-- index.html 主应用的入口文件 --> |
step 5 npm run dev
直接运行 npm run dev,可以发现 vite 马上就开启了服务。相对于原来 70s 左右的时间,这快了可不止一星半点,简直就是火箭起飞。
当然,服务的启动并不代表迁移已经完成。vite 更多的是在访问对应的资源时才会去处理内容。所以需要访问对应的页面来进行验证。而真正的迁移,才刚刚开始。
无尽采坑:阶段一
问题 1:URIError: URI malformed
vite 开启之后,访问登录页 https://localhost:4433/static/login_platform.html,报错 URIError: URI malformed
这是 decodeURIComponent 函数在执行时报的错。
通过在错误堆栈中,加上 console 语句,得知出错的 URI 是/static/%3C%=%20BASE_URL%20%%3Efavicon.ico
对应的代码则是 login.html 中的代码,这里的 BASE_URL 是 vue-cli 中模板变量的写法,这里可以修改为/public/favicon.ico(根据自己的目录结构调整,切勿盲目套用)
1 | <link |
修改完成之后,vite 则提示去掉 public 前缀,那我们直接去掉就行。
问题 2:Failed to resolve import “xxx”
提示找不到对应的 vue 文件(实际肯定是有)。经过查阅资料这一块主要是为了类型提示的问题(ts 的项目),vite 不在自动查找.vue 扩展名的文件。
这里只需要补全 PageLogin.vue 即可。 在这里补全.vue 扩展名不太现实,因为项目里面大部分都是省略了扩展名的。好在官方给出了解决的方案,直接配置即可。
https://cn.vitejs.dev/config/#resolve-extensions
1 | resolve.extensions |
配置 resolve.extensions 的方式,并不能够完全的解决问题。这里卖个关子,后面会继续遇到因为缺少.vue 扩展名而导致的问题。那里会给出完美的解决方案。
问题 3: less import
修复完问题 2 之后,继续访问页面。这个时候又出了新的问题。
经过查阅资料,vite 对 less 和 sass 文件的 import 进行了优化的处理,不在需要在别名前添加 ~符号来进行识别。
1 | vite 为 Sass 和 Less 改进了 @import 解析,以保证 vite 别名也能被使用。另外,url() 中的相对路径引用的,与根文件不同目录中的 Sass/Less 文件会自动变基以保证正确性。 |
这里就只需要全局搜索然后全局替换就行,如果有发现全局没有替换到的(多了空格什么的),手动替换一下就行。
问题 4:net::ERR_ABORTED 408 (Request Timeout)
修复好问题 3,再次访问时出现很多资源 408 的错误。
出现这个问题不要慌,重新刷新即可,这是 vite 在对一些依赖进行预构建。因为是请求到对应的资源才会进行预构建,在碰到需要预构建的包后,vite 直接返回了 408。
有时候需要重新刷新多次,每次构建完成之后重新刷新,如果发现新的需要预构建的依赖会再次返回 408
至于这里为什么返回 408,而不是直接挂起请求预构建完成之后再返回,个人不是很清楚。有了解 vite 原理的同学可以解答下。
1 | vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。 |
问题 5:Vue warn:runtime-only
等待所有内容都预构建完成之后,又出现了新的问题。
这里问题比较好理解,vue 的默认引用的内容是不包含模板编译功能的。因为之前使用的是 vue-cli,毕竟是一家的东西 vue-cli 针对这个情况做了特殊的处理。
这种情况在 vite.config.js 中,添加一条别名即可解决。
1 | // vite.config.js |
问题 6:require is not defined
这里继续爆出来 import 的问题,但是这里肯定不是因为确实 .vue 扩展名引起的。经过分析,问题的原因是业务代码中使用了 require 语句,而 vite 主要是依赖于 ES module 机制。这也就导致无法识别该业务代码,进而导致该模块加载失败。
1 | 这里吐槽下,这里 vite 控制台 和 浏览器控制台爆出了不同的错误,而 vite 控制台的错误信息完全没法排查思路。 |
这种情况下,需要手动对业务代码进行处理。
1 | { |
采坑阶段一总结
修改完上述问题之后,本项目的多入口应用中的一个登录页面入口,终于可以正常展示了。
- vite 控制台,chrome 控制台均无报错
- 之前修改的 require 改为 import 的 logo,正常显示
- 通过网络请加载的验证码,正常显示(说明反向代理这些都是正常的)
虽然遇到问题还是比较多,但是解决起来都不是很费时间,排查起来也比较顺利。至此迁移工作可以说已经进行了一半。除去 html 模板文件中修改 favicon.ico 和导入图片的 requrie 之外,没有改动其他的业务代码,对业务的入侵很小。
话不多说,输入账号密码,开启第二个阶段的采坑。
无尽采坑:阶段二
问题 7: uses lang html for template
这是一个相当无语的错误,应该是 vite-plugin-vue2 插件的 bug。问题的原因是在 template 中,写了 lang=”html”
1 | <template lang="html"> |
虽然模板默认就是 html,这个声明时多此一举,但是报错感觉不应该。
好在这个问题比较好解决,通过查找字符串全局替换即可。你问我为什么要多此一举要声明 lang=”html”?我也不知道,这不就是老项目的魅力嘛。
问题 8: npm 包出错
这也是本次迁移过程中,最为棘手的问题。不止一个 npm 包出错,且出错的内容也不一样。有些出错可以通过 vite 的配置解决,有些则无法解决。
一般来说有以下几种方法来解决 npm 包报错的问题。
方法一:修改别名,指向源码
即在 vite.config.js 中添加 alias
1 | '@sxf/sp-qrcode': resolve(__dirname, './node_modules/@sxf/sp-qrcode/src/index.js'), |
这种方法适用于 npm 包的源码是用 ES module 规范编写的,且上传的 npm 包中包含了源码。
方法二:修改别名,指向 esm 格式的打包文件
修改方式同方法一,只不过是执行打包后的 esm 文件。
这种方式适用于 npm 包已经 build 了 esm 模块的内容,但是未在 packages.json 中提供 module 字段指向对应的文件。
一般开源的,使用人数较多的 npm 包不会出现这种情况,只是因为这次出问题有一些是内部的包,确实存在 build 了 esm 的文件,但是没有在 package.json 中指明的情况。
方案三:optimiziDep.include
默认情况下,不在 node_modules 中的,链接的包不会被预构建。使用此选项可强制预构建链接的包。
从官网的文档的描述信息来看 optimiziDep.include 是无法解决 npm 包出错的问题。因为不管怎样,node_modules 中的内容都会被预构建。但是从实际的使用中来看
- 在 vite 2.4.4 中,optimiziDep.include 能够解决部分 npm 加载的问题。
- 在 vite 2.5.3 中,optimiziDep.include 不再起作用。
因为 vite 一直在高速迭代,保不齐部分情况下此方法依旧有用。
方案四:参考问题 10
问题 10 也是 npm 出问题,但是这个问题比较具有代表性,所以单独拿出来。
方案五:换个 npm 包,或者修改 npm 包
如果上面三种方案都无法解决,那就只能从 npm 包本身下手了。
对于自己内部维护 npm 包,可以重新打包一个 ES module 的文件并发布。这里推荐用 rollup 打包,能够很轻松的构建 CommonJS、ES module、UMD、IIFE 格式的文件
对于第三方的 npm 包,出现这种情况应该是很久没有维护了,所以也应该换一个了。
问题 9:Failed to resolve entry (ops,same proplem)
出现问题原因与问题 2 相同,也是因为没有.vue 扩展名的问题。
@sxf/cloudsec-component 是一个自己维护的业务组件库,基于基础的 UI 组件库,将一些常用的业务进行封装,单独发布一个 npm 包。主要是为了方便同一个开发团队维护多个产品(这点与公司组织架构有一定的关系,不在深究)。因为业务组件需要调试的原因,npm 发布的是.vue 的源码。
同样因为保持着相同的编码规范,业务中对组件库的引用,以及组件库本身开发时的互相引用都是省略了.vue 扩展的。
而以上两种方案都无法通过 resolve.extensions 解决。
1 | 在 vite 2.4.4 的版本中,配置了 resolve.extensions 之后,无法识别 vue-router 中的 () => import('foo/bar/baz') |
解决方案一:人肉修改
人肉一个个修改,这种情况适用于刚开始不久的项目。但是对于一个维护数年的老项目来说,这基本不现实。20W 行左右的代码,几千个 vue 文件。作为程序员明显不能这么干活。
解决方案二:vite 插件
vite 插件的第一个例子就是讲的引入一个虚拟文件,这里的问题其实类似。就是没有补充扩展名,导致找不到,那么可以通过插件查找。
这里提供一下伪代码,未进行实际的验证。
因为这只是解决了 vite 加载的问题,但是没有解决类型提示的问题,这对于后续进行 ts 相关改造不友好,所以没有去验证此方案。
感兴趣的可以验证一下,个人在实际的业务中是采用的方案三。
1 | module.exports = function () { |
问题 10: does not provide an export named ‘default’
这个问题是因为第三方的 npm 包是提供的是 CommonJS 规范的文件。
1 | CommonJS 和 UMD 兼容性: 开发阶段中,vite 的开发服务器将所有代码视为原生 ES 模块。因此,vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。 |
vite 官网说,会兼容 CommonJS 格式的文件,但是这里为什么还是因为 CommonJS 格式的问题呢?
其实这个文件是可以 import 导入的只是并没有提供 default 这个属性,这么说 vite 也确实支持了 CommonJS。
遇到这种情况我们需要引入 @originjs/vite-plugin-CommonJS 这个包
1 | const { viteCommonJS } = require('@originjs/vite-plugin-CommonJS'); |
采坑阶段二总结
在 主入口页面的迁移过程中,改动了非常多的代码。
- 业务代码 293 个文件
- 业务组件的代码 139 个文件
但是修改这些文件大多数没有修改业务,只是补全了链接。但是 less 的文件 import 却是和 webpack 不兼容。
此外,还有有一些有问题的 npm 包,这次迁移的过程并没有完全解决,只是将对应的代码给注释。
最终通过以上措施,终于能够访问了,也看到了主入口的页面。
最终总结
vite 优点大家都知道,我主要来说一下现阶段的缺点。
总的来说现阶段的 vite 还存在以下问题:
- 手动重新 npm run dev 后,预构建之后缓存丢失,而修改 vite.config.js 导致的自动重启不会。
- 预构建需要访问到对应的文件才能触发,会导致频繁 408,且速度非常慢。整个下来时间比较久,而且需要你手动刷新浏览器。比较奇怪的是 vite 2.4.4 在这方面 比 vite 2.5.3 快非常多,基本上只会出现一次 408。
- 无法很好的处理非 ES module 的 npm 包,与 webpack 还存在一定的差距。
- 请求数量巨大,从 network 的面板来看,请求的数量高达 468,除去 XHR 请求之外也有将近 400 个请求。所以首次加载的时间比较慢大概花了 5 秒,这一块应该是可以通过 vite 的配置来优化。等内部的一些 npm 包调整完毕能够进行正常的开发之后,会总结一些开发的实践并发出来。
- 功能不稳定。在早期使用 vite 2.4.4 进行迁移时,问题稍微还多一些。之后更新到 vite 2.5.3 之后,部分问题被修复了,但是也增加了一些新的问题。
针对老项目需要迁移到 vite,需要考虑以下情况:
- 不在支持非 ES module 的业务代码(如 require,module.exports),如果有这些代码需要进行调整,可以尝试与问题 9 相同的方式。
- less/sass 的@import: vite 不在需要 ~ 来使用别名,这一点与 webpack 不兼容。
- 因为 IE11 天然不支持 ES module,所以 IE 调试比较麻烦,不建议需要支持 IE11 的项目将 vite 作为唯一的构建工具。
- 现阶段的 vite 对 npm 包有着较高的要求,迁移之前可以先看下项目的 npm 包,该替换的替换,该升级的升级。
整体来说,vite 的开发体验比较好,轻量且快。即便是一个臃肿的老项目也能够做到反应迅速且不占用过多资源。项目较大时 webpack 启动时会占用大量的电脑资源,导致这一段时间 VSCode 非常卡,这一点体验非常不好。