跳到主要内容

依赖预构建

当你第一次运行 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 使用以下流程自动发现和预构建依赖:

  1. 扫描源代码:启动时,Vite 会扫描你的源代码,找到所有"裸导入"(即期望从 node_modules 解析的导入)
  2. 预构建:使用 Rolldown 将发现的依赖打包成 ESM 格式
  3. 缓存:预构建结果缓存在 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.jsonyarn.lockpnpm-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 会显示错误信息。常见原因:

  1. 依赖使用了 Node.js 内置模块:需要在配置中提供 polyfill
  2. 依赖使用了特殊的语法:需要配置 esbuild 插件
  3. 依赖导出结构不标准:检查依赖的导出方式

依赖更改后页面不更新

如果修改了依赖(例如 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.includeoptimizeDeps.exclude 进行调整。修改依赖后如果发现更新未生效,尝试使用 --force 重新预构建。