Webpack 进阶
大约 6 分钟约 1768 字
Webpack 进阶
Webpack 高级配置概览
Webpack 是前端工程化中最成熟的打包工具,其强大的 Loader/Plugin 体系几乎可以处理任何类型的资源。进阶 Webpack 配置涵盖代码分割、模块联邦、自定义 Loader/Plugin 开发和构建性能优化。理解这些高级特性有助于优化构建产物体积和开发体验。
代码分割策略
代码分割(Code Splitting)是减小首屏加载体积的核心手段。Webpack 提供多种代码分割方式。
SplitChunksPlugin 配置
// webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
mode: 'production',
entry: {
main: './src/index.tsx',
admin: './src/admin/index.tsx',
},
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
publicPath: '/',
},
optimization: {
// 代码分割策略
splitChunks: {
chunks: 'all',
minSize: 20 * 1024, // 最小 20KB 才拆分
minRemainingSize: 0,
minChunks: 1, // 至少被 1 个 chunk 引用
maxAsyncRequests: 30, // 最大异步请求数
maxInitialRequests: 30, // 最大初始请求数
automaticNameDelimiter: '~',
cacheGroups: {
// React 核心库单独拆分
react: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router|scheduler)[\\/]/,
name: 'react-vendor',
priority: 30,
reuseExistingChunk: true,
},
// UI 组件库单独拆分
ui: {
test: /[\\/]node_modules[\\/](@ant-design|antd|element-plus)[\\/]/,
name: 'ui-vendor',
priority: 20,
reuseExistingChunk: true,
},
// 图表库单独拆分
charts: {
test: /[\\/]node_modules[\\/](echarts|d3)[\\/]/,
name: 'charts-vendor',
priority: 20,
reuseExistingChunk: true,
},
// 其他第三方库
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10,
reuseExistingChunk: true,
},
// 公共模块(被多个入口引用)
common: {
minChunks: 2,
name: 'common',
priority: 5,
reuseExistingChunk: true,
},
},
},
// 将运行时代码提取到单独文件
runtimeChunk: {
name: 'runtime',
},
// 模块 ID 生成策略(确保哈希稳定)
moduleIds: 'deterministic',
// 压缩配置
minimize: true,
minimizer: [
// 使用 terser 或 swc 压缩 JS
`...`,
],
},
module: {
rules: [
{
test: /\.tsx?$/,
use: {
loader: 'swc-loader', // SWC 替代 Babel,速度更快
options: {
jsc: {
parser: { syntax: 'typescript', tsx: true },
transform: { react: { runtime: 'automatic' } },
},
},
},
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader', // 处理 autoprefixer 等
],
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
{
loader: 'less-loader',
options: { lessOptions: { javascriptEnabled: true } },
},
],
},
{
test: /\.(png|jpg|gif|svg|webp)$/,
type: 'asset',
parser: {
dataUrlCondition: { maxSize: 8 * 1024 }, // 小于 8KB 转 base64
},
generator: {
filename: 'images/[name].[contenthash:8][ext]',
},
},
{
test: /\.(woff2?|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[contenthash:8][ext]',
},
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css',
}),
],
};动态导入实现路由懒加载
// React Router 懒加载
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 动态导入 — Webpack 自动创建独立 chunk
const Dashboard = lazy(() => import('./pages/Dashboard'));
const UserList = lazy(() => import('./pages/UserList'));
const Settings = lazy(() => import('./pages/Settings'));
const DeviceMonitor = lazy(() => import('./pages/DeviceMonitor'));
// 预加载策略
function prefetchPage(path: string) {
// webpackPrefetch — 空闲时预加载
import(/* webpackPrefetch: true */ './pages/DeviceMonitor');
}
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div className="loading">加载中...</div>}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/users" element={<UserList />} />
<Route path="/settings" element={<Settings />} />
<Route path="/devices" element={<DeviceMonitor />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}Module Federation — 微前端
Module Federation 是 Webpack 5 引入的模块联邦方案,允许多个独立构建的应用在运行时共享模块。
Host 应用配置
// host/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host_app',
remotes: {
// 远程应用 — key: 暴露的全局变量名
deviceApp: 'deviceApp@http://localhost:3001/remoteEntry.js',
alarmApp: 'alarmApp@http://localhost:3002/remoteEntry.js',
},
shared: {
react: {
singleton: true, // 全局唯一 React 实例
requiredVersion: '^18.2.0',
eager: false, // 异步加载共享依赖
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
'react-router-dom': {
singleton: true,
},
},
}),
],
};Remote 应用配置
// device-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'deviceApp',
filename: 'remoteEntry.js',
exposes: {
// 暴露的模块
'./DeviceList': './src/components/DeviceList',
'./DeviceDetail': './src/pages/DeviceDetail',
'./useDevices': './src/hooks/useDevices',
},
shared: {
react: { singleton: true, requiredVersion: '^18.2.0' },
'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
},
}),
],
};在 Host 中使用远程模块
// host/src/App.tsx
import React, { lazy, Suspense } from 'react';
// 动态导入远程模块
const RemoteDeviceList = lazy(() => import('deviceApp/DeviceList'));
const RemoteDeviceDetail = lazy(() => import('deviceApp/DeviceDetail'));
function App() {
return (
<div>
<h1>主应用</h1>
<Suspense fallback={<div>加载远程模块...</div>}>
<RemoteDeviceList />
</Suspense>
</div>
);
}自定义 Plugin 开发
Webpack Plugin 通过 Tapable 事件系统在构建的不同阶段注入自定义逻辑。
class BuildTimePlugin {
constructor(options = {}) {
this.options = options;
}
apply(compiler) {
const { logFile = false } = this.options;
compiler.hooks.done.tap('BuildTimePlugin', (stats) => {
const duration = (stats.endTime - stats.startTime) / 1000;
const assets = stats.toJson().assets;
if (stats.hasErrors()) {
console.error(`[BuildTime] 构建失败 (${duration.toFixed(2)}s)`);
stats.toJson().errors.forEach(err => console.error(err.message));
} else {
const totalSize = assets.reduce((sum, a) => sum + a.size, 0);
const totalSizeKB = (totalSize / 1024).toFixed(2);
console.log(`[BuildTime] 构建成功 (${duration.toFixed(2)}s)`);
console.log(`[BuildTime] ${assets.length} 个文件, 总大小 ${totalSizeKB} KB`);
}
});
}
}
// 构建进度插件
class BuildProgressPlugin {
apply(compiler) {
let lastProgress = 0;
compiler.hooks.compilation.tap('BuildProgressPlugin', (compilation) => {
new webpack.ProgressPlugin((percentage, message) => {
const pct = Math.round(percentage * 100);
// 只在有显著变化时输出
if (pct - lastProgress >= 5 || pct === 100) {
const bar = '█'.repeat(Math.floor(pct / 5)) + '░'.repeat(20 - Math.floor(pct / 5));
process.stdout.write(`\r[${bar}] ${pct}% ${message}`);
lastProgress = pct;
}
}).apply(compiler);
});
}
}
// 环境变量注入插件
class DefineEnvPlugin {
constructor(envVars) {
this.envVars = envVars;
}
apply(compiler) {
const definitions = {};
for (const [key, value] of Object.entries(this.envVars)) {
definitions[`process.env.${key}`] = JSON.stringify(value);
}
new webpack.DefinePlugin(definitions).apply(compiler);
}
}自定义 Loader 开发
Loader 用于转换特定类型的文件,将其处理为 Webpack 能理解的模块。
// markdown-loader.js — 将 Markdown 转为 HTML 模块
const marked = require('marked');
module.exports = function markdownLoader(source) {
const html = marked.parse(source);
// 返回 JS 模块代码
return `
import React from 'react';
export default function MarkdownComponent() {
return React.createElement('div', {
className: 'markdown-content',
dangerouslySetInnerHTML: { __html: ${JSON.stringify(html)} }
});
}
export const rawHtml = ${JSON.stringify(html)};
`;
};
// svg-to-component-loader.js — 将 SVG 转为 React 组件
module.exports = function svgToComponentLoader(source) {
// 提取 SVG 属性和内容
const match = source.match(/<svg([^>]*)>([\s\S]*)<\/svg>/);
if (!match) throw new Error('Invalid SVG');
const [, attrs, content] = match;
return `
import React from 'react';
const SvgIcon = React.forwardRef((props, ref) => (
<svg ref={ref} {...${JSON.stringify(parseSVGAttrs(attrs))}} {...props}>
${content}
</svg>
));
SvgIcon.displayName = 'SvgIcon';
export default SvgIcon;
`;
};
function parseSVGAttrs(attrString) {
const attrs = {};
const regex = /(\w[\w-]*)=["']([^"']*)["']/g;
let match;
while ((match = regex.exec(attrString)) !== null) {
attrs[match[1]] = match[2];
}
return attrs;
}构建性能优化
缓存配置
module.exports = {
// 文件系统缓存 — 二次构建速度提升 80%+
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename], // 配置文件变化时重新构建
},
cacheDirectory: path.resolve(__dirname, '.webpack_cache'),
},
// 多线程构建
// 使用 thread-loader 或 HappyPack 处理耗时 Loader
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'thread-loader',
options: { workers: require('os').cpus().length - 1 },
},
'swc-loader',
],
},
],
},
// 排除不需要构建的目录
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
extensions: ['.tsx', '.ts', '.js', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
},
},
};Tree Shaking 优化
module.exports = {
optimization: {
usedExports: true, // 分析未使用的导出
sideEffects: true, // 根据 package.json 的 sideEffects 标记
},
};
// package.json — 标记哪些文件有副作用
{
"sideEffects": [
"*.css",
"*.less",
"./src/polyfills.ts"
]
}构建产物分析
// 开发环境启用分析
if (process.env.ANALYZE) {
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: true,
reportFilename: 'bundle-report.html',
})
);
}
// 命令行分析
// ANALYZE=true npx webpack --profile --json > stats.json
// npx webpack-bundle-analyzer stats.json常见误区
- 所有 node_modules 打成一个 vendor 包 — 应按功能拆分(react-vendor、ui-vendor)
- 忘记配置 runtimeChunk — 入口文件变化频繁,影响缓存命中率
- 过度使用 Loader 处理本可以预处理的资源 — 如大图片应使用 CDN 或 OSS
- source-map 上传到 CDN — 应只上传到错误监控服务(如 Sentry)
排错清单
- 用
webpack --profile --json > stats.json输出构建分析 - 检查 SplitChunks 配置是否合理分包
- 确认 Tree Shaking 是否生效(检查 sideEffects 配置)
- 确认 contenthash 是否基于文件内容变化
- 用 Bundle Analyzer 定期检查包体积
最佳实践总结
- 按功能拆分 vendor — react、ui、charts 分别打包
- 使用 SWC/esbuild 替代 Babel — 构建速度提升 5-10 倍
- 启用文件系统缓存 — 二次构建速度大幅提升
- 配置 runtimeChunk — 提升缓存命中率
- 使用 Bundle Analyzer — 定期检查包体积
