构建工具的本质
构建工具是前端工程的编译器,将开发环境的高级语法转换为生产环境的优化代码。
构建流程
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Source │ → │ Parse │ → │ Transform │ → │ Generate │
│ Code │ │ AST │ │ (Babel) │ │ Output │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
.tsx AST 生成 SWC/esbuild .js bundle
.ts 重写 Tree-shaking .css bundle
.scss 依赖分析 压缩混淆 .asset
主流构建工具对比
| 特性 | Webpack | Vite | Rollup | esbuild |
|---|---|---|---|---|
| 定位 | 通用打包 | 新一代构建 | 库打包 | Go 实现 |
| 速度 | 慢 | 快 | 中等 | 极快 |
| 生态 | 丰富 | 崛起中 | 一般 | 发展中 |
| 学习曲线 | 陡峭 | 平缓 | 平缓 | 平缓 |
| 生产优化 | 优秀 | 优秀 | 优秀 | 一般 |
| HMR | 中等 | 极快 | 无 | 无 |
Webpack 深入理解
核心概念
┌────────────────────────────────────────────────────────────┐
│ Webpack 核心概念 │
├────────────────────────────────────────────────────────────┤
│ │
│ Entry (入口) Output (输出) │
│ ┌─────────┐ ┌─────────┐ │
│ │ entry │ ────────→ │ output │ │
│ │ './src/ │ │ dist/ │ │
│ │ index' │ │ bundle │ │
│ └─────────┘ └─────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Loader │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ babel │ │ css │ │ file │ │ │
│ │ │ loader │ │ loader │ │ loader │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Plugin │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Html │ │ Mini │ │ Define │ │ │
│ │ │ Plugin │ │Css │ │ Plugin │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘
Webpack 配置详解
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
// 入口配置
entry: {
main: './src/index.js',
vendor: './src/vendor.js'
},
// 输出配置
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction
? '[name].[contenthash:8].js'
: '[name].js',
chunkFilename: isProduction
? '[name].[contenthash:8].chunk.js'
: '[name].chunk.js',
clean: true, // 构建前清理 dist
publicPath: '/', // 公共路径
},
// 解析配置
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils')
},
modules: [
path.resolve(__dirname, 'src'),
'node_modules'
]
},
// Loader 配置
module: {
rules: [
// JavaScript/TypeScript
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false
}
}
]
},
// Vue
{
test: /\.vue$/,
loader: 'vue-loader'
},
// CSS
{
test: /\.css$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1
}
},
'postcss-loader' // Autoprefixer
]
},
// SCSS
{
test: /\.scss$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
'sass-loader'
]
},
// 图片资源
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 8KB 以下转 base64
}
},
generator: {
filename: 'images/[name].[hash:8][ext]'
}
},
// 字体资源
{
test: /\.(woff2?|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[hash:8][ext]'
}
},
// CSV/XML/JSON
{
test: /\.(csv|xml|json)$/,
type: 'json'
}
]
},
// Plugin 配置
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html',
inject: 'body',
minify: isProduction ? {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
} : false
}),
...(isProduction ? [
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css',
chunkFilename: '[name].[contenthash:8].chunk.css'
})
] : [])
],
// 优化配置
optimization: {
minimize: isProduction,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: isProduction, // 生产环境移除 console
drop_debugger: true
},
output: {
comments: false
}
},
extractComments: false
})
],
// 代码分割
splitChunks: {
chunks: 'all',
cacheGroups: {
// Node_modules 单独打包
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
// 公共模块抽取
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
},
// 按需加载的模块
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
},
// 运行时 chunk
runtimeChunk: 'single',
// 模块 ID 稳定化
moduleIds: 'deterministic'
},
// 开发服务器
devServer: {
static: './dist',
hot: true,
port: 3000,
open: true,
compress: true,
historyApiFallback: true,
// 代理配置
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
},
// 客户端配置
client: {
overlay: {
errors: true,
warnings: false
}
}
},
// Source Map
devtool: isProduction
? 'source-map' // 生产用 source-map
: 'eval-cheap-module-source-map', // 开发用快速 source-map
// 性能提示
performance: {
hints: isProduction ? 'warning' : false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
},
// 缓存配置
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(__dirname, '.webpack-cache')
}
};
};
Vite 深入理解
Vite 原理
Vite 利用浏览器原生 ES Modules 实现开发服务器,实现真正的按需编译。
┌─────────────────────────────────────────────────────────────┐
│ Vite 开发模式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Browser Vite Dev Server │
│ ┌─────────┐ ┌─────────────┐ │
│ │ index │ ────────────→ │ Transform │ │
│ │ .html │ │ (on-demand) │ │
│ └────┬────┘ └──────┬──────┘ │
│ │ │ │
│ │ GET /src/main.ts │ │
│ ├──────────────────────────┤ │
│ │ │ │
│ │ ┌───────────────┐ │ │
│ │ │ <script type │ │ │
│ │ │ ="module" │ │ │
│ │ │ src=" │ ←────┼──── 拦截,按需编译 │
│ │ │ /src/main │ │ │
│ │ │ .ts" │ │ │
│ │ └───────────────┘ │ │
│ │ │ │
│ │ ┌───────────────┐ │ │
│ │ │ import './ │ │ │
│ │ │ App.vue' │ ←────┼──── 编译 .vue 文件 │
│ │ └───────────────┘ │ │
│ │ │ │
│ └──────────────────────────┘ │
│ │
│ 只有访问到的模块才会被编译,未访问的代码不会编译 │
│ │
└─────────────────────────────────────────────────────────────┘
Vite 配置
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import vue from '@vitejs/plugin-vue';
import legacy from '@vitejs/plugin-legacy';
import { viteHybridPlugin } from 'vite-plugin-hybrid'; // 同时支持 CJS 和 ESM
import path from 'path';
export default defineConfig(({ mode }) => {
const isProduction = mode === 'production';
return {
// 基础路径
base: isProduction ? '/my-app/' : '/',
// 解析配置
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils')
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
// 插件
plugins: [
vue({
script: {
defineModel: true,
propsDestructure: true
}
}),
react({
// Fast Refresh
fastRefresh: true,
// JSX 运行时
jsxRuntime: 'automatic'
}),
// 兼容性
legacy({
targets: ['defaults', 'not IE 11'],
additionalLegacyPolyfills: ['regenerator-runtime/runtime']
})
],
// CSS 配置
css: {
// CSS Modules
modules: {
localsConvention: 'camelCase',
generateScopedName: isProduction
? '[hash:base64:8]'
: '[name]__[local]__[hash:base64:5]'
},
// PostCSS
postcss: './postcss.config.js',
// 预处理器
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`,
api: 'modern-compiler'
}
}
},
// 构建配置
build: {
target: 'esnext',
outDir: 'dist',
assetsDir: 'assets',
sourcemap: isProduction ? 'hidden' : 'inline',
// Rollup 选项
rollupOptions: {
input: {
main: path.resolve(__dirname, 'index.html'),
nested: path.resolve(__dirname, 'nested.html')
},
output: {
chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash][extname]',
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'axios']
},
// 拆包配置
manualImports: isProduction ? {
lodash: ['chunklodash'],
react: ['react', 'react-dom']
} : undefined
}
},
// 压缩
minify: isProduction ? 'terser' : 'esbuild',
terserOptions: {
compress: {
drop_console: true
}
},
// 分块大小警告
chunkSizeWarningLimit: 500
},
// 开发服务器
server: {
port: 3000,
host: true,
open: true,
cors: true,
// 代理
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, '')
}
},
// HMR
hmr: {
overlay: true
},
// 预热
warmup: {
clientFiles: ['./src/main.tsx'],
ssrFiles: ['./src/entry-server.tsx']
}
},
// 预览服务器
preview: {
port: 4173
},
// 依赖优化
optimizeDeps: {
include: ['react', 'react-dom', 'lodash', 'axios']
},
// 环境变量
envDir: './env',
envPrefix: ['VITE_', 'PUBLIC_']
};
});
Rollup 深入理解
Rollup 专注于库打包,输出干净、高效的 ESM 模块。
Rollup 配置
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';
import typescript from '@rollup/plugin-typescript';
import babel from '@rollup/plugin-babel';
import { visualizer } from 'rollup-plugin-visualizer';
export default {
input: 'src/index.ts',
output: {
file: 'dist/bundle.esm.js',
format: 'esm',
sourcemap: true,
exports: 'named'
},
plugins: [
// TypeScript
typescript({
tsconfig: './tsconfig.json',
sourceMap: true
}),
// 解析 node_modules
resolve({
browser: true,
preferBuiltins: false
}),
// 转换 CommonJS
commonjs(),
// Babel
babel({
babelHelpers: 'bundled',
extensions: ['.js', '.ts']
}),
// 压缩(生产)
terser({
compress: {
pure: ['console.log']
}
}),
// 可视化分析
visualizer({
filename: 'stats.html',
open: true
})
],
// 外部依赖
external: ['react', 'react-dom'],
// 警告处理
onwarn(warning, warn) {
// 跳过某些警告
if (warning.code === 'CIRCULAR_DEPENDENCY') return;
warn(warning);
}
};
库打包多格式输出
// 输出多种格式
export default [
// ESM
{
input: 'src/index.ts',
output: {
file: 'dist/index.esm.js',
format: 'esm',
sourcemap: true
}
},
// CommonJS
{
input: 'src/index.ts',
output: {
file: 'dist/index.cjs.js',
format: 'cjs',
sourcemap: true,
exports: 'named'
}
},
// UMD(全局变量)
{
input: 'src/index.ts',
output: {
file: 'dist/index.umd.js',
format: 'umd',
sourcemap: true,
name: 'MyLib', // 全局变量名
globals: {
react: 'React',
'react-dom': 'ReactDOM'
}
}
}
];
esbuild 深入理解
esbuild 使用 Go 语言编写,性能是传统 JS 构建工具的 10-100 倍。
esbuild API
const esbuild = require('esbuild');
const path = require('path');
// 构建
async function build() {
await esbuild.build({
entryPoints: ['src/index.tsx'],
bundle: true,
outdir: 'dist',
format: 'esm',
platform: 'browser',
target: ['es2020'],
// JSX
jsx: 'automatic',
jsxImportSource: 'react',
// 优化
minify: true,
treeShaking: true,
// Source Map
sourcemap: true,
// 代码分割
splitting: true,
// _loader: {
// '.svg': 'dataurl'
// },
// 定义全局变量
define: {
'process.env.NODE_ENV': '"production"'
},
// 外部依赖
external: ['react', 'react-dom'],
// 日志
logLevel: 'info',
metafile: true // 生成元数据
});
console.log('Build completed');
}
// 开发服务
async function serve() {
const ctx = await esbuild.context({
entryPoints: ['src/index.tsx'],
bundle: true,
format: 'esm',
outdir: 'dist',
jsx: 'automatic',
jsxImportSource: 'react',
serve: {
port: 3000,
servedir: 'public'
},
watch: {
onRebuild: (error, result) => {
if (error) {
console.error('Rebuild failed:', error);
} else {
console.log('Rebuilt:', result);
}
}
}
});
await ctx.watch();
console.log('Watching...');
}
build().catch(() => process.exit(1));
使用插件
const esbuild = require('esbuild');
const fs = require('fs');
// 自定义插件:加载 .env 文件
function envPlugin() {
return {
name: 'env',
setup(build) {
build.onLoad({ filter: /\.env$/ }, async (args) => {
const contents = await fs.promises.readFile(args.path, 'utf8');
const envVars = {};
contents.split('\n').forEach(line => {
const match = line.match(/^([^=]+)=(.*)$/);
if (match) {
const key = match[1].trim();
const value = match[2].trim();
envVars[key] = JSON.stringify(value);
}
});
return {
contents: `export ${Object.entries(envVars)
.map(([k, v]) => `const ${k} = ${v}`)
.join(';')}`,
loader: 'js'
};
});
}
};
}
构建工具性能优化
Webpack 性能优化
// 优化点
module.exports = {
// 1. 使用 cache
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
},
// 2. 并行构建
parallelism: 100,
// 3. 懒编译
entry: {
main: () => new Promise((resolve) => {
setTimeout(() => resolve('./src/main.js'), 5000);
})
},
// 4. DllPlugin 预编译
plugins: [
new webpack.DllPlugin({
context: __dirname,
name: '[name]_dll',
path: path.join(__dirname, 'manifest.json')
})
]
};
Vite 性能优化
// vite.config.ts 优化
export default defineConfig({
// 依赖预构建
optimizeDeps: {
include: [
'react',
'react-dom',
'lodash/lodash.default',
'big-library'
],
exclude: ['internal-lib'] // 排除某些包
},
// 构建策略
build: {
// 分包大小
chunkSizeWarningLimit: 500, // KB
// 禁用某些优化
commonjsOptions: {
ignoreDynamicRequires: true
}
},
// 开发服务器
server: {
// 使用 http2
http: true,
// 预热
warmup: {
ssrFiles: ['./src/entry-server.tsx']
}
}
});
构建工具选择指南
选择建议
| 场景 | 推荐工具 | 原因 |
|---|---|---|
| 大型应用(>1000 模块) | Webpack / Vite | 成熟的生态和优化 |
| 小型项目 / 库 | Rollup / esbuild | 轻量、快速 |
| React 新项目 | Vite | 最佳开发体验 |
| Vue 项目 | Vite | 官方推荐 |
| 需要兼容旧浏览器 | Webpack | 更好的 polyfill |
| 库开发 | Rollup | 干净的输出 |
| 追求极致速度 | esbuild | Go 实现,极速 |
这一章想说的
构建工具是前端工程的核心:
- Webpack:功能最全面,生态最丰富,但配置复杂
- Vite:基于 ESM,开发体验极佳,适合新项目
- Rollup:专注于库打包,输出干净
- esbuild:极速构建,Go 语言实现
理解构建工具的原理比会配置更重要——了解 Babel/PostCSS 如何转换代码、Tree-shaking 如何工作、Code Splitting 的策略,才能真正掌握前端工程化。