Webpack 详解
约 1695 字大约 6 分钟
Webpack 详解
Webpack基础概念 🟢
1. 什么是Webpack
Webpack是一个现代JavaScript应用程序的静态模块打包工具。当webpack处理应用程序时,它会在内部构建一个依赖图(dependency graph),此依赖图对应映射到项目所需的每个模块,并生成一个或多个bundle。
主要功能:
- 模块打包:将不同模块的文件打包整合在一起
- 代码转换:将TypeScript、SCSS等转换成JavaScript、CSS
- 依赖管理:管理项目中的依赖关系
- 性能优化:压缩代码、提取公共代码、按需加载等
2. 核心概念
2.1 Entry(入口)
指定webpack开始构建的入口模块,从这个入口开始,webpack会找出有哪些模块和库是入口起点(直接和间接)依赖的。
// 单入口配置
module.exports = {
entry: './src/index.js'
};
// 多入口配置
module.exports = {
entry: {
main: './src/index.js',
vendor: './src/vendor.js'
}
};
// 动态入口
module.exports = {
entry: () => new Promise((resolve) => {
resolve({
main: './src/index.js',
vendor: './src/vendor.js'
});
})
};
2.2 Output(出口)
告诉webpack在哪里输出它所创建的bundle,以及如何命名这些文件。
const path = require('path');
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
chunkFilename: '[name].[chunkhash].chunk.js',
publicPath: '/',
clean: true, // 在生成文件之前清空 output 目录
assetModuleFilename: 'images/[hash][ext][query]' // 资源文件名配置
}
};
2.3 Loader(加载器)
webpack本身只能理解JavaScript和JSON文件,loader让webpack能够去处理其他类型的文件。
module.exports = {
module: {
rules: [
// JavaScript/TypeScript处理
{
test: /\.(js|ts)x?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript'
],
plugins: [
'@babel/plugin-transform-runtime'
]
}
}
},
// CSS处理
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 1
}
},
'postcss-loader'
]
},
// 图片处理
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 4 * 1024 // 4kb
}
}
}
]
}
};
2.4 Plugin(插件)
插件用于执行范围更广的任务,包括:打包优化、资源管理、注入环境变量等。
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
// 生成HTML文件
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
minify: {
collapseWhitespace: true,
removeComments: true
}
}),
// 提取CSS文件
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[id].[contenthash].css'
}),
// 分析打包结果
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
]
};
Webpack高级配置 🟡
1. 优化配置
1.1 构建性能优化
- 缓存配置:
module.exports = {
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
},
name: 'production-cache'
},
module: {
rules: [{
test: /\.js$/,
use: 'babel-loader',
options: {
cacheDirectory: true // babel-loader缓存
}
}]
}
};
- 多进程构建:
const ThreadLoader = require('thread-loader');
ThreadLoader.warmup({
// 预热线程池
workers: 4,
workerParallelJobs: 50
}, [
'babel-loader',
'@babel/preset-env'
]);
module.exports = {
module: {
rules: [{
test: /\.js$/,
use: [
{
loader: 'thread-loader',
options: {
workers: 4,
workerParallelJobs: 50
}
},
'babel-loader'
]
}]
}
};
1.2 打包优化
- 代码分割:
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
- Tree Shaking配置:
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
minimize: true,
concatenateModules: true
}
};
2. 开发环境配置
2.1 开发服务器配置
module.exports = {
devServer: {
static: {
directory: path.join(__dirname, 'public')
},
compress: true,
port: 3000,
hot: true,
historyApiFallback: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
pathRewrite: { '^/api': '' },
changeOrigin: true
}
},
client: {
overlay: true,
progress: true
}
}
};
2.2 Source Map配置
module.exports = {
devtool: process.env.NODE_ENV ``` 'production'
? 'source-map'
: 'eval-cheap-module-source-map'
};
Webpack高级配置进阶 🔴
1. 模块联邦(Module Federation)
1.1 基本配置
问题:如何实现多个独立应用间的代码共享?
答案:使用模块联邦实现微前端架构:
// 主应用 webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
filename: 'remoteEntry.js',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
app2: 'app2@http://localhost:3002/remoteEntry.js'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
// 子应用 webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./Header': './src/components/Header'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
1.2 高级用法
问题:如何处理模块联邦中的动态远程加载?
答案:使用动态导入和错误处理:
// 动态加载远程模块
const loadComponent = async (scope, module) => {
// 初始化共享作用域
await __webpack_init_sharing__('default');
const container = window[scope];
// 初始化容器
await container.init(__webpack_share_scopes__.default);
const factory = await container.get(module);
return factory();
};
// 错误处理和重试机制
const loadRemoteComponent = async (scope, module) => {
const MAX_RETRIES = 3;
let retries = 0;
while (retries < MAX_RETRIES) {
try {
return await loadComponent(scope, module);
} catch (error) {
retries++;
if (retries === MAX_RETRIES) {
throw new Error(`Failed to load remote component: ${scope}/${module}`);
}
// 指数退避
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, retries) * 1000)
);
}
}
};
2. 构建性能优化
2.1 持久化缓存
问题:如何优化Webpack的构建缓存?
答案:配置高级缓存策略:
module.exports = {
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
},
name: `cache-${process.env.NODE_ENV}`,
version: createEnvironmentHash(process.env),
cacheDirectory: path.resolve(__dirname, '.temp_cache'),
compression: 'gzip',
store: 'pack',
idleTimeout: 60000,
memoryCacheUnaffected: true
},
snapshot: {
managedPaths: [/^(.+?[\\/]node_modules[\\/])/],
immutablePaths: [],
buildDependencies: {
timestamp: true,
hash: true
},
module: {
timestamp: true
},
resolve: {
timestamp: true
}
}
};
function createEnvironmentHash(env) {
const hash = require('crypto').createHash('md5');
hash.update(JSON.stringify(env));
return hash.digest('hex');
}
2.2 多进程优化
问题:如何合理使用多进程提升构建速度?
答案:结合thread-loader和parallel-webpack:
const os = require('os');
const threadLoader = require('thread-loader');
const workerPool = {
workers: os.cpus().length - 1,
poolTimeout: 2000,
workerNodeArgs: ['--max-old-space-size=4096'],
workerParallelJobs: 50
};
// 预热线程池
threadLoader.warmup(workerPool, [
'babel-loader',
'@babel/preset-env',
'@babel/preset-react'
]);
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'thread-loader',
options: workerPool
},
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
cacheDirectory: true
}
}
]
}
]
}
};
3. 高级插件开发
3.1 自定义插件
问题:如何开发一个复杂的Webpack插件?
答案:实现带有完整生命周期的插件:
class AdvancedPlugin {
constructor(options = {}) {
this.options = options;
this.hooks = {
beforeCompile: new AsyncSeriesHook(['params']),
afterCompile: new SyncHook(['compilation'])
};
}
apply(compiler) {
// 注册自定义钩子
compiler.hooks.beforeCompile.tapAsync(
'AdvancedPlugin',
async (params, callback) => {
await this.hooks.beforeCompile.promise(params);
callback();
}
);
// 监听编译完成
compiler.hooks.done.tap('AdvancedPlugin', stats => {
this.hooks.afterCompile.call(stats.compilation);
});
// 资源优化
compiler.hooks.compilation.tap('AdvancedPlugin', compilation => {
compilation.hooks.optimizeAssets.tapAsync(
'AdvancedPlugin',
async (assets, callback) => {
try {
await this.optimizeAssets(assets, compilation);
callback();
} catch (error) {
callback(error);
}
}
);
});
}
async optimizeAssets(assets, compilation) {
// 资源优化逻辑
for (const [name, source] of Object.entries(assets)) {
if (this.shouldOptimize(name)) {
const optimized = await this.optimize(source);
compilation.updateAsset(name, optimized);
}
}
}
shouldOptimize(assetName) {
// 判断是否需要优化
const { include, exclude } = this.options;
if (include && !include.test(assetName)) return false;
if (exclude && exclude.test(assetName)) return false;
return true;
}
async optimize(source) {
// 具体的优化实现
return source;
}
}
3.2 插件性能优化
问题:如何优化插件的性能?
答案:实现高效的插件:
class HighPerformancePlugin {
constructor(options = {}) {
this.options = options;
this.cache = new Map();
}
apply(compiler) {
// 使用WeakMap缓存编译结果
const compilationCache = new WeakMap();
compiler.hooks.compilation.tap(
'HighPerformancePlugin',
compilation => {
// 并行处理资源
compilation.hooks.optimizeAssets.tapPromise(
'HighPerformancePlugin',
async assets => {
const promises = Object.entries(assets)
.filter(([name]) => this.shouldProcess(name))
.map(async ([name, asset]) => {
const cached = this.cache.get(name);
if (cached && this.isCacheValid(cached, asset)) {
return [name, cached.result];
}
const result = await this.processAsset(asset);
this.cache.set(name, {
result,
hash: this.getAssetHash(asset)
});
return [name, result];
});
const results = await Promise.all(promises);
results.forEach(([name, result]) => {
compilation.updateAsset(name, result);
});
}
);
}
);
// 清理缓存
compiler.hooks.done.tap('HighPerformancePlugin', () => {
if (this.cache.size > 100) {
this.cache.clear();
}
});
}
isCacheValid(cached, asset) {
return this.getAssetHash(asset) === cached.hash;
}
getAssetHash(asset) {
return crypto
.createHash('md5')
.update(asset.source())
.digest('hex');
}
async processAsset(asset) {
// 具体的资源处理逻辑
return asset;
}
}
4. 构建分析与优化
4.1 构建分析工具
问题:如何分析和优化构建产物?
答案:使用高级分析工具:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const DuplicatePackageCheckerPlugin = require('duplicate-package-checker-webpack-plugin');
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
defaultSizes: 'gzip',
openAnalyzer: false,
generateStatsFile: true,
statsFilename: 'stats.json',
statsOptions: {
source: false,
modules: true,
chunks: true,
assets: true
},
excludeAssets: [
/\.map$/,
/asset-manifest\.json$/
]
}),
new DuplicatePackageCheckerPlugin({
verbose: true,
emitError: true,
strict: true
})
]
};
// 使用speed-measure-webpack-plugin包装配置
const smp = new SpeedMeasurePlugin({
outputFormat: 'humanVerbose',
loaderTopFiles: 10
});
module.exports = smp.wrap(config);
[未完待续...]