主题
第 17 章 · 构建工具(Vite / Webpack / Rollup / esbuild / Turbopack)
一句话开篇:本章你将看懂"为什么我写的
.tsx/.vue/.scss浏览器能跑"——构建工具就是那个"翻译官 + 打包工"。
0. 生活类比(先建立直觉)
0.1 构建工具 = 装修队 + 物流公司
你写代码就像写菜谱(用各种语言:TS、JSX、SCSS、Vue),但浏览器只认三样原料:HTML、CSS、JS。中间需要一个"加工队"做三件事:
- 翻译(编译):TS → JS、SCSS → CSS、JSX → JS
- 打包(合并):把成百上千个文件合并成几个 bundle
- 优化(压缩、分割、Tree Shaking):把成品打磨得又小又快
0.2 Webpack vs Vite = 老牌翻译社 vs AI 实时翻译
- Webpack 像老牌翻译社:每次启动开发服务器,都要先把整本书翻译一遍才能看(冷启动慢),改一个字也要重新走一次工序(热更新慢)
- Vite 像 AI 实时翻译:启动几乎瞬间,浏览器要哪页就翻哪页(按需编译),改了哪页就只重译那一页(HMR 极快)
0.3 Tree Shaking = 装修前清掉没用的家具
你网购了 100 件家具(一整个 lodash),但实际只用 5 件(只用了 debounce 和 throttle)。聪明的装修队会在搬进家之前帮你筛掉 95 件,只搬必要的——这就是"摇树"。
0.4 代码分割 = 自助餐分桌
不要把 1000 道菜全摆一张桌上(一个超大 bundle)压垮地板(首屏加载慢)。按"前菜桌、主餐桌、甜品桌"分开(按路由 / 按功能切分),客人走到哪桌再上哪桌的菜(懒加载)。
1. 概念是什么
1.1 构建工具(Build Tool)
把源代码(含各种"非标准"语言)变成浏览器可执行产物的工具链。涵盖:
- 编译(compile)
- 打包(bundle)
- 转换(transform,如 SCSS → CSS)
- 优化(minify、tree-shake、split)
- 开发服务器(dev server,含 HMR)
1.2 五大主流构建工具速览
| 工具 | 定位 | 写的语言 | 适用场景 |
|---|---|---|---|
| Vite | 现代构建工具 | JS(用 esbuild + Rollup) | 应用开发首选 |
| Webpack | 老牌打包器 | JS | 老项目、复杂自定义需求 |
| esbuild | 极速 bundler / transformer | Go | Vite/Vitest 内部使用 |
| Rollup | 库打包器 | JS | npm 库打包首选 |
| Turbopack | Next.js 新打包器 | Rust | Next.js 项目(增量编译) |
2. 为什么需要它
2.1 不用构建工具会怎样?
假设你的代码是这样:
html
<!-- 浏览器打不开 -->
<script src="App.tsx"></script>
<script src="utils.ts"></script>
<link rel="stylesheet" href="theme.scss" />浏览器看到 .tsx、.ts、.scss 直接懵——根本不认识。没有构建工具,整个现代前端工作流不存在。
2.2 构建工具到底解决了什么问题?
| 痛点 | 构建工具的解决方案 |
|---|---|
| 浏览器不认识 TS/JSX/SCSS | 编译为 JS/CSS |
| 文件太多请求太慢 | 打包合并 |
| 文件太大首屏卡 | 代码分割、Tree Shaking、压缩 |
| 改代码要刷页面 | HMR(热模块替换) |
| 引入第三方库要手写 script | npm 生态 + 模块解析 |
| 旧浏览器不支持新语法 | Babel/SWC 转译 + Polyfill |
| 静态资源(图片/字体) | 自动 hash + 优化 |
3. 怎么用(最小示例)
3.1 Vite 一分钟启动
bash
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev # 启动开发服务器(< 1 秒)
npm run build # 生产构建3.2 最简 vite.config.ts
ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 5173 },
});3.3 最简 webpack.config.js
js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
],
},
mode: 'production',
};4. 进阶要点
4.1 Vite 原理:开发模式 vs 生产模式
Vite 最大的创新是开发模式不打包,直接利用浏览器的 ESM 能力。
【开发模式 (dev)】 - 极速冷启动
浏览器请求 /src/main.ts
↓
Vite Dev Server 拦截
↓
┌────────────┐
│ esbuild │ ← 即时编译 TS/JSX → JS(不打包)
│ (毫秒级) │
└─────┬──────┘
↓
返回 ESM 模块给浏览器
↓
浏览器用原生 import 继续按需加载子模块
【依赖预构建(dependency pre-bundling)】
首次启动时,把 node_modules 中的 CJS/UMD 模块用 esbuild 转成 ESM
缓存到 node_modules/.vite/
后续直接命中缓存(~10ms)
【生产模式 (build)】
为什么不用 esbuild 直接打包?
→ esbuild 的代码分割、CSS 处理还不够成熟
→ 改用 Rollup 打包,产物质量更高(Tree Shaking 极佳)核心理念:开发要快,生产要稳——分别用最擅长的工具。
4.2 Webpack 五大核心概念
┌──────────────┐
┌──────────►│ loader │── 转换文件 (ts→js, scss→css)
│ └──────────────┘
┌──────────┴───┐ ┌──────────────┐
│ entry │──────►│ plugin │── 介入构建生命周期
│ (入口文件) │ └──────────────┘
└──────────────┘ ┌──────────────┐
▲ │ output │── 输出位置和文件名
│ └──────────────┘
│ ┌──────────────┐
└───────────│ mode │── development / production / none
└──────────────┘- entry:从哪里开始打包(可多个)
- output:打包到哪里、怎么命名
- loader:让 webpack 能"看懂"非 JS 文件(ts-loader、css-loader、file-loader)
- plugin:在构建过程的各个钩子做事(HtmlWebpackPlugin、DefinePlugin、MiniCssExtractPlugin)
- mode:决定内置优化策略(production 自动压缩、treeshake)
4.3 esbuild 简介
- 语言:Go(编译型,多线程)
- 速度:比 Webpack 快 10~100 倍
- 角色:Vite 的 dev 时编译 + Vitest 的转换器 + Snowpack/tsup 等工具的内核
- 不足:插件生态弱、CSS 处理简陋、产物的代码分割能力不如 Rollup
bash
# 直接当 CLI 用
npx esbuild src/main.ts --bundle --outfile=dist/bundle.js4.4 Rollup 简介
- 定位:库打包器(不是应用打包器)
- 强项:Tree Shaking 极致、产物干净(少胶水代码)
- 谁在用:Vue 3、React、几乎所有 npm 库的官方打包都用 Rollup
- Vite 的生产模式用的就是 Rollup
js
// rollup.config.js
import typescript from '@rollup/plugin-typescript';
export default {
input: 'src/index.ts',
output: [
{ file: 'dist/index.cjs', format: 'cjs' },
{ file: 'dist/index.mjs', format: 'es' }
],
plugins: [typescript()],
external: ['react'] // 不打包 react,由用户提供
};4.5 Turbopack 简介
- 谁出的:Vercel(Next.js 母公司)
- 语言:Rust
- 目标:Webpack 的接班人(支持持久缓存 + 增量编译)
- 现状(2026):已在 Next.js 中作为默认 dev server
4.6 Tree Shaking 原理
前提:ESM 静态结构(import/export 必须出现在文件顶层)
js
// utils.js
export function used() {}
export function unused() {} // 没人 import
export function alsoUnused() {}
// main.js
import { used } from './utils';
used();
// 打包后,unused 和 alsoUnused 被丢弃关键限制:
- CJS 不支持(
require是动态的) - 有"副作用"的代码不能丢(修改全局、注册插件等)
- 库需要在
package.json写"sideEffects": false
4.7 代码分割(Code Splitting)
把一个超大 bundle 拆成多个 chunk,按需加载。
三种触发方式:
- 多入口:
entry: { home: ..., admin: ... } - 动态 import:
import('./Page')自动单独打 chunk - 公共依赖提取:
splitChunks配置(如把 react/react-dom 抽成 vendor.js)
打包前:
src/
├── main.js (3KB)
├── HomePage.js (50KB)
├── AdminPage.js (80KB) ← 90% 用户用不到
└── lodash (70KB)
打包后(合理分割):
dist/
├── main.[hash].js (5KB) ← 入口
├── home.[hash].js (50KB) ← 路由懒加载
├── admin.[hash].js (80KB) ← 路由懒加载
└── vendor.[hash].js (70KB) ← 公共依赖4.8 HMR(Hot Module Replacement)原理
1. 浏览器与 dev server 建立 WebSocket 连接
2. 你保存代码 → dev server 检测到文件变化
3. dev server 编译变更的模块,发送给浏览器
4. 浏览器收到消息,调用 module.hot.accept() 替换该模块
5. React/Vue 组件状态保持,仅"热替换"了组件代码
效果:改个颜色 → 0.1 秒内看到,不刷新页面、状态不丢4.9 Vite vs Webpack 速度差异原因
| 维度 | Webpack | Vite |
|---|---|---|
| 冷启动 | 慢(要打包整个项目才能跑) | 快(不打包,按需编译) |
| HMR | 慢(要重新打包受影响的 chunk) | 快(只编译那一个文件) |
| 编译器 | JS(v8 单线程) | esbuild(Go 多线程) |
| Dev server 响应 | 编译完成后一次性返回 | 浏览器请求时即时编译 |
| 第三方库处理 | 每次 dev 都参与打包 | 一次预构建后缓存 |
| 项目越大 | 越慢(线性恶化) | 几乎不受影响 |
典型对比(中型项目):
| 操作 | Webpack | Vite |
|---|---|---|
| 冷启动 | 10~30 秒 | < 1 秒 |
| HMR | 1~3 秒 | < 100ms |
| 生产构建 | 1~3 分钟 | 30~60 秒(用 Rollup) |
4.10 主流工具横向对比
| 工具 | 语言 | 角色 | 速度 | Tree Shaking | 适合 |
|---|---|---|---|---|---|
| Vite | JS+Go+JS | 应用开发 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | React/Vue 应用 |
| Webpack | JS | 应用打包 | ⭐⭐ | ⭐⭐⭐ | 老项目、企业级 |
| esbuild | Go | bundler/transpiler | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 工具链内核 |
| Rollup | JS | 库打包 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | npm 库 |
| Turbopack | Rust | 应用打包 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Next.js |
| Parcel | JS | 零配置打包 | ⭐⭐⭐⭐ | ⭐⭐⭐ | 学习/原型 |
5. 实战案例
详见 examples/ 目录:
vite.config.ts— Vite 完整配置示例webpack.config.js— Webpack 完整配置示例rollup.config.js— Rollup 库打包配置示例
6. ⚠️ 易踩的坑
坑 1:Vite 开发能跑、生产构建报错
开发模式不打包,浏览器对动态语法很宽容;生产模式用 Rollup 打包,更严格。
典型例子:循环依赖在 dev 下相安无事,build 时报错。
✅ 办法:CI 中用 vite build 而非 vite;本地开发也可定期跑一次 build 验证。
坑 2:Webpack 配错 mode
js
mode: 'production' // ✅ 自动压缩、treeshake、设置 NODE_ENV
mode: 'development' // 默认源码可读、有 sourcemap
mode: 'none' // 完全不优化(极少用)漏写 mode → 默认 production,但开发时也被压缩,调试痛苦。
坑 3:process.env.NODE_ENV 没换成字符串
js
// 源代码
if (process.env.NODE_ENV === 'production') { ... }
// Webpack 用 DefinePlugin / Vite 用 define 配置
// 编译时被替换成
if ('production' === 'production') { ... }
// 然后 minifier 直接保留 if 内的代码、删掉 dev 代码漏配 → dev 代码混到生产,bundle 多几十 KB。
坑 4:把图片用 import 导入,但没配置资源处理
js
import logo from './logo.png'; // Webpack 需要 file-loader/asset module✅ Webpack 5+ 用 type: 'asset/resource';Vite 默认就支持。
坑 5:开发用 default import 引入 CJS 包导致打包出错
js
// 某个 CJS 库
module.exports = { foo: 1 };
// Vite dev 下能跑(依赖预构建会做兼容)
import lib from 'cjs-lib';
console.log(lib.foo);
// build 后可能报 lib.foo undefined
// 因为 ESM 导入 CJS 时,整个 module.exports 可能挂在 default 上✅ 看库的 package.json exports 字段,按规范引入;或加 optimizeDeps.include。
坑 6:忘记设置 base 部署到子路径出问题
ts
// vite.config.ts
export default defineConfig({
base: '/my-app/', // 部署到 https://example.com/my-app/ 时必须配
});漏写 → 静态资源路径 /assets/xxx.js,但实际在 /my-app/assets/xxx.js,404。
坑 7:Tree Shaking 失效
常见原因:
- 库是 CJS(如老版 lodash) → 用 lodash-es 替代
- 没用命名 import:
import _ from 'lodash'不行,要import { debounce } from 'lodash-es' - 库没声明
sideEffects: false - 副作用代码(CSS import、polyfill)没被识别保留
坑 8:HMR 不生效
常见原因:
- 模块没正确 export(如直接修改 window 全局)
- React 组件没用 React.lazy 或 hooks,部分情况无法热替换
- 框架插件未启用(Vite 必须用
@vitejs/plugin-react或-vue)
坑 9:构建产物没加 hash 导致 CDN 缓存不更新
js
// ❌ 上线后用户拿不到新代码
output: { filename: 'bundle.js' }
// ✅ 内容变化时文件名变化,自动破缓存
output: { filename: '[name].[contenthash].js' }7. 最佳实践 Checklist
- [ ] 新项目首选 Vite,老项目继续 Webpack 也无妨
- [ ] 库打包用 Rollup(或 tsup / unbuild)
- [ ] 生产构建必须
mode: 'production',启用压缩 + Tree Shaking - [ ] 文件名带
[contenthash]用于长期缓存 - [ ] 路由级别使用 动态 import 实现代码分割
- [ ] 引入大型库时优先选择 ESM 版本(如 lodash-es)
- [ ] 配置 sourcemap:dev 用
cheap-module-source-map,prod 用source-map(或上传到 Sentry) - [ ] Vite 项目里把首屏依赖加入
optimizeDeps.include减少卡顿 - [ ] 使用
vite-bundle-visualizer/webpack-bundle-analyzer定期审视 bundle 体积 - [ ] CSS 启用代码分割(按 chunk 抽 css)
- [ ] 生产构建关键路径资源加
<link rel="preload"> - [ ] CI 中跑
npm run build校验,避免"dev 能跑 build 报错"
8. 一句话总结
构建工具 = 把"开发体验最好的源码"翻译成"运行体验最好的产物"——开发时用 Vite/esbuild 追极速,生产时用 Rollup/Webpack 追产物质量。