构建工具
发表于:2026-07-29
字数统计:7500 字
预计阅读26分钟
涵盖 Webpack vs Vite 对比、ES Module 三阶段原理、Vite 冷启动与 HMR 原理、Vite 插件系统、Tree Shaking、Source Map。
一、Webpack vs Vite 速览
| 维度 | Webpack | Vite |
|---|---|---|
| 开发模式 | 打包后启动(冷启动慢) | 浏览器原生 ESM(启动快) |
| HMR | 慢(全量打包) | 快(按需编译) |
| 生产模式 | 打包(Tree Shaking、Code Splitting) | 用 Rollup 打包 |
| 配置复杂度 | 高 | 低 |
| 生态 | 成熟、插件多 | 新但增长快 |
| 大项目冷启动 | 慢(几十秒~几分钟) | 快(毫秒级) |
| HMR 速度 | 受项目规模影响 | 与项目规模几乎无关 |
| 产物 | 兼容性更好 | 现代浏览器(生产用 Rollup 兼容性好) |
二、Webpack 核心概念
1. 五大核心概念
Plain
1. Entry(入口):打包的起点,从哪个文件开始构建依赖图
2. Output(输出):打包产物的输出位置和文件名
3. Loader(加载器):把非 JS 文件(CSS、图片、TS 等)转换为 JS 模块
4. Plugin(插件):扩展 Webpack 能力(压缩、HTML 生成、环境变量注入等)
5. Mode(模式):development / production,决定默认优化2. 基础配置
JavaScript
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js',
vendor: './src/vendor.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js',
publicPath: '/',
clean: true
},
resolve: {
extensions: ['.js', '.ts', '.vue', '.json'],
alias: {
'@': path.resolve(__dirname, 'src')
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.(png|svg|jpg|gif)$/,
type: 'asset/resource'
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({ template: './index.html' }),
new MiniCssExtractPlugin({ filename: '[name].[contenthash].css' })
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all'
}
}
},
runtimeChunk: 'single'
},
devServer: {
hot: true,
port: 3000,
historyApiFallback: true // SPA
}
}3. 构建流程
Plain
Webpack 构建流程:
1. 初始化参数:从配置文件 + CLI 参数 + 默认值合并
2. 开始编译:用上一步得到的参数初始化 Compiler 对象
3. 确定入口:从 entry 开始找到所有依赖模块
4. 编译模块:从入口开始调用 Loader 编译每个模块
5. 完成编译:得到编译后的模块和依赖关系
6. 输出资源:根据入口和模块依赖,组装成多个 Chunk
7. 输出完成:根据 output 配置写入文件系统4. 常见 Loader 与 Plugin
Plain
常用 Loader:
- babel-loader ES6+ / TS → ES5
- ts-loader TypeScript
- vue-loader .vue 文件
- css-loader CSS → JS 模块
- style-loader 把 CSS 注入 DOM
- postcss-loader PostCSS 处理
- url-loader 小文件转 base64
- file-loader 文件输出
- raw-loader 加载文件为字符串
常用 Plugin:
- HtmlWebpackPlugin 生成 HTML
- MiniCssExtractPlugin 提取 CSS 到独立文件
- CleanWebpackPlugin 清理 dist
- CopyWebpackPlugin 复制静态文件
- DefinePlugin 定义全局变量
- TerserPlugin JS 压缩
- CompressionPlugin Gzip 压缩三、Vite 核心原理
1. 为什么 Vite 快?
Plain
传统打包器(Webpack)的问题:
- 开发模式下也要打包所有模块
- 项目越大,启动越慢(几百 MB 的项目要打包几十秒~几分钟)
- HMR 也要重新打包
Vite 的解决方案:
- 开发模式:利用浏览器原生 ES Module,按需编译
- 不打包,启动极快
- HMR 精准更新(只更新改动的文件)
- 生产模式:用 Rollup 打包(产物更紧凑)2. ES Module 的三阶段加载
Plain
Vite 的核心基于浏览器的原生 ESM。当浏览器遇到 import 时,会向服务器发请求:
第一阶段(预构建 / Dep Pre-Bundling):
- 把 CommonJS / UMD 转换为 ESM
- 把多个模块合并(esbuild,速度极快)
- 优化依赖缓存
第二阶段(ESM 按需加载):
- 浏览器请求入口 HTML
- 浏览器解析到 import 时,发请求到 Vite Dev Server
- Vite 把请求的文件编译后返回(按需)
- 浏览器继续解析 import,递归加载
第三阶段(HMR):
- 文件修改后,Vite 通过 WebSocket 推送更新
- 浏览器按需重新加载变化的部分
- 无需全量刷新3. Vite 配置
JavaScript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: p => p.replace(/^\/api/, '')
}
}
},
build: {
outDir: 'dist',
sourcemap: false,
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia']
}
}
}
}
})4. Vite 启动流程
Plain
Vite 启动:
1. 启动 Koa 服务器(Dev Server)
2. 拦截浏览器请求
3. 对于 .vue、.ts、.scss 等文件,按需编译
4. 编译后通过 HTTP 返回给浏览器
5. 浏览器原生 ESM 解析
首次访问 index.html:
- 浏览器解析 HTML,遇到 <script type="module" src="...">
- 浏览器请求 main.js
- Vite 返回编译后的 main.js(包含 import)
- 浏览器解析 import,继续请求依赖
- 每个依赖都是单独的请求
- Vite 按需编译每个文件
相比 Webpack:Vite 不需要先打包所有依赖5. Vite 预构建(Dep Pre-Bundling)
Plain
为什么需要预构建:
1. CommonJS / UMD 兼容性:很多 npm 包是 CJS,浏览器不支持
2. 减少请求数:lodash 有 600+ 模块,每个请求太慢
3. 缓存:预构建结果缓存在 node_modules/.vite
用什么预构建:
- esbuild(Go 编写,速度极快,比 Babel + Webpack 快 10-100 倍)
触发时机:
- 启动时
- 修改 package.json 后
- 手动:vite --force6. Vite HMR 原理
Plain
Vite HMR 的工作流程:
1. Vite 通过 WebSocket 与浏览器建立连接
2. 文件修改后,Vite 监听器触发
3. Vite 通过 WebSocket 推送更新消息(仅推送变动的模块)
4. 浏览器收到消息,根据模块类型执行:
- Vue 组件:调用 component.hot.accept 替换组件
- 普通 JS:执行 hot.accept 回调
- CSS:直接替换 style 标签
5. 状态保留:组件更新而非重新加载,状态不丢失
Vite HMR 的优势:
- 只推送变化的模块(不是整个页面)
- ESM 天然支持,无需打包
- 与项目规模无关7. Vite 插件系统
JavaScript
// 插件本质:带 name 的对象,包含多个生命周期钩子
export default function myPlugin() {
return {
name: 'my-plugin',
// Vite 特有钩子
configResolved(config) { /* 配置解析后 */ },
configureServer(server) { /* 配置 Dev Server */ },
transformIndexHtml(html) { /* 转换 HTML */ },
handleHotUpdate(ctx) { /* 处理 HMR */ },
// 兼容 Rollup 钩子
resolveId(id) { /* 解析模块 ID */ },
load(id) { /* 加载模块 */ },
transform(code, id) { /* 转换代码 */ }
}
}JavaScript
// 示例:自动注入版本号
export default function injectVersion() {
return {
name: 'inject-version',
transformIndexHtml(html) {
return html.replace(
'<body>',
`<body>
<script>window.__APP_VERSION__ = "${Date.now()}"</script>`
)
}
}
}四、Tree Shaking
1. 原理
Plain
Tree Shaking:移除 JavaScript 中未使用的代码(dead code elimination)
依赖:
1. ES Module 静态结构(import/export 不能动态)
2. 副作用分析
3. 模块打包器支持(Webpack / Rollup / esbuild)
注意:CommonJS 不支持(require 是动态的)2. 配置
JavaScript
// Webpack
module.exports = {
mode: 'production', // 自动启用
optimization: {
usedExports: true, // 标记未使用
sideEffects: false // 假设所有模块无副作用
}
}
// package.json
{
"sideEffects": false, // 整个项目无副作用
"sideEffects": ["*.css"] // 除 CSS 外无副作用
}JavaScript
// ❌ 有副作用的代码(会被保留)
let count = 0
export function increment() {
count++
return count
}
// ✅ 纯函数(可以被 Tree Shaking)
export function add(a, b) {
return a + b
}五、Source Map
1. 概念
Plain
Source Map:把编译后的代码映射回源代码,方便调试
格式:main.js.map
内容:
- version:Source Map 版本
- sources:源文件列表
- names:变量名映射
- mappings:位置映射(VLQ 编码)
- file:生成的 JS 文件名
- sourcesContent:源文件内容(可选)2. 配置
JavaScript
// Webpack
module.exports = {
devtool: 'source-map' // 开发环境推荐
// 'eval-source-map' // 开发环境,速度更快
// 'cheap-module-source-map' // 生产环境(只映射行号)
// production 不用 devtool 或用 hidden-source-map
}
// Vite
export default defineConfig({
build: {
sourcemap: false, // 生产环境关闭(或 'hidden')
}
})3. Source Map 类型对比
Plain
eval eval 包装,最快,无 Source Map
cheap-source-map 行级映射,无列,无 loader 映射
source-map 完整 Source Map,最慢
eval-source-map eval + Source Map
inline-source-map Source Map 内联到文件
hidden-source-map 不在文件尾添加注释,但生成 .map六、其他构建工具对比
Plain
Rollup:
- 适合库(Vue、React 都用)
- 产物简洁
- 适合 ES Module
esbuild:
- Go 编写,速度极快
- 用作 Vite 预构建
- 不支持完整 Source Map(开发体验略差)
Parcel:
- 零配置
- 自动识别依赖
- 适合小型项目
Turbopack:
- Rust 编写,Webpack 作者开发
- Next.js 13+ 默认
- 速度快
SWC:
- Rust 编写
- 替代 Babel
- 与 Next.js、Turbopack 集成七、面试高频问答
Q1: Webpack 和 Vite 的区别?
答:
| 维度 | Webpack | Vite |
|---|---|---|
| 原理 | 打包所有模块 | 利用浏览器原生 ESM |
| 启动 | 慢(先打包) | 快(按需加载) |
| HMR | 全量更新 | 精准更新 |
| 生产 | Webpack 打包 | Rollup 打包 |
| 适用 | 大型项目、复杂需求 | 现代项目、新项目 |
核心区别:Vite 利用了现代浏览器原生支持 ESM 这一特性,跳过了开发时的打包过程,从而实现快速冷启动和精准 HMR。
Q2: Vite 为什么快?
答:
- 启动快:不打包所有模块,直接用浏览器 ESM 按需加载
- 预构建依赖:用 esbuild 把 CJS 转 ESM + 合并依赖(毫秒级)
- HMR 快:只更新修改的模块,不刷新整个页面
- 按需编译:浏览器请求哪个文件,Vite 才编译哪个
Q3: 什么是 Tree Shaking?依赖什么?
答:Tree Shaking 是移除 JavaScript 中未使用代码的优化技术。
依赖条件:
- ES Module 静态结构:
import/export必须在编译时确定 - 副作用分析:在 package.json 中声明
"sideEffects": false - 打包器支持:Webpack / Rollup / esbuild
注意:CommonJS 不支持 Tree Shaking(require 是动态的)。
Q4: Vite 的预构建是干什么的?
答:Vite 启动时会用 esbuild 把依赖包预构建成 ESM 格式,主要解决:
- CommonJS 兼容:把 CJS / UMD 转为 ESM
- 减少请求数:把 lodash 这种多模块包合并成单文件
- 缓存优化:缓存到
node_modules/.vite,二次启动极快