Skip to content

第 17 章 · 构建工具(Vite / Webpack / Rollup / esbuild / Turbopack)

一句话开篇:本章你将看懂"为什么我写的 .tsx / .vue / .scss 浏览器能跑"——构建工具就是那个"翻译官 + 打包工"。


0. 生活类比(先建立直觉)

0.1 构建工具 = 装修队 + 物流公司

你写代码就像写菜谱(用各种语言:TS、JSX、SCSS、Vue),但浏览器只认三样原料:HTML、CSS、JS。中间需要一个"加工队"做三件事:

  1. 翻译(编译):TS → JS、SCSS → CSS、JSX → JS
  2. 打包(合并):把成百上千个文件合并成几个 bundle
  3. 优化(压缩、分割、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 / transformerGoVite/Vitest 内部使用
Rollup库打包器JSnpm 库打包首选
TurbopackNext.js 新打包器RustNext.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(热模块替换)
引入第三方库要手写 scriptnpm 生态 + 模块解析
旧浏览器不支持新语法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.js

4.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,按需加载。

三种触发方式

  1. 多入口entry: { home: ..., admin: ... }
  2. 动态 importimport('./Page') 自动单独打 chunk
  3. 公共依赖提取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 速度差异原因

维度WebpackVite
冷启动慢(要打包整个项目才能跑)快(不打包,按需编译)
HMR慢(要重新打包受影响的 chunk)快(只编译那一个文件)
编译器JS(v8 单线程)esbuild(Go 多线程)
Dev server 响应编译完成后一次性返回浏览器请求时即时编译
第三方库处理每次 dev 都参与打包一次预构建后缓存
项目越大越慢(线性恶化)几乎不受影响

典型对比(中型项目):

操作WebpackVite
冷启动10~30 秒< 1 秒
HMR1~3 秒< 100ms
生产构建1~3 分钟30~60 秒(用 Rollup)

4.10 主流工具横向对比

工具语言角色速度Tree Shaking适合
ViteJS+Go+JS应用开发⭐⭐⭐⭐⭐⭐⭐⭐⭐React/Vue 应用
WebpackJS应用打包⭐⭐⭐⭐⭐老项目、企业级
esbuildGobundler/transpiler⭐⭐⭐⭐⭐⭐⭐⭐工具链内核
RollupJS库打包⭐⭐⭐⭐⭐⭐⭐⭐npm 库
TurbopackRust应用打包⭐⭐⭐⭐⭐⭐⭐⭐⭐Next.js
ParcelJS零配置打包⭐⭐⭐⭐⭐⭐⭐学习/原型

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 追产物质量。


9. 延伸阅读