工程化篇 | Vite
前言
如果说 Webpack 定义了前端构建工具的”第一个时代”,那么 Vite 就是推开”第二个时代”大门的那个人。
2020 年,尤雨溪发布了 Vite 的第一个版本。当时很多人的第一反应是:“又一个打包工具?“但用过之后才发现,Vite 的开发体验和 Webpack 完全不在一个量级——冷启动从几十秒变成几百毫秒,改一行代码几乎瞬间就能看到效果。
但面试中被问到 Vite,很多人的回答还停留在”它比 Webpack 快”。面试官继续追问:
- Vite 为什么快?快在哪里?
- 开发模式和生产模式为什么用不同的方案?
- 什么是”预构建”?为什么需要它?
- Vite 能不能完全替代 Webpack?
就答不上来了。
本章,我们从 Vite 的设计哲学出发,深入理解它的开发模式和生产模式两套方案,再对比 Webpack 的本质差异,最后聊聊插件系统和常见配置。
诊断自测
Q1:Vite 在开发模式下为什么不打包?浏览器怎么处理那些 import 语句?
点击查看答案
Vite 在开发模式下利用了浏览器原生的 ES Module(<script type="module">)能力。当浏览器解析到 import 语句时,会向 Vite 的开发服务器发起 HTTP 请求,Vite 按需编译并返回对应的模块。这样就不需要像 Webpack 那样在启动时打包所有模块,从而实现了极快的冷启动。
Q2:Vite 的”预构建(Pre-bundling)“是做什么的?为什么需要它?
点击查看答案
预构建有两个目的:
- 兼容 CommonJS/UMD:node_modules 中大量的包是 CJS 格式的(如 React),浏览器不认识
require,需要先把它们转成 ESM 格式 - 合并模块请求:有些包(如 lodash-es)有几百个独立的小模块文件,如果不合并,浏览器会发出几百个 HTTP 请求,性能反而更差
Vite 使用 esbuild 在首次启动时快速预构建这些依赖,把它们各自合并成单个 ESM 文件,并缓存到 node_modules/.vite 中。
Q3:Vite 在生产模式下用的是 Rollup 而不是 esbuild 来打包,为什么?
点击查看答案
虽然 esbuild 速度极快,但在生产打包所需的一些高级特性上不够成熟,比如:代码分割(Code Splitting)的灵活策略、CSS 代码分割、遗留浏览器兼容(需要生成不同的 polyfill 版本)等。Rollup 在这些方面更成熟、生态更丰富、产物质量更高。所以 Vite 选择了”开发用 esbuild(快),生产用 Rollup(稳)“的双引擎策略。
一、Vite 的设计哲学:为什么不打包?
1.1 Webpack 的问题
在传统的 Webpack 开发流程中,每次启动开发服务器都要经历这样的过程:
全部源码 → 依赖分析 → 模块转换 → 打包 → 启动 Dev Server → 页面可用
项目越大,这个过程越慢。一个中大型项目冷启动 30 秒是常态,有些巨型项目甚至要好几分钟。
更痛苦的是 HMR。Webpack 的 HMR 虽然不用整页刷新,但它需要重新打包受影响的模块链,项目越大越慢。
1.2 Vite 的核心洞察
Vite 观察到一个关键事实:现代浏览器已经原生支持 ES Module 了。
ES Module 的 import / export 语法不仅是 JavaScript 的语言特性,也是浏览器实际支持的模块加载方式。当你在 HTML 中写 <script type="module" src="./main.js">,浏览器会:
- 请求
main.js - 解析其中的
import语句 - 对每个
import发起新的 HTTP 请求 - 递归加载所有依赖
既然浏览器自己就能处理模块加载,那开发时为什么还要打包?
Vite 的核心思路:在开发模式下,不打包,让浏览器自己加载模块。 服务器只负责”按需编译”——浏览器请求哪个文件,服务器就编译哪个文件。
二、开发模式:原生 ESM + esbuild 预构建
2.1 按需编译
浏览器请求 main.js → Vite 编译 main.js(如果是 .vue/.ts,先转换)→ 返回 ESM 格式的 JS
→ 浏览器解析 import,请求 App.vue
→ Vite 编译 App.vue → 返回
→ ......
这个过程是按需的——只有浏览器实际请求到的文件才会被编译。如果你的项目有 1000 个文件,但当前页面只用到了 50 个,那就只编译 50 个。
这就是 Vite 冷启动快的根本原因:Webpack 需要在启动前打包所有模块,而 Vite 把”编译”推迟到了浏览器请求的那一刻。
2.2 预构建(Dependency Pre-Bundling)
但是,“让浏览器直接加载 node_modules”会遇到两个问题:
问题一:CJS 不兼容
很多 npm 包还是 CommonJS 格式的(require / module.exports),浏览器不认识。
问题二:模块过多导致请求爆炸
有些包(比如 lodash-es)有几百个独立的小模块文件。如果浏览器一个一个地请求,性能会很差。
import { debounce } from 'lodash-es';
// lodash-es 内部还会 import 几十个其他模块
// 浏览器可能要发出上百个请求
为了解决这两个问题,Vite 会在首次启动时用 esbuild 对 node_modules 中的依赖做预构建:
- 把 CJS/UMD 格式的包转成 ESM
- 把一个包的几百个小模块合并成一个文件
- 结果缓存到
node_modules/.vite目录
node_modules/lodash-es/(几百个文件)
↓ esbuild 预构建
node_modules/.vite/deps/lodash-es.js(一个文件)
为什么选 esbuild?因为 esbuild 是用 Go 写的,速度比 JavaScript 写的打包器快 10-100 倍。预构建通常在几百毫秒内就能完成。
2.3 HMR:比 Webpack 更快
Vite 的 HMR 同样基于原生 ESM。当一个文件发生变化:
- Vite 只需要精确地让该模块失效
- 浏览器重新请求这一个模块
- 不需要重新打包整条依赖链
而 Webpack 的 HMR 需要重新构建受影响的 chunk,项目越大越慢。Vite 的 HMR 速度和项目大小几乎无关——因为无论项目有多大,每次更新都只处理变更的那个模块。
三、生产模式:Rollup 打包
3.1 为什么生产环境不能也用原生 ESM?
你可能会问:既然开发模式下原生 ESM 这么好用,生产环境为什么不也这样?原因有几个:
- 网络请求过多:原生 ESM 在运行时递归加载模块,意味着大量的 HTTP 请求。生产环境下,这些请求的延迟会严重影响用户体验
- 缺少优化:Tree Shaking、代码压缩、代码分割等优化,都需要打包器在构建时做静态分析,运行时加载做不了这些
- 兼容性:虽然现代浏览器都支持 ESM,但生产环境可能还需要兼容老浏览器
所以 Vite 在生产模式下使用 Rollup 做传统的打包:
源码 → Rollup 打包 → Tree Shaking → 代码分割 → 压缩 → 输出产物
3.2 为什么选 Rollup 而不是 esbuild?
esbuild 虽然快,但在以下方面不够成熟:
- 代码分割策略不够灵活:生产环境需要精细地控制 chunk 的拆分策略
- CSS 代码分割:Rollup 生态有成熟的方案
- 插件生态:Rollup 的插件生态更丰富、更稳定
Vite 的创造者尤雨溪在博客中也提到:当 esbuild 在这些方面成熟后,不排除未来会切换。事实上,Vite 团队也在推进 Rolldown(用 Rust 重写的 Rollup 兼容构建工具),目标是统一开发和生产的构建引擎。
3.3 Rollup 的核心优势
Rollup 天然适合库和应用的打包,它的几个特点:
- ESM 优先:Rollup 从诞生之初就以 ESM 为核心,Tree Shaking 能力强
- 产物干净:相比 Webpack 的运行时代码,Rollup 的产物更精简
- 插件生态好:大量成熟的 Rollup 插件可以直接在 Vite 中使用
四、Vite vs Webpack:本质差异
| 维度 | Webpack | Vite |
|---|---|---|
| 开发模式原理 | 先打包所有模块,再启动 Dev Server | 不打包,基于原生 ESM 按需编译 |
| 冷启动速度 | 慢(项目越大越慢) | 快(与项目大小几乎无关) |
| HMR 速度 | 需要重建受影响的 chunk | 只处理变更模块,速度恒定 |
| 生产打包 | 自身打包 | 委托 Rollup |
| 模块格式 | CJS / ESM 都支持 | ESM 优先 |
| 配置复杂度 | 高(需要手动配置大量 loader/plugin) | 低(大量功能开箱即用) |
| 生态成熟度 | 极其成熟 | 快速成长中 |
| 适合场景 | 复杂的大型项目、需要精细控制的场景 | 新项目、追求开发体验的场景 |
一句话总结: Webpack 是”先打包,再开发”;Vite 是”先开发,再打包(仅生产环境)“。
为什么 Vite 不能完全替代 Webpack?
- 生态差距:Webpack 有十年的生态积累,很多特殊需求只有 Webpack 的 loader/plugin 能满足
- 微前端:qiankun 等微前端方案对 Webpack 的支持更好
- 存量项目:大量已有项目基于 Webpack 构建,迁移成本不可忽视
- 开发/生产一致性:Vite 开发用 ESM + esbuild,生产用 Rollup,两套系统偶尔会有行为不一致的问题
五、Vite 插件系统
5.1 基于 Rollup 插件接口
Vite 的插件系统基于 Rollup 的插件接口设计,大部分 Rollup 插件可以直接在 Vite 中使用。同时 Vite 扩展了一些开发服务器特有的钩子。
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import legacy from '@vitejs/plugin-legacy';
export default defineConfig({
plugins: [
vue(),
legacy({
targets: ['defaults', 'not IE 11']
})
]
});
5.2 插件钩子分类
Vite 的插件钩子分为两类:
通用钩子(和 Rollup 共享):
resolveId:自定义模块解析load:自定义模块加载transform:转换模块内容
Vite 独有钩子:
configResolved:Vite 配置解析完成后调用configureServer:配置开发服务器(可以添加自定义中间件)transformIndexHtml:转换index.htmlhandleHotUpdate:自定义 HMR 更新处理
5.3 一个简单的 Vite 插件
function myPlugin() {
return {
name: 'my-plugin',
// Vite 独有:配置开发服务器
configureServer(server) {
server.middlewares.use('/api/hello', (req, res) => {
res.end(JSON.stringify({ message: 'Hello from plugin!' }));
});
},
// Rollup 通用:转换模块内容
transform(code, id) {
if (id.endsWith('.js')) {
return code.replace(/__VERSION__/g, '"1.0.0"');
}
}
};
}
六、常见配置
6.1 基础配置
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
// 开发服务器
server: {
port: 3000,
open: true, // 自动打开浏览器
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
// 构建选项
build: {
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash-es', 'dayjs']
}
}
}
},
// 路径别名
resolve: {
alias: {
'@': '/src'
}
},
// CSS 配置
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
}
}
});
6.2 环境变量
Vite 使用 .env 文件管理环境变量,只有以 VITE_ 开头的变量才会暴露给客户端代码:
# .env.development
VITE_API_URL=http://localhost:8080
VITE_APP_TITLE=My App (Dev)
// 在代码中使用
console.log(import.meta.env.VITE_API_URL);
console.log(import.meta.env.MODE); // 'development' | 'production'
6.3 多页面应用
export default defineConfig({
build: {
rollupOptions: {
input: {
main: 'index.html',
admin: 'admin.html'
}
}
}
});
常见误区
误区一:“Vite 比 Webpack 快是因为用了 esbuild 打包”
不准确。Vite 在开发模式下的”快”主要来自不打包——基于原生 ESM 按需编译,esbuild 只是用来做预构建和 TS/JSX 转译的。在生产模式下,Vite 用的是 Rollup 打包,速度和 Webpack 差不多。把 Vite 的速度优势简单归结为”esbuild 快”,说明没有理解 Vite 的核心设计。
误区二:“Vite 只适合小项目”
恰恰相反,Vite 的按需编译特性让它在大项目中的优势更明显。项目越大,Webpack 冷启动越慢,而 Vite 的冷启动时间几乎不受项目大小影响。很多大型项目(如 Vue 3 官方生态、SvelteKit、Nuxt 3、Astro)都在用 Vite。
误区三:“Vite 的开发模式和生产模式行为完全一致”
不一致。开发模式基于原生 ESM + esbuild 转译,生产模式基于 Rollup 打包。两套系统偶尔会有行为差异,比如某些模块在开发模式下正常但生产打包后报错(通常是因为 CJS/ESM 互操作的问题)。这也是 Vite 团队推进 Rolldown 的原因之一——用同一个引擎同时服务开发和生产。
误区四:“用了 Vite 就不需要了解 Rollup 了”
Vite 的生产打包就是 Rollup,插件系统也是基于 Rollup 的。如果你需要自定义打包行为、写 Vite 插件或者排查生产构建问题,Rollup 的知识是必须的。build.rollupOptions 这个配置项直接透传给 Rollup,不了解 Rollup 等于黑箱操作。
小结
本章我们从 Vite 的设计哲学出发,理解了它”开发不打包、生产用 Rollup”的双模式架构,深入分析了原生 ESM 按需编译和 esbuild 预构建的工作原理。
核心要点
- Vite 开发模式的核心:利用浏览器原生 ESM,按需编译,不做全量打包
- 预构建的两个目的:CJS → ESM 转换 + 合并小模块(用 esbuild 实现)
- 生产模式用 Rollup:因为 Rollup 在代码分割、CSS 处理、插件生态方面更成熟
- Vite vs Webpack 的本质差异:不是”用了更快的打包器”,而是”开发时根本不打包”
- 插件系统基于 Rollup:大部分 Rollup 插件可以直接用,Vite 额外扩展了 Dev Server 相关的钩子
- Rolldown 是未来方向:用 Rust 重写的 Rollup 兼容引擎,目标是统一开发和生产
本章思维导图
- 设计哲学
- 核心洞察:浏览器原生支持 ESM
- 开发时不打包,按需编译
- 开发模式
- 原生 ESM:浏览器请求什么,编译什么
- esbuild 预构建:CJS→ESM、合并小模块
- HMR:模块级精确更新,速度与项目大小无关
- 生产模式
- 为什么需要打包:请求数量、优化、兼容性
- 为什么选 Rollup:代码分割、CSS 处理、插件生态
- 未来方向:Rolldown(Rust 重写的 Rollup)
- vs Webpack
- 开发:按需编译 vs 全量打包
- HMR:模块级更新 vs chunk 级重建
- 生态:快速成长 vs 极度成熟
- 插件系统
- 基于 Rollup 插件接口
- 通用钩子:resolveId / load / transform
- Vite 独有钩子:configureServer / handleHotUpdate
- 常见配置
- server(端口、代理)
- build(产物、rollupOptions)
- resolve.alias / css / 环境变量
练习挑战
第一题 ⭐(基础):概念理解
请解释以下三个概念的关系:原生 ESM、esbuild、Rollup。它们各自在 Vite 中承担什么角色?
点击查看答案与解析
- 原生 ESM:Vite 开发模式的基础。Vite 利用浏览器原生的 ES Module 能力,让浏览器自己处理模块加载,服务器只做按需编译。
- esbuild:Vite 开发模式的”加速器”。负责两件事:(1) 预构建第三方依赖(CJS→ESM 转换 + 合并小模块),(2) TypeScript 和 JSX 的快速转译。
- Rollup:Vite 生产模式的打包引擎。负责代码分割、Tree Shaking、压缩等生产级优化,产物质量高、生态成熟。
三者分工明确:原生 ESM 是开发模式的”运行时策略”,esbuild 是开发模式的”编译加速器”,Rollup 是生产模式的”打包引擎”。
第二题 ⭐⭐(进阶):排查预构建问题
你在项目中安装了一个新的 npm 包 awesome-lib,在开发模式下 import { helper } from 'awesome-lib' 报错:Uncaught SyntaxError: Unexpected token 'exports'。请分析原因并给出解决方案。
点击查看答案与解析
原因: awesome-lib 是 CommonJS 格式的包(使用了 module.exports / exports),但浏览器只认 ESM 语法。这意味着 Vite 的预构建没有正确处理这个包。
可能的原因:
- 这个包没有被 Vite 识别为需要预构建的依赖(比如它是通过某种间接方式引入的)
- 预构建缓存过期或损坏
解决方案:
- 手动将该包加入预构建列表:
// vite.config.js
export default defineConfig({
optimizeDeps: {
include: ['awesome-lib']
}
});
- 清除预构建缓存并重启:
rm -rf node_modules/.vite
npx vite --force # 强制重新预构建
- 如果包本身有 ESM 版本,检查
package.json中的module或exports字段是否正确指向了 ESM 入口。
第三题 ⭐⭐⭐(综合):写一个 Vite 插件
请写一个 Vite 插件 vite-plugin-build-info,功能:
- 在开发模式下,启动服务器后在控制台打印
[BuildInfo] Dev server started at {时间} - 在生产模式下,在打包后的
index.html底部注入一段 HTML 注释:<!-- Built at: {时间} -->
点击查看答案与解析
function buildInfoPlugin() {
return {
name: 'vite-plugin-build-info',
// 开发模式:服务器启动后打印信息
configureServer(server) {
server.httpServer?.once('listening', () => {
console.log(`[BuildInfo] Dev server started at ${new Date().toISOString()}`);
});
},
// 生产模式:在 HTML 底部注入构建时间注释
transformIndexHtml(html) {
// transformIndexHtml 只在 build 时生效(开发模式也会调用,但这里我们都注入也没关系)
const buildTime = new Date().toISOString();
return html.replace('</html>', `<!-- Built at: ${buildTime} -->\n</html>`);
}
};
}
export default buildInfoPlugin;
要点解析:
configureServer是 Vite 独有钩子,只在开发模式下被调用,可以访问底层的 HTTP 服务器transformIndexHtml是 Vite 独有钩子,用于转换index.html的内容- 插件函数返回一个对象,必须有
name属性(用于调试和错误报告) - 这道题考察的是对 Vite 插件系统两类钩子(通用钩子 vs Vite 独有钩子)的理解
自我检测
- 能解释 Vite 在开发模式下”不打包”的原理,包括浏览器原生 ESM 如何工作
- 能说清楚预构建(Pre-bundling)的两个目的和实现方式
- 能解释为什么生产模式选择 Rollup 而不是 esbuild
- 能列举 Vite 和 Webpack 在开发模式、HMR、生产打包三个维度的核心差异
- 能区分 Vite 插件的两类钩子(Rollup 通用钩子 vs Vite 独有钩子)
- 能配置 Vite 的开发服务器代理、路径别名、环境变量等常见选项
- 能解释开发模式和生产模式”双引擎”带来的行为不一致风险
- 能说出 Rolldown 项目的目标和它对 Vite 未来的意义
购买课程解锁全部内容
大厂前端面试通关:71 篇构建完整知识体系
¥89.90