Skip to content

构建工具深度解析

一、构建工具演进史与核心问题

1.1 为什么需要构建工具?

早期 Web 开发                现代 Web 开发
─────────────             ─────────────
HTML + CSS + JS          →  TypeScript/JSX/Vue SFC
手动引入 <script>        →  模块化 (ESM/CJS)
全量加载                  →  按需加载/代码分割
无优化                    →  压缩/Tree Shaking/Polyfill

核心解决的问题:

  1. 模块化打包 - 解决浏览器不支持 CommonJS/ESM 的历史问题
  2. 语法转换 - TS → JS, JSX → JS, SCSS → CSS
  3. 性能优化 - 压缩、Tree Shaking、Code Splitting
  4. 开发体验 - HMR、Source Map、Dev Server

1.2 构建工具的两种架构范式

┌─────────────────────────────────────────────────────────────┐
│                   Bundle-based (Webpack)                     │
├─────────────────────────────────────────────────────────────┤
│  源代码  →  解析依赖  →  构建依赖图  →  打包 Bundle  →  浏览器  │
│                                                               │
│  特点: 启动前必须处理所有模块,项目越大启动越慢                    │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                    Native ESM (Vite)                         │
├─────────────────────────────────────────────────────────────┤
│  浏览器请求  →  按需编译单个模块  →  返回 ESM  →  浏览器解析     │
│                                                               │
│  特点: 按需编译,启动快,但运行时请求多                          │
└─────────────────────────────────────────────────────────────┘

二、深入分析:为什么 Vite 比 Webpack 快

2.1 根本原因:开发模式的架构差异

Webpack 开发模式流程

项目启动


┌─────────────────────────────────────────┐
│  遍历所有入口文件,递归解析所有依赖       │ ← 耗时瓶颈 1
│  (可能数千个模块)                         │
└─────────────────────────────────────────┘


┌─────────────────────────────────────────┐
│  对每个模块调用 Loader 链进行转换         │ ← 耗时瓶颈 2
│  (Babel/TS/CSS 等都需要 AST 解析)        │
└─────────────────────────────────────────┘


┌─────────────────────────────────────────┐
│  生成 Bundle,写入内存文件系统           │ ← 耗时瓶颈 3
└─────────────────────────────────────────┘


启动 Dev Server (可能需要 30s - 2min+)

Vite 开发模式流程

项目启动


┌─────────────────────────────────────────┐
│  预构建 node_modules (esbuild, ~1s)     │ ← 仅处理依赖,极快
└─────────────────────────────────────────┘


启动 Dev Server (通常 < 1s)


浏览器请求 /src/main.tsx


┌─────────────────────────────────────────┐
│  仅编译 main.tsx (按需、单文件)          │ ← 运行时编译
└─────────────────────────────────────────┘


浏览器解析 import,继续请求依赖...

2.2 数据对比:真实项目启动时间

项目规模Webpack 冷启动Vite 冷启动倍数差异
小型 (~100 模块)8-15s0.5-1s~10x
中型 (~1000 模块)30-60s1-2s~30x
大型 (~5000+ 模块)2-5min2-5s~60x

2.3 五个关键技术差异

差异 1: 编译时机

javascript
// Webpack: 启动时编译所有模块
// 即使用户只访问首页,也要编译整个项目

// Vite: 按需编译
// 用户访问哪个页面,就编译哪个页面的依赖链
// 懒加载的模块在真正加载时才编译

差异 2: 编译器性能 (JavaScript vs Native)

┌─────────────────────────────────────────────────────────────┐
│  Webpack 编译链 (全部 JavaScript 实现)                        │
├─────────────────────────────────────────────────────────────┤
│  babel-loader (JS) → webpack (JS) → terser (JS)              │
│  单线程,受 V8 性能限制                                        │
│  典型速度: 100-500 模块/秒                                    │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  Vite 预构建 (esbuild - Go 实现)                              │
├─────────────────────────────────────────────────────────────┤
│  多线程并行,无 AST 传递开销                                   │
│  典型速度: 10000-100000 模块/秒 (比 JS 快 10-100x)            │
└─────────────────────────────────────────────────────────────┘
javascript
// esbuild 为什么快?
// 1. Go 编译为原生代码,无 JS 解释执行开销
// 2. 多核并行处理
// 3. 从零设计,避免历史包袱
// 4. 内存高效,最小化数据传递

// 性能对比 (大型项目)
// Babel + Webpack: 10+ 分钟
// esbuild: 几秒钟

差异 3: HMR 更新范围

javascript
// Webpack HMR: 需要重新构建整个 chunk
// 修改一个组件 → 重建包含该组件的整个 chunk → 发送完整 chunk

// Vite HMR: 精确到模块级别
// 修改一个组件 → 仅重新请求该模块 → 浏览器仅更新该模块

// 示例: 修改 Button.tsx
// Webpack: 可能需要重建 200KB 的 chunk
// Vite: 仅传输 5KB 的 Button.tsx

差异 4: 浏览器原生 ESM 支持

html
<!-- Vite 利用浏览器原生能力 -->
<script type="module">
  // 浏览器直接解析 import 语句
  import { createApp } from '/node_modules/.vite/deps/vue.js'
  import App from '/src/App.vue'
  
  // 浏览器自动发起请求获取依赖
</script>

<!-- Webpack 需要自己实现模块系统 -->
<script>
  // __webpack_require__ 模拟模块系统
  // 所有模块打包在一个 bundle 里
</script>

差异 5: 缓存策略

javascript
// Vite 缓存策略
// 1. 预构建结果缓存 (node_modules/.vite)
// 2. 304 协商缓存 (源文件未修改时)
// 3. 强缓存 (依赖文件加 hash)

// HTTP 缓存示例
// /src/App.vue?t=1699999999  → 协商缓存
// /.vite/deps/vue.js?v=abc123 → 强缓存 1 年

2.4 Vite 的代价与局限

javascript
// 1. 首屏请求瀑布流
// 大型项目首次加载可能有数百个模块请求
// 虽然 HTTP/2 多路复用,但仍有延迟

// 2. 开发/生产环境差异
// 开发: Vite + esbuild
// 生产: Rollup
// 可能出现开发正常、生产出问题的情况

// 3. 依赖预构建的边界情况
// 动态依赖可能无法被正确预构建
import(`./locale/${lang}.js`)  // 可能需要手动配置

// 4. 浏览器兼容性
// 需要 Chrome 87+, Firefox 78+, Safari 14+
// 不支持 IE11

三、构建工具全景图

3.1 主流构建工具分类

┌─────────────────────────────────────────────────────────────────┐
│                       构建工具全景图                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│   ┌─────────────┐  ┌─────────────┐  ┌─────────────┐             │
│   │   Webpack   │  │   Rollup    │  │   Parcel    │             │
│   │  (JS 实现)   │  │  (JS 实现)   │  │  (Rust SWC) │             │
│   │   2012年     │  │   2015年     │  │   2017年     │             │
│   │   大而全     │  │   库打包     │  │   零配置     │             │
│   └─────────────┘  └─────────────┘  └─────────────┘             │
│                                                                   │
│   ┌─────────────┐  ┌─────────────┐  ┌─────────────┐             │
│   │   esbuild   │  │    Vite     │  │     SWC     │             │
│   │  (Go 实现)   │  │  (esbuild)  │  │  (Rust 实现) │             │
│   │   2020年     │  │   2020年     │  │   2019年     │             │
│   │   极速编译   │  │  Dev + Prod │  │   Babel 替代 │             │
│   └─────────────┘  └─────────────┘  └─────────────┘             │
│                                                                   │
│   ┌─────────────┐  ┌─────────────┐  ┌─────────────┐             │
│   │  Turbopack  │  │   Rspack    │  │     Bun     │             │
│   │  (Rust 实现) │  │  (Rust 实现) │  │  (Zig 实现)  │             │
│   │   2022年     │  │   2023年     │  │   2022年     │             │
│   │  Next.js    │  │  Webpack 兼容│  │  全栈运行时  │             │
│   └─────────────┘  └─────────────┘  └─────────────┘             │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

3.2 各工具深度对比

Webpack - 成熟的全能选手

javascript
// 优势
// 1. 生态系统最完善,几乎任何需求都有解决方案
// 2. 高度可配置,能处理任何复杂场景
// 3. Code Splitting 能力强大
// 4. 稳定可靠,大厂背书

// 劣势
// 1. 配置复杂,学习曲线陡峭
// 2. 构建速度慢 (JS 单线程限制)
// 3. 开发体验不如新工具

// 适用场景
// - 大型企业应用
// - 需要复杂打包逻辑的项目
// - 对构建产物有精细控制需求

Rollup - 库打包专家

javascript
// 设计哲学: 专注 ES Modules,产出最小最干净的包

// 优势
// 1. Tree Shaking 效果最好 (基于 ESM 静态分析)
// 2. 输出格式灵活 (ESM/CJS/UMD/IIFE)
// 3. 产物可读性好,代码干净
// 4. 插件 API 简洁

// 劣势
// 1. 不适合大型应用 (Code Splitting 能力弱)
// 2. 对 CommonJS 支持需要插件
// 3. HMR 支持有限

// 适用场景
// - npm 库/组件库打包
// - 需要多格式输出
// - 对包体积有极致要求

// 配置示例
export default {
  input: 'src/index.ts',
  output: [
    { file: 'dist/index.esm.js', format: 'esm' },
    { file: 'dist/index.cjs.js', format: 'cjs' },
    { file: 'dist/index.umd.js', format: 'umd', name: 'MyLib' }
  ],
  external: ['react', 'react-dom'],  // 外部依赖不打包
  plugins: [
    typescript(),
    terser()
  ]
};

esbuild - 速度之王

javascript
// 设计哲学: 重写一切,追求极致速度

// 为什么用 Go 而不是 Rust?
// 作者 Evan Wallace: "Go 的编译速度更快,
// 开发效率高,且性能已经足够"

// 核心优势
// 1. 比 Webpack/Rollup 快 10-100x
// 2. 内置 TypeScript/JSX 支持
// 3. 并行处理,充分利用多核
// 4. 内存效率高

// 局限性
// 1. 不支持类型检查 (只做语法转换)
// 2. 插件能力有限
// 3. 不支持 HMR
// 4. Code Splitting 能力弱

// 主要用途
// - 开发时的快速编译
// - CI/CD 中的快速构建
// - 作为其他工具的底层 (Vite)

// 使用示例
import * as esbuild from 'esbuild';

await esbuild.build({
  entryPoints: ['src/index.tsx'],
  bundle: true,
  minify: true,
  sourcemap: true,
  target: ['chrome90', 'firefox88', 'safari14'],
  outfile: 'dist/bundle.js',
  loader: { '.png': 'dataurl' },
});

SWC - Babel 的 Rust 替代品

javascript
// 设计哲学: 用 Rust 重写 Babel,保持 API 兼容

// 优势
// 1. 比 Babel 快 20-70x (单线程), 4x (多线程)
// 2. 支持 TypeScript/JSX 转换
// 3. 支持压缩 (替代 Terser)
// 4. 可作为 Webpack/Rollup loader 使用

// 劣势
// 1. 插件用 Rust 写,门槛高
// 2. Babel 插件不能直接用
// 3. 某些 Babel 插件无对应实现

// 与 Babel 性能对比 (React 项目)
// Babel: 编译 1000 文件约 20s
// SWC: 编译 1000 文件约 0.5s

// 配置示例 (.swcrc)
{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": true
    },
    "transform": {
      "react": {
        "runtime": "automatic"
      }
    },
    "target": "es2020"
  },
  "minify": true
}

Turbopack - Vercel 的野心

javascript
// 设计哲学: Rust 重写 Webpack,增量编译

// 核心特性
// 1. 增量计算引擎 (Turbo Engine)
// 2. 函数级别缓存,极致复用
// 3. 原生支持 React Server Components

// 与 Vite 的架构差异
// Vite: 运行时按需编译,依赖浏览器 ESM
// Turbopack: 编译时增量计算,仍然打包

// 目前状态 (2024)
// - 仅支持 Next.js
// - 仍在 Beta 阶段
// - 功能还不完整

// 增量编译原理
/*
┌─────────────────────────────────────────┐
│  Turbo Engine 增量计算                    │
├─────────────────────────────────────────┤
│                                          │
│  修改 Button.tsx                          │
│       │                                   │
│       ▼                                   │
│  计算 Button.tsx 的 hash                  │
│       │                                   │
│       ▼                                   │
│  检查缓存: hash 变了吗?                    │
│       │                                   │
│   ┌───┴───┐                              │
│   │       │                               │
│   ▼       ▼                               │
│  变了   没变                               │
│   │       │                               │
│   ▼       ▼                               │
│ 重编译  直接用缓存                          │
│                                          │
└─────────────────────────────────────────┘
*/

Rspack - Webpack 兼容的 Rust 版本

javascript
// 设计哲学: Rust 重写 Webpack 核心,保持 API 兼容

// 核心优势
// 1. Webpack 配置几乎 100% 兼容
// 2. 大部分 Webpack 插件可直接使用
// 3. 比 Webpack 快 5-10x
// 4. 字节跳动背书,在大规模项目验证

// 迁移成本极低
// 只需改包名,大部分配置不用动

// 配置对比
// webpack.config.js → rspack.config.js
// const webpack = require('webpack')
// 改为
// const rspack = require('@rspack/core')

// 性能数据 (字节内部项目)
// Webpack: 冷启动 3 分钟
// Rspack: 冷启动 10 秒

// 适用场景
// - 存量 Webpack 项目想提速
// - 不想大改配置的迁移
// - 大型 Monorepo 项目

Parcel - 零配置理念

javascript
// 设计哲学: 零配置,开箱即用

// 优势
// 1. 真正的零配置,自动检测项目类型
// 2. 内置多线程编译 (用 Rust SWC)
// 3. 自动安装依赖
// 4. 对新手友好

// 劣势
// 1. 高度定制场景不灵活
// 2. 大型项目可能不够用
// 3. 生态相对小

// 使用示例
// 无需配置文件!
// npx parcel src/index.html

// 自动处理:
// - TypeScript
// - JSX
// - CSS/SCSS
// - 图片
// - ...

Bun - 全栈运行时

javascript
// 设计哲学: 一个工具解决所有问题

// Bun 是什么?
// 1. JavaScript 运行时 (替代 Node.js)
// 2. 包管理器 (替代 npm/yarn/pnpm)
// 3. 打包器 (替代 Webpack/Vite)
// 4. 测试运行器 (替代 Jest)

// 打包器能力
// - 内置 bundler,无需配置
// - 速度接近 esbuild
// - 原生支持 TypeScript/JSX

// 使用示例
// bun build ./src/index.tsx --outdir ./dist

// 目前状态
// - 还在快速迭代
// - 生态不完善
// - 生产环境用的还少

3.3 底层编译器对比

特性BabelSWCesbuild
实现语言JavaScriptRustGo
速度1x (基准)20-70x10-100x
插件系统极其丰富Rust 插件有限的 JS 插件
TypeScript通过插件原生支持原生支持(仅转换)
类型检查不支持不支持不支持
成熟度最成熟成熟成熟
定制能力最强中等

四、设计哲学对比

4.1 不同工具的核心理念

┌─────────────────────────────────────────────────────────────────┐
│                       设计哲学对比                               │
├──────────────┬──────────────────────────────────────────────────┤
│   Webpack    │  "Everything is a module"                        │
│              │  万物皆模块,极致的抽象能力                        │
├──────────────┼──────────────────────────────────────────────────┤
│   Rollup     │  "ES Modules First"                              │
│              │  拥抱标准,产出最纯净的代码                        │
├──────────────┼──────────────────────────────────────────────────┤
│   Vite       │  "Unbundled Development"                         │
│              │  开发时不打包,利用浏览器原生能力                   │
├──────────────┼──────────────────────────────────────────────────┤
│   esbuild    │  "Speed is a feature"                            │
│              │  速度本身就是功能,重写一切                        │
├──────────────┼──────────────────────────────────────────────────┤
│   Turbopack  │  "Incremental by design"                         │
│              │  增量计算,永不重复工作                            │
├──────────────┼──────────────────────────────────────────────────┤
│   Rspack     │  "Webpack Compatible, Rust Powered"              │
│              │  兼容生态,性能升级                                │
├──────────────┼──────────────────────────────────────────────────┤
│   Parcel     │  "Zero configuration"                            │
│              │  零配置,开箱即用                                  │
└──────────────┴──────────────────────────────────────────────────┘

4.2 性能优化的不同路径

javascript
// 路径 1: 语言级优化 (换语言重写)
// Go: esbuild
// Rust: SWC, Turbopack, Rspack, Parcel
// Zig: Bun
// 收益: 10-100x 性能提升

// 路径 2: 架构级优化 (改变工作方式)
// Vite: 开发时不打包
// Turbopack: 增量编译
// 收益: 启动时间从分钟级降到秒级

// 路径 3: 算法级优化 (更好的缓存)
// Webpack 5: 持久化缓存
// Turbopack: 函数级缓存
// 收益: 2-10x 二次构建提升

// 路径 4: 并行化
// Parcel: 多线程
// esbuild: 多核并行
// 收益: 线性提升 (接近核心数)

五、构建工具选型指南

5.1 决策树

                        ┌─────────────────┐
                        │ 项目类型是什么? │
                        └────────┬────────┘

            ┌────────────────────┼────────────────────┐
            ▼                    ▼                    ▼
      ┌───────────┐        ┌───────────┐        ┌───────────┐
      │  npm 库    │        │  应用项目  │        │  Monorepo │
      └─────┬─────┘        └─────┬─────┘        └─────┬─────┘
            │                    │                    │
            ▼                    │                    │
    ┌───────────────┐           │                    │
    │    Rollup     │           │                    │
    │ (+ esbuild)   │           │                    │
    └───────────────┘           │                    │
                                │                    │
            ┌───────────────────┤                    │
            ▼                   ▼                    │
      ┌───────────┐       ┌───────────┐             │
      │  新项目?  │       │  存量项目? │             │
      └─────┬─────┘       └─────┬─────┘             │
            │                   │                   │
    ┌───────┴───────┐   ┌───────┴───────┐          │
    ▼               ▼   ▼               ▼          │
┌───────┐       ┌───────┐           ┌───────┐      │
│ Vite  │       │Next.js│           │Webpack│      │
│       │       │(Turbo)│           │ 继续用 │      │
└───────┘       └───────┘           └───┬───┘      │
                                        │          │
                              想提速? ──┘          │
                                  │                │
                                  ▼                │
                           ┌───────────┐          │
                           │  Rspack   │←─────────┘
                           └───────────┘

5.2 场景化推荐

场景 1: 全新 React/Vue 项目

javascript
// 推荐: Vite
// 理由:
// 1. 开发体验最好,启动秒级
// 2. 配置简单,约定优于配置
// 3. 社区活跃,插件丰富
// 4. 官方推荐 (Vue, Svelte)

// 创建项目
npm create vite@latest my-app -- --template react-ts

场景 2: Next.js 应用

javascript
// 推荐: 使用 Next.js 内置 (Turbopack)
// 理由:
// 1. 深度集成,无需配置
// 2. RSC 支持最好
// 3. 性能持续优化

// next.config.js
module.exports = {
  experimental: {
    turbo: {} // 启用 Turbopack
  }
};

场景 3: 组件库/工具库

javascript
// 推荐: Rollup (或 Vite library mode)
// 理由:
// 1. 产物最干净
// 2. 多格式输出
// 3. Tree Shaking 最好

// 或者用 tsup (基于 esbuild)
// 极简配置
// tsup src/index.ts --format cjs,esm --dts

场景 4: 存量大型 Webpack 项目

javascript
// 推荐: Rspack
// 理由:
// 1. 配置兼容,迁移成本低
// 2. 性能提升 5-10x
// 3. 字节大规模验证

// 迁移步骤
// 1. npm install @rspack/core @rspack/cli
// 2. 重命名 webpack.config.js → rspack.config.js
// 3. 将 require('webpack') 改为 require('@rspack/core')
// 4. 运行测试

场景 5: 大型 Monorepo

javascript
// 推荐: Rspack 或 Turbopack
// 考虑因素:
// 1. 增量构建能力
// 2. 缓存复用
// 3. 并行构建

// 配合工具
// - Turborepo (任务编排)
// - Nx (构建系统)
// - pnpm workspace (包管理)

5.3 选型评估矩阵

评估维度WebpackViteRollupesbuildRspackTurbopack
开发启动速度★★☆★★★★★★★★★★★★★★★★★★★★★★
生产构建速度★★★★★★★★★★★★★★★★★★★★★★★
配置简单度★★☆★★★★★★★★★★★★★★★★★★
生态丰富度★★★★★★★★★★★★★★★★★★★★★
功能完整度★★★★★★★★★★★★★★★★★★★★★★
大型项目适用★★★★★★★★★★★☆★★☆★★★★★★★★★
库打包★★★★★★★★★★★★★★★★★★★☆
学习曲线陡峭平缓中等平缓中等平缓

5.4 避坑指南

javascript
// 坑 1: Vite 开发/生产环境差异
// 现象: 开发正常,生产报错
// 原因: 开发用 esbuild,生产用 Rollup
// 解决: 定期 build 测试,注意 CommonJS 依赖

// 坑 2: esbuild 不做类型检查
// 现象: 类型错误没报,运行时才崩
// 原因: esbuild 只转换语法
// 解决: CI 中加 tsc --noEmit

// 坑 3: Rspack 插件兼容性
// 现象: 某些 Webpack 插件不工作
// 原因: 不是所有钩子都实现了
// 解决: 查看兼容性文档,用内置替代

// 坑 4: Tree Shaking 不生效
// 现象: 包体积没减小
// 原因: CommonJS 模块无法 Tree Shake
// 解决: 用 ESM 版本的包 (xxx-es)

// 坑 5: 缓存导致的诡异问题
// 现象: 改了代码没生效
// 原因: 各级缓存没清除
// 解决: 清除 node_modules/.cache, .vite, 浏览器缓存

六、Webpack 深入原理

6.1 完整编译流程

javascript
/*
┌─────────────────────────────────────────────────────────────────┐
│                    Webpack 编译流程详解                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  1. 初始化阶段                                                    │
│     │                                                             │
│     ├─ 读取配置文件 (webpack.config.js)                           │
│     ├─ 合并 CLI 参数                                              │
│     ├─ 创建 Compiler 对象                                         │
│     └─ 加载所有配置的 Plugin                                      │
│                                                                   │
│  2. 编译阶段 (make)                                               │
│     │                                                             │
│     ├─ 从 entry 开始                                              │
│     ├─ 调用 Loader 转换模块                                       │
│     ├─ 使用 acorn 解析为 AST                                      │
│     ├─ 遍历 AST 找到 require/import                               │
│     ├─ 递归处理依赖模块                                           │
│     └─ 构建模块依赖图 (ModuleGraph)                               │
│                                                                   │
│  3. 生成阶段 (seal)                                               │
│     │                                                             │
│     ├─ 根据依赖图生成 Chunk                                       │
│     ├─ 对 Chunk 进行优化                                          │
│     │   ├─ Tree Shaking                                          │
│     │   ├─ Scope Hoisting                                        │
│     │   └─ Code Splitting                                        │
│     └─ 生成最终代码                                               │
│                                                                   │
│  4. 输出阶段 (emit)                                               │
│     │                                                             │
│     ├─ 生成文件内容                                               │
│     ├─ 写入文件系统                                               │
│     └─ 触发 done 钩子                                             │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘
*/

6.2 Tapable 钩子系统

javascript
// Webpack 的插件系统基于 Tapable
// Tapable 提供各种钩子类型

const { 
  SyncHook,           // 同步串行
  SyncBailHook,       // 同步串行,返回非 undefined 时终止
  SyncWaterfallHook,  // 同步串行,上一个返回值传给下一个
  AsyncParallelHook,  // 异步并行
  AsyncSeriesHook     // 异步串行
} = require('tapable');

// Compiler 核心钩子
class Compiler {
  constructor() {
    this.hooks = {
      // 编译开始
      run: new AsyncSeriesHook(['compiler']),
      // 编译完成
      done: new AsyncSeriesHook(['stats']),
      // 编译失败
      failed: new SyncHook(['error']),
      // 输出前
      emit: new AsyncSeriesHook(['compilation']),
      // 输出后
      afterEmit: new AsyncSeriesHook(['compilation']),
    };
  }
}

// 插件注册方式
class MyPlugin {
  apply(compiler) {
    // 同步钩子用 tap
    compiler.hooks.done.tap('MyPlugin', (stats) => {
      console.log('编译完成');
    });
    
    // 异步钩子用 tapAsync 或 tapPromise
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
      setTimeout(() => {
        console.log('异步操作完成');
        callback();
      }, 1000);
    });
  }
}

6.3 Loader 执行机制

javascript
// Loader 链式调用 (从右到左,从下到上)
module: {
  rules: [{
    test: /\.scss$/,
    use: [
      'style-loader',    // 3. 将 CSS 注入 DOM
      'css-loader',      // 2. 处理 CSS 中的 @import 和 url()
      'sass-loader'      // 1. 将 SCSS 编译为 CSS
    ]
  }]
}

// Loader 执行流程
/*
scss 源码

    ▼ sass-loader (normal)

css 代码

    ▼ css-loader (normal)

JS 模块代码 (包含 CSS 依赖)

    ▼ style-loader (normal)

最终 JS 代码 (运行时注入样式)
*/

// Pitch 阶段 (从左到右)
// 可以跳过后续 loader
module.exports = function(source) {
  return transform(source);
};

module.exports.pitch = function(remainingRequest) {
  // 如果 pitch 返回值,跳过后续 loader
  if (shouldSkip) {
    return 'module.exports = "cached content"';
  }
};

6.4 手写简易 Webpack

javascript
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');

// 1. 解析单个模块
function parseModule(filePath) {
  const content = fs.readFileSync(filePath, 'utf-8');
  
  // 解析为 AST
  const ast = parser.parse(content, {
    sourceType: 'module'
  });
  
  // 收集依赖
  const dependencies = [];
  traverse(ast, {
    ImportDeclaration({ node }) {
      dependencies.push(node.source.value);
    }
  });
  
  // 转换代码
  const { code } = babel.transformFromAstSync(ast, null, {
    presets: ['@babel/preset-env']
  });
  
  return { filePath, dependencies, code };
}

// 2. 构建依赖图
function buildDependencyGraph(entry) {
  const entryModule = parseModule(entry);
  const modules = [entryModule];
  const moduleMap = new Map();
  
  // BFS 遍历所有依赖
  for (const module of modules) {
    moduleMap.set(module.filePath, module);
    
    const dirname = path.dirname(module.filePath);
    module.dependencies.forEach(dep => {
      const depPath = path.resolve(dirname, dep);
      if (!moduleMap.has(depPath)) {
        modules.push(parseModule(depPath));
      }
    });
  }
  
  return modules;
}

// 3. 生成 Bundle
function generateBundle(modules, entry) {
  let modulesCode = '';
  
  modules.forEach(module => {
    modulesCode += `
      "${module.filePath}": function(require, module, exports) {
        ${module.code}
      },
    `;
  });
  
  return `
    (function(modules) {
      const cache = {};
      
      function require(moduleId) {
        if (cache[moduleId]) {
          return cache[moduleId].exports;
        }
        
        const module = cache[moduleId] = { exports: {} };
        modules[moduleId](require, module, module.exports);
        return module.exports;
      }
      
      require("${entry}");
    })({${modulesCode}});
  `;
}

// 使用
const modules = buildDependencyGraph('./src/index.js');
const bundle = generateBundle(modules, './src/index.js');
fs.writeFileSync('./dist/bundle.js', bundle);

七、Vite 深入原理

7.1 预构建详解

javascript
// 为什么需要预构建?

// 问题 1: CommonJS 不能直接在浏览器运行
// lodash 是 CommonJS,需要转换为 ESM
import _ from 'lodash'  // ❌ 浏览器不认识 CommonJS

// 问题 2: 依赖有大量内部模块
// lodash-es 有 600+ 个内部模块
// 每个模块一个 HTTP 请求,太慢了
import debounce from 'lodash-es/debounce'
// 实际会触发几十个请求 (debounce 的依赖)

// 预构建解决方案
// 1. 用 esbuild 将依赖打包为单个 ESM 文件
// 2. 放到 node_modules/.vite/deps 目录
// 3. 重写 import 路径指向预构建产物

// 预构建流程
/*
启动 Vite


┌─────────────────────────────┐
│ 扫描项目源码中的 bare import │
│ import xxx from 'lodash'    │
└─────────────────────────────┘


┌─────────────────────────────┐
│ 调用 esbuild 打包这些依赖    │
│ 输出到 .vite/deps           │
└─────────────────────────────┘


┌─────────────────────────────┐
│ 生成依赖元数据               │
│ .vite/deps/_metadata.json   │
└─────────────────────────────┘
*/

// 手动控制预构建
export default {
  optimizeDeps: {
    // 强制预构建 (解决动态 import 检测不到的问题)
    include: ['esm-dep > cjs-dep'],
    
    // 排除预构建 (某些包需要在运行时处理)
    exclude: ['my-linked-package'],
    
    // 强制重新预构建
    force: true,
    
    // esbuild 选项
    esbuildOptions: {
      plugins: []
    }
  }
}

7.2 模块热更新 (HMR) 原理

javascript
/*
┌─────────────────────────────────────────────────────────────────┐
│                      Vite HMR 架构                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│   浏览器                              Vite Server                 │
│  ┌────────────┐                     ┌────────────┐               │
│  │            │   WebSocket 连接     │            │               │
│  │  HMR       │◄───────────────────►│   Watcher  │               │
│  │  Client    │                     │  (chokidar)│               │
│  │            │                     │            │               │
│  └────────────┘                     └────────────┘               │
│       │                                   │                       │
│       │  收到更新消息                      │  文件变化             │
│       │  { type: 'update',               │                       │
│       │    updates: [...] }              │                       │
│       ▼                                   │                       │
│  ┌────────────┐                          │                       │
│  │ import()   │                          │                       │
│  │ 重新请求   │◄─────────────────────────┘                       │
│  │ 变化模块   │                                                   │
│  └────────────┘                                                   │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘
*/

// HMR 边界
// Vue/React 组件天然是 HMR 边界
// 普通 JS 模块需要手动处理

// 手动 HMR API
if (import.meta.hot) {
  // 接受自身更新
  import.meta.hot.accept((newModule) => {
    // newModule 是更新后的模块
    // 手动更新状态
  });
  
  // 接受依赖更新
  import.meta.hot.accept('./dep.js', (newDep) => {
    // dep.js 更新时触发
  });
  
  // 清理副作用
  import.meta.hot.dispose((data) => {
    // 模块更新前调用
    clearInterval(timer);
    data.savedValue = someValue;  // 传递给新模块
  });
  
  // 无法热更新时回退到整页刷新
  import.meta.hot.decline();
  
  // 让父模块处理更新
  import.meta.hot.invalidate();
}

7.3 插件系统

javascript
// Vite 插件 = Rollup 插件 + Vite 特有钩子

// Vite 特有钩子
const myVitePlugin = () => ({
  name: 'my-vite-plugin',
  
  // Vite 特有: 配置解析
  config(config, { command, mode }) {
    // 返回的配置会被合并
    return {
      resolve: {
        alias: { '@': '/src' }
      }
    };
  },
  
  // Vite 特有: 配置解析完成
  configResolved(resolvedConfig) {
    // 存储最终配置
  },
  
  // Vite 特有: 配置开发服务器
  configureServer(server) {
    // 添加自定义中间件
    server.middlewares.use((req, res, next) => {
      if (req.url === '/custom') {
        res.end('custom response');
      } else {
        next();
      }
    });
  },
  
  // Vite 特有: 转换 index.html
  transformIndexHtml(html) {
    return html.replace(
      /<title>(.*?)<\/title>/,
      '<title>Modified Title</title>'
    );
  },
  
  // Vite 特有: HMR 更新处理
  handleHotUpdate({ file, server }) {
    // 自定义 HMR 行为
    if (file.endsWith('.custom')) {
      server.ws.send({
        type: 'custom',
        event: 'custom-update',
        data: { file }
      });
      return [];  // 阻止默认 HMR
    }
  },
  
  // Rollup 钩子 (构建时)
  buildStart() {},
  resolveId(source) {},
  load(id) {},
  transform(code, id) {},
  buildEnd() {},
});

// 插件执行顺序控制
export default {
  plugins: [
    {
      ...myPlugin(),
      enforce: 'pre'  // 在核心插件之前
    },
    corePlugin(),     // 默认顺序
    {
      ...postPlugin(),
      enforce: 'post' // 在核心插件之后
    }
  ]
};

八、高频面试题深度解析

Q1: Webpack 和 Vite 的本质区别是什么?

javascript
/*
核心区别: 开发时的模块处理方式

Webpack 模式:
- 启动前: 分析并打包所有模块
- 启动后: 提供打包后的 bundle
- 改变代码: 重新打包受影响的 chunk
- 本质: 传统打包器思维

Vite 模式:
- 启动前: 仅预构建 node_modules
- 启动后: 按需编译被请求的模块
- 改变代码: 仅重新编译该模块
- 本质: 利用浏览器原生 ESM

根本原因:
浏览器直到 2018 年才广泛支持 ESM
Webpack 诞生于 2012 年,必须自己实现模块系统
Vite 诞生于 2020 年,可以依赖浏览器原生能力
*/

Q2: 为什么 esbuild/SWC 比 Babel/Webpack 快那么多?

javascript
/*
1. 语言层面
   - Babel/Webpack: JavaScript (解释执行)
   - esbuild: Go (编译为原生代码)
   - SWC: Rust (编译为原生代码)
   
2. 并行处理
   - Babel: 单线程
   - esbuild: 利用 Go 的 goroutine 多核并行
   - SWC: 利用 Rust 的 rayon 并行库
   
3. 内存效率
   - JS 工具链: 大量 AST 对象创建和 GC
   - 原生工具: 更直接的内存控制
   
4. 设计取舍
   - Babel: 追求极致的可扩展性,大量运行时判断
   - esbuild: 功能够用就行,极致

前端面试知识库