依赖预构建
当你第一次运行 vite 启动开发服务器时,可能会注意到终端输出了类似这样的信息:
Pre-bundling dependencies:
vue
vue-router
axios
(this will be run only when your dependencies have changed)
这就是 Vite 在执行依赖预构建(Dependency Pre-Bundling)。这个步骤是 Vite 开发体验极速的关键因素之一,理解它的工作原理有助于你更好地配置和排查问题。
为什么需要依赖预构建?
依赖预构建服务于两个核心目的:兼容性转换和性能优化。
CommonJS 和 UMD 兼容性
Vite 的开发服务器将所有代码以原生 ES 模块(ESM)的形式提供服务。然而,npm 生态系统中有大量的包仍然以 CommonJS 或 UMD 格式发布,而不是 ESM。
当你在代码中这样导入一个 CommonJS 包时:
import React, { useState } from 'react'
浏览器原生无法理解这种导入方式,因为 React 发布的是 CommonJS 格式。Vite 通过预构建将这些 CommonJS/UMD 依赖转换为 ESM 格式,使得你可以在开发中正常使用它们。
Vite 在转换 CommonJS 模块时会进行智能的导入分析。即使 CommonJS 模块的导出是动态赋值的(如 React),具名导入也能正常工作:
// 这段代码在 Vite 中可以正常工作
import React, { useState, useEffect } from 'react'
性能优化:合并大量小模块
许多包的 ESM 构建版本由大量内部模块组成。一个典型的例子是 lodash-es,它有超过 600 个内部模块。
当你这样导入时:
import { debounce } from 'lodash-es'
如果没有预构建,浏览器会同时发起 600 多个 HTTP 请求来加载这些模块!虽然服务器处理这些请求毫无压力,但浏览器对并发请求数量有限制,这会导致网络拥塞,页面加载明显变慢。
Vite 通过预构建将 lodash-es 合并为单个模块,将 600 多个请求减少为 1 个请求,显著提升页面加载性能。
预构建的工作流程
自动依赖发现
Vite 使用以下流程自动发现和预构建依赖:
- 扫描源代码:启动时,Vite 会扫描你的源代码,找到所有"裸导入"(即期望从
node_modules解析的导入) - 预构建:使用 Rolldown 将发现的依赖打包成 ESM 格式
- 缓存:预构建结果缓存在
node_modules/.vite目录
// 这些是"裸导入",会被 Vite 发现并预构建
import vue from 'vue'
import { router } from 'vue-router'
import axios from 'axios'
Vite 8 使用 Rolldown 进行依赖预构建,这使得预构建过程非常快速,通常只需要几秒钟,即使对于大型项目也是如此。
运行时发现新依赖
服务器启动后,如果遇到新的不在缓存中的依赖导入,Vite 会重新运行预构建过程并重新加载页面。这种情况通常发生在:
- 动态导入中使用的依赖
- 插件转换后生成的导入
缓存机制
文件系统缓存
Vite 将预构建的依赖缓存在 node_modules/.vite 目录。Vite 通过以下信息判断是否需要重新预构建:
- 包管理器的锁文件内容(如
package-lock.json、yarn.lock、pnpm-lock.yaml) - 补丁文件夹的修改时间
vite.config.js中的相关配置NODE_ENV的值
只有当上述内容发生变化时,才会重新运行预构建。
强制重新预构建
你可以通过以下方式强制 Vite 重新预构建:
# 方式一:使用 --force 标志
npx vite --force
# 方式二:手动删除缓存目录
rm -rf node_modules/.vite
浏览器缓存
预构建后的依赖请求会被强缓存,使用 max-age=31536000,immutable HTTP 头。这意味着一旦缓存生效,这些请求永远不会再次访问开发服务器。
缓存会在安装不同版本时自动失效(通过追加的版本查询字符串)。如果你想在本地调试依赖,可以:
- 在浏览器开发者工具的 Network 面板中临时禁用缓存
- 使用
--force标志重启 Vite 开发服务器 - 刷新页面
配置预构建行为
Vite 的默认依赖发现机制在大多数情况下都能正常工作,但有时你需要手动配置。
显式包含依赖
某些依赖可能无法被自动发现(例如导入是在插件转换后生成的)。你可以使用 optimizeDeps.include 显式包含这些依赖:
// vite.config.js
export default defineConfig({
optimizeDeps: {
include: [
// 显式包含的依赖
'my-lib',
// 包含特定文件
'my-lib/specific-file',
// 嵌套依赖
'deeply/nested/dependency',
],
},
})
什么情况下需要显式包含依赖?
- 依赖是通过插件动态导入的
- 依赖很大(有许多内部模块)或是 CommonJS 格式
- 依赖使用了特殊的 Node.js polyfills
显式排除依赖
对于小型且已经是 ESM 格式的依赖,可以排除它们,让浏览器直接加载:
export default defineConfig({
optimizeDeps: {
exclude: [
// 排除的依赖会直接加载原始文件
'my-small-esm-package',
],
},
})
排除依赖时需要注意:
- 确保依赖是有效的 ESM 格式
- 排除 CommonJS 依赖会导致运行时错误
自定义预构建配置
你可以通过 optimizeDeps.rolldownOptions 进一步自定义 Rolldown 行为:
export default defineConfig({
optimizeDeps: {
rolldownOptions: {
// 修改构建目标
target: 'es2020',
// 添加 Rolldown 插件处理特殊文件
plugins: [
// 插件配置
],
},
},
})
强制预构建时机
默认情况下,Vite 在服务器启动前预构建依赖。你可以通过 optimizeDeps.entries 指定扫描入口:
export default defineConfig({
optimizeDeps: {
// 指定扫描入口文件
entries: [
'./src/main.js',
'./src/**/index.js',
],
},
})
Monorepo 和链接依赖
在 Monorepo 设置中,一个依赖可能是同一个仓库中的链接包。Vite 会自动检测不是从 node_modules 解析的依赖,并将链接的依赖视为源代码处理。
这意味着 Vite 不会打包链接的依赖,而是分析其依赖列表。但这要求链接的依赖必须导出为 ESM 格式。
如果链接的依赖不是 ESM 格式,需要在配置中显式处理:
export default defineConfig({
optimizeDeps: {
// 包含链接的 CommonJS 依赖
include: ['linked-dep'],
},
build: {
commonjsOptions: {
include: [/linked-dep/, /node_modules/],
},
},
})
修改链接依赖后,需要重启开发服务器使更改生效:
npx vite --force
开发与生产的一致性
Vite 8 使用统一的 Rolldown 打包器,这意味着开发和生产环境的行为更加一致:
- 开发环境:使用 Rolldown 进行依赖预构建和代码转换
- 生产环境:使用 Rolldown 进行完整打包
相比之前的双打包器架构(esbuild + Rollup),统一打包器减少了开发和生产环境之间的不一致性,降低了边缘情况的出现概率。
常见问题排查
依赖预构建失败
如果预构建失败,Vite 会显示错误信息。常见原因:
- 依赖使用了 Node.js 内置模块:需要在配置中提供 polyfill
- 依赖使用了特殊的语法:需要配置 esbuild 插件
- 依赖导出结构不标准:检查依赖的导出方式
依赖更改后页面不更新
如果修改了依赖(例如 npm install 新版本),但页面没有更新:
# 强制重新预构建
npx vite --force
控制台显示 "Module not found"
这可能是因为导入是在运行时动态生成的。解决方法:
export default defineConfig({
optimizeDeps: {
include: ['your-missing-dependency'],
},
})
配置参考
optimizeDeps 完整配置
export default defineConfig({
optimizeDeps: {
// 预构建入口文件
entries: ['./src/main.js'],
// 强制预构建的依赖
include: [
'vue',
'vue-router',
'axios',
],
// 排除预构建的依赖
exclude: [
'my-linked-package',
],
// 自定义 Rolldown 配置
rolldownOptions: {
target: 'es2020',
plugins: [],
},
// 是否在服务器启动前预构建
noDiscovery: false,
},
})
相关配置选项
| 选项 | 说明 |
|---|---|
entries | 扫描入口,默认自动检测 |
include | 强制预构建的依赖列表 |
exclude | 排除预构建的依赖列表 |
rolldownOptions | 传递给 Rolldown 的选项 |
noDiscovery | 设为 true 时禁用自动发现,只使用 include 列表 |
写在最后
依赖预构建是 Vite 极速开发体验的关键技术。它解决了两个核心问题:将 CommonJS/UMD 转换为 ESM 格式,以及合并大量小模块减少网络请求。
大多数情况下,Vite 会自动处理依赖预构建。当遇到特殊情况时,可以通过 optimizeDeps.include 和 optimizeDeps.exclude 进行调整。修改依赖后如果发现更新未生效,尝试使用 --force 重新预构建。