当前,前端社区用 Vite 替代 Webpack 的呼声正日趋高涨。但对于长期维护的业务项目,很多同学可能仍然对上车存有疑虑——Vite 真的足够支撑非玩具级的项目吗?为此本文会分享一个实际案例,介绍我们是如何(比较轻松地)在公司核心业务中落地 Vite 的。
稿定 Web 端业务中的平面编辑器已经有五年以上的历史。作为一个历经多人主导维护的前端项目,它有这么一些复杂度:
编辑器在 2016 年的第一次提交,基于 Vue 0.8 和 AMD 语法
我们不敢说这就是所谓的「大型企业级」项目,但这至少肯定不是个玩具项目。然而超乎预期的是,Vite 的迁移成本甚至比升级 Webpack 和 Babel 大版本还要低。只花了一个下午的时间,基于 Vite 的编辑器最小可用 MVP 就跑起来了。下面分几点介绍相关的实践经验:
我们知道,以 Webpack 为代表的主流前端 bundler 之所以慢,根源在于它们冷启动时必须递归打包出整个项目的依赖树,并受限于 JavaScript 的天性(解释执行与单线程模型)而存在吞吐量上的瓶颈。为了解决这两个痛点,Vite 另起炉灶切换了路线:
Vite 的这个设计与 webpack-dev-server 之间的区别,在其文档中也已经展示得很清楚,一图胜千言:
Webpack 式的经典 bundler 示意图
Vite 式的 No-bundler 示意图
基于这个差异我们就可以知道,要让 Vite 支持原有的 Webpack 项目,需要保证的无非两件事:
当然这只是最简单的思维模型。实际的前端项目中往往还会引入一些奇怪的东西,比如 CSS、JSON、Worker、WASM、HTML 模板……虽然 Vite 对这些需求已经内建了良好的支持,但确实谁也不敢保证能一键开箱即用——这并不是 Vite 或 Webpack 的问题,而是移植代码构建环境时的共通难点。对这类任务,最难的地方总在于从零到一的「点亮」。因此这里对此的建议是这样的:充分熟悉从项目入口到各组件渲染完成之间所经历的代码(子)树,确保这一个最小的子集能够在新环境下正常运作。其他代码都可以大刀阔斧地暂时移除掉。
对于架构设计合理的软件项目,一般都可以容易地实现模块的精简和扩展。例如在这个编辑器中,我们就支持了可配置并按需加载的元素类型。对于现有的 20 余种业务元素,它们对应的模块都已经支持了按需加载,只会在遇到相应数据时 import()
导入。因此在迁移时,只需保留若干基础元素模块实现用于测试即可。类似地,在业务项目中也可以通过精简路由配置等方式,定制出一个用于走通主流程的最小可用版本。
上述的代码精简过程,其实不外乎是建立一个干净的 example 页面来导入项目,注释掉部分代码然后反复执行 vite
命令测试,这里不再赘述。对于 Vite 迁移,很多同学最担忧的可能还是 Webpack 插件兼容性方面的问题。我们恰好也遇到了类似的问题,这里简单分享一下。
在前面 2016 年的编辑器上古版本代码截图中有一个细节,那就是其中引入了 editor.html
作为组件的 HTML 模板。这个行为历经多年一直保留到了现在——也就是说这里没有使用 SFC 单文件组件,而是对 text-element.js
等组件配套放一个 text-element.html
作为其模板,像这样:
// 导入 HTML 源码
import TextElementTpl from './text-element.html'
// Vue 2.0 的经典配置
export default {
template: TextElementTpl,
methods: {
// ...
},
created() {
// ...
}
}
在 Webpack 配置中,我们一般会用 HTML loader 来支持它,那么 Vite 呢?这类需求似乎并没有内置,而现在社区的 vite-plugin-html 是为 EJS 模板设计的,star 数量好像也不多……但真的就要等社区做现成的给你吗?
其实,Vite 的插件系统是直接依赖 rollup 的。对于这个需求,只要这样在 vite.config.js
里写个几行的插件就够了:
// 使用 rollup 附带的 plugin utils
const { createFilter, dataToEsm } = require('@rollup/pluginutils');
function createMyHTMLPlugin() {
// 建立一个用于筛选模块的 filter
const filter = createFilter(['**/*.html']);
return {
name: 'vite-plugin-my-html', // 起个名字
// 根据 id 来筛选模块,并在遇到匹配的模块时变换其 source
transform(source, id) {
if (!filter(id)) return;
// 这样 HTML 字符串就能被 export default 给其他 JS 模块了
return dataToEsm(source);
},
};
}
// 这样就可以按照 Vite 的标准 API 来使用插件了
module.exports = {
plugins: [createMyHTMLPlugin()],
}
这个 createMyHTMLPlugin
不就是个非常简单的函数而已吗?但它却切实地解决了一个实际问题。个人认为对用户友好的构建系统应该做到在大多数时候能开箱即用,并能通过简单的逻辑自行扩展。在这一点上,可以说 Vite 还是做得相当出色的。另外 Vite 相比 Snowpack 的一个主要区别,就是它的插件系统与 Rollup 有更深的集成,由此实现了在 dev 和 build 两种模式下通用的插件 API。因此在业务中,也有机会自行「套壳」一些成熟的 Rollup 插件来实现需求。
在这次实践中用到的 Vite 配置相当少,值得一提的主要是这么几条:
resolve.alias
配置,可以覆写(或者说劫持)掉模块路径。注意最好尽量让这个配置少一点,滥用它容易降低代码模块结构对工具链的友好性。define
配置,可以支持 process.env.__DEV__
这样的环境变量注入。注意 Vite 会把字符串直接注入成产物代码中的 raw expression,所以如果只想传递 true
这种简单常量,要额外 JSON.stringify
包一层。vite-plugin-vue2
可以支持 Vue 2.0 的 SFC。这里的理由在于虽然编辑器内的主要组件没有使用 SFC,但测试页面的 demo 入口是个 app.vue
。通过这个插件,可以让它们良好地共存。import "./dist.css"
即可。import Worker from "worker.js?worker"
的语法,可以支持 Web Worker。另外也可以进一步将其配合 resolve.alias
配置,来继续兼容 Webpack。import init from "./a.wasm"
的内置支持以外,还有一种实践是让 WASM 的 JS 适配层支持传入可配置的 WASM 路径,这方面比较典型的例子可以参考 CanvasKit 等包。基于上面介绍的这些实践,应当已经足够解决 Vite 对各类业务模块的加载问题了。但最后还有一个比较头疼的地方:如果 node_modules 中的依赖不能被 esbuild 正确打包,又该怎么办呢?
在这次迁移中,这样的问题我们有遇到两处,各自的原因有所不同:
arguments
数据,导致 esbuild 报错。require('fs')
的函数。这也会导致报错。对于这两个问题,其实都有一种通用的 workaround 手法:建立一个 third_party 目录,把存在问题的上游模块拷贝一份进去,在这里修复问题并调整模块依赖即可。如 Pica 库内 require('./a.js')
的代码,就可以复制到 third_party 目录后,将模块导入路径改为 require('pica/src/a.js')
,这样并不需全量复制整个上游依赖。而对于这里遇到的两个 CommonJS 问题,具体的修复也都很容易,例如把对 arguments
的读取放到 export default
的函数体内,并直接移除在浏览器环境下用不到的 Node 文件读取逻辑等。这样的 third_party 模式实际上倒也不算什么 hack,在很多语言的工程中有很广泛的使用,但也有些地方值得注意:
// FIXME
之类的注释,方便接受者确认修改之处。以上就是全部值得列出的问题了,最后放一张基于 Vite 启动本地环境成功时的截图:
上图的日志有个问题,即加载了两个不同的 Vue 版本。这是因为 SFC 部分和依赖 HTML 模板的代码误用了不同的 Vue 依赖。这个问题后来通过 alias
配置将 vue
全部重写到 vue/dist/vue
而解决了。
由于编辑器 SDK 原本就使用 Babel 独立发版,因此原有的 NPM 发布过程不受影响,Vite 整体的侵入性也并不高。至于最终效果上也没有什么别的,就是油门踩到底加速了一下:
.vite
目录缓存后,启动 vite
命令的时间仅需约 300 毫秒。这样一来,这个历史项目就重新获得了即时反馈级别的开发体验,同时也让更高效的 CI 集成成为了可能。这里的想象空间还很大,我们很期待让 Vite 在未来发挥出更大的作用。
var
全部查找替换成 let
这种事你干过没有?这些手段并没有什么高下之分,能简单方便地解决问题就好。实际上作为本文的作者,之前个人还尝试过一些类似的代码移植。这类工作就像是一个破解密室逃脱游戏的过程,非常有趣。个人感觉像这次的 Vite 迁移,在实践手段上其实和之前的经历都是相当共通的:
所以最后,非常鼓励大家多做兴趣驱动的技术尝试。没准未来的哪天,折腾它们的经验就能帮助你找到抓手,赋能业务,形成闭环,打出一套组合拳呢(