大家都知道, Vite 是一个提倡 no-bundle 的构建工具,相比于传统的 Webpack,能做到开发时的模块按需编译,而不用先打包完再加载。这一点我们在快速上手这一节已经具体地分析过了。
需要注意的是,我们所说的模块代码其实分为两个部分,一部分是源代码,也就是业务代码,另一部分是第三方依赖的代码,即node_modules中的代码。所谓的no-bundle只是对于源代码而言,对于第三方依赖而言,Vite 还是选择 bundle(打包),并且使用速度极快的打包器 Esbuild 来完成这一过程,达到秒级的依赖编译速度。
在这一小节,我将带你一起熟悉 Vite 的预构建功能,首先分析它解决了 no-bundle 构建模式下的哪些问题。紧接着我会通过具体的示例来介绍 Vite 中相应的配置项,让你理解各个配置属性的使用场景和推荐的使用姿势。最后我们会对预构建的一部分特殊场景进行讨论,并分享我的解决思路。相信学完本小节你不仅能熟悉 Vite 预构建各项配置的使用,还能深入体会各个配置的应用场景,懂得如何在实战中驾驭预构建的能力。
# 为什么需要预构建?
在介绍使用姿势之前,我想有必要先解决大家的一个疑惑:为什么在开发阶段要对第三方依赖需要进行预构建呢? 如果不进行预构建会怎么样呢?
首先 Vite 是基于浏览器原生 ES 模块规范实现的 Dev Server,不论是应用代码,还是第三方依赖的代码,理应符合 ESM 规范才能够正常运行。

但可惜的是,我们没有办法控制第三方的打包规范,就目前来看,还有相当多的第三方库仍然没有 ES 版本的产物,比如大名鼎鼎的 react:
// react 入口文件
// 只有 CommonJS 格式
if (process.env.NODE_ENV === "production") {
module.exports = require("./cjs/react.production.min.js");
} else {
module.exports = require("./cjs/react.development.js");
}
这种 CommonJS 格式的代码在 Vite 当中无法直接运行的,因此我们需要将它做一层处理,将其转换成 ESM 格式的产物。
其次,还有一个比较重要的问题——请求瀑布流问题。这里我以知名的loadsh-es库为例,这个库有 ES 版本的产物,因此在 Vite 中可以直接运行,但实际上加载的时候会发出特别多的的请求,导致页面加载的前几秒几乎处于卡顿的状态,拿一个简单的 demo 项目举例,请求情况如下图所示:

我在应用代码中调用了debounce方法,这个方法会依赖很多工具函数,如下图所示:

每个import都会触发一次新的文件请求,因此在这种依赖层级深、涉及模块数量多的情况下,会触发成百上千个网络请求,巨大的请求量加上 Chrome 对同一个域名下只能同时支持 6 个 HTTP 并发请求的限制,导致页面加载十分缓慢,与 Vite 主导性能优势的初衷背道而驰。不过,在进行依赖的预构建之后,lodash-es这个库的代码被打包成了一个文件,这样请求的数量会骤然减少,页面加载也快了许多。下图是进行预构建之后的请求情况,你可以对照看看:

小结一下,依赖预构建主要做了两件事情:
- 一是将其他格式(如 UMD 和 CommonJS)的产物转换为 ESM 格式,使其在浏览器通过
<script type="module"><script>的方式正常加载。 - 二是打包第三方库的代码,将各个第三方库分散的文件合并到一起,减少 HTTP 请求数量,避免页面加载性能劣化。
而这两件事情全部由性能优异的 Esbuild (基于 Golang 开发)来完成,而不是传统的 Webpack/Rollup,所以也不会有明显的打包性能问题,反而是 Vite 项目启动飞快(秒级启动)的一个核心原因。
ps: Vite 1.x 使用了 Rollup 来进行依赖预构建,在 2.x 版本将 Rollup 换成了 Esbuild,编译速度提升了近 100 倍!
# 如何开启预构建?
在 Vite 中有两种开启预构建的方式,分别是自动开启和手动开启。
# 自动开启
首先是自动开启。当我们在第一次启动项目的时候,会在命令行窗口看见如下的信息:

同时,在项目启动成功后,你可以在根目录下的node_modules中发现.vite目录,这就是预构建产物文件存放的目录,内容如下:

在浏览器访问页面后
