Webpack专题研究
企业级Webpack配置
核心配置结构
javascript
// webpack.config.js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
entry: {
main: './src/index.js',
vendor: ['react', 'react-dom']
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction ? '[name].[contenthash].js' : '[name].js',
chunkFilename: isProduction ? '[id].[contenthash].js' : '[id].js',
publicPath: isProduction ? 'https://cdn.example.com/' : '/'
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['@babel/plugin-transform-runtime']
}
}
},
{
test: /\.(css|scss)$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
{
loader: 'css-loader',
options: {
modules: {
auto: /\.module\.(css|scss)$/,
localIdentName: '[name]__[local]--[hash:base64:5]'
}
}
},
'postcss-loader',
'sass-loader'
]
},
{
test: /\.(png|jpe?g|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 8kb
}
},
generator: {
filename: 'assets/images/[name].[hash][ext]'
}
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'assets/fonts/[name].[hash][ext]'
}
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html',
minify: isProduction ? {
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true
} : false
}),
new MiniCssExtractPlugin({
filename: isProduction ? '[name].[contenthash].css' : '[name].css',
chunkFilename: isProduction ? '[id].[contenthash].css' : '[id].css'
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development'),
'process.env.API_URL': JSON.stringify(process.env.API_URL || 'http://localhost:3000')
})
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial'
},
common: {
name: 'common',
minChunks: 2,
priority: 5,
chunks: 'initial'
}
}
},
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: isProduction,
drop_debugger: isProduction
},
output: {
comments: false
}
}
}),
new CssMinimizerPlugin()
],
runtimeChunk: 'single'
},
resolve: {
extensions: ['.js', '.jsx', '.json'],
alias: {
'@': path.resolve(__dirname, 'src')
}
},
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 3000,
hot: true,
historyApiFallback: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
},
devtool: isProduction ? 'source-map' : 'cheap-module-eval-source-map'
};
};常见场景与配置
1. 多页面应用配置
javascript
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
page1: './src/page1.js',
page2: './src/page2.js'
},
plugins: [
new HtmlWebpackPlugin({
filename: 'page1.html',
template: './src/page1.html',
chunks: ['page1', 'vendor', 'common']
}),
new HtmlWebpackPlugin({
filename: 'page2.html',
template: './src/page2.html',
chunks: ['page2', 'vendor', 'common']
})
]
};2. 代码分割与懒加载
javascript
// 动态导入实现懒加载
import('./lazyComponent').then(module => {
const LazyComponent = module.default;
// 使用LazyComponent
});
// React中的懒加载
import React, { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}3. 环境变量配置
javascript
// webpack.config.js
module.exports = (env, argv) => {
return {
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(argv.mode),
API_URL: JSON.stringify(process.env.API_URL || 'http://localhost:3000'),
VERSION: JSON.stringify(require('./package.json').version)
}
})
]
};
};4. Tree Shaking配置
javascript
// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
minimize: true
}
};易错点与坑
1. 路径配置错误
问题:静态资源路径错误,导致404 原因:publicPath配置不正确,或者资源引用路径与输出路径不匹配 解决方案:
javascript
output: {
publicPath: process.env.NODE_ENV === 'production' ? 'https://cdn.example.com/' : '/'
}2. 缓存失效问题
问题:版本更新后,用户仍访问旧资源 原因:未使用内容哈希或缓存策略不当 解决方案:
javascript
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[id].[contenthash].js'
}3. 构建性能问题
问题:构建时间过长 原因:文件过多、配置不当、依赖过多 解决方案:
- 使用DllPlugin预编译常用依赖
- 配置exclude/include减少文件扫描范围
- 使用HardSourceWebpackPlugin缓存构建结果
- 升级Webpack版本到最新
4. 配置冲突
问题:不同loader或plugin之间配置冲突 原因:配置顺序不当或参数冲突 解决方案:
- 注意loader执行顺序(从右到左,从下到上)
- 仔细阅读插件文档,避免参数冲突
- 使用
enforce: 'pre'或enforce: 'post'控制loader执行阶段
企业级Webpack插件开发
1. CDN自动上传插件
javascript
// CdnUploadPlugin.js
const fs = require('fs');
const path = require('path');
const axios = require('axios');
const FormData = require('form-data');
class CdnUploadPlugin {
constructor(options) {
this.options = {
cdnUrl: '',
cdnToken: '',
uploadPath: '/',
include: /\.(js|css|png|jpg|gif|svg|woff|woff2|eot|ttf|otf)$/,
...options
};
}
apply(compiler) {
compiler.hooks.afterEmit.tapAsync('CdnUploadPlugin', async (compilation, callback) => {
try {
const assets = Object.keys(compilation.assets);
const uploadPromises = [];
for (const asset of assets) {
if (this.options.include.test(asset)) {
const assetPath = path.join(compiler.outputPath, asset);
if (fs.existsSync(assetPath)) {
uploadPromises.push(this.uploadFile(asset, assetPath));
}
}
}
await Promise.all(uploadPromises);
console.log('所有静态资源已成功上传到CDN');
callback();
} catch (error) {
console.error('CDN上传失败:', error);
callback(error);
}
});
}
async uploadFile(assetName, assetPath) {
const form = new FormData();
form.append('file', fs.createReadStream(assetPath));
form.append('path', this.options.uploadPath);
const response = await axios.post(this.options.cdnUrl, form, {
headers: {
'Authorization': `Bearer ${this.options.cdnToken}`,
...form.getHeaders()
}
});
return response.data;
}
}
module.exports = CdnUploadPlugin;使用方法:
javascript
const CdnUploadPlugin = require('./plugins/CdnUploadPlugin');
module.exports = {
plugins: [
new CdnUploadPlugin({
cdnUrl: 'https://api.example.com/upload',
cdnToken: process.env.CDN_TOKEN,
uploadPath: '/static/'
})
]
};2. 版本号生成插件
javascript
// VersionGeneratorPlugin.js
const fs = require('fs');
const path = require('path');
class VersionGeneratorPlugin {
constructor(options) {
this.options = {
versionFile: 'version.json',
outputPath: '',
...options
};
}
apply(compiler) {
compiler.hooks.afterEmit.tapAsync('VersionGeneratorPlugin', (compilation, callback) => {
try {
const versionData = {
version: this.generateVersion(),
buildTime: new Date().toISOString(),
commitHash: this.getGitCommitHash(),
branch: this.getGitBranch()
};
const outputPath = path.join(
this.options.outputPath || compiler.outputPath,
this.options.versionFile
);
fs.writeFileSync(outputPath, JSON.stringify(versionData, null, 2));
console.log(`版本文件已生成: ${outputPath}`);
// 也可以将版本信息注入到全局变量
compilation.assets['version.txt'] = {
source: () => `${versionData.version}\n${versionData.buildTime}\n${versionData.commitHash}`,
size: () => Buffer.byteLength(`${versionData.version}\n${versionData.buildTime}\n${versionData.commitHash}`)
};
callback();
} catch (error) {
console.error('版本文件生成失败:', error);
callback(error);
}
});
}
generateVersion() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
return `${year}${month}${day}.${hours}${minutes}`;
}
getGitCommitHash() {
try {
return fs.readFileSync('.git/HEAD', 'utf8')
.trim()
.split(' ')[1]
.split('/')
.pop();
} catch (error) {
return 'unknown';
}
}
getGitBranch() {
try {
return fs.readFileSync('.git/HEAD', 'utf8')
.trim()
.split('/')
.pop();
} catch (error) {
return 'unknown';
}
}
}
module.exports = VersionGeneratorPlugin;使用方法:
javascript
const VersionGeneratorPlugin = require('./plugins/VersionGeneratorPlugin');
module.exports = {
plugins: [
new VersionGeneratorPlugin({
versionFile: 'version.json',
outputPath: './dist'
})
]
};3. 构建分析与报告插件
javascript
// BuildAnalyzerPlugin.js
const fs = require('fs');
const path = require('path');
class BuildAnalyzerPlugin {
constructor(options) {
this.options = {
reportFile: 'build-report.html',
outputPath: '',
...options
};
}
apply(compiler) {
compiler.hooks.done.tap('BuildAnalyzerPlugin', (stats) => {
try {
const statsData = stats.toJson();
const report = this.generateReport(statsData);
const outputPath = path.join(
this.options.outputPath || compiler.outputPath,
this.options.reportFile
);
fs.writeFileSync(outputPath, report);
console.log(`构建分析报告已生成: ${outputPath}`);
} catch (error) {
console.error('构建分析报告生成失败:', error);
}
});
}
generateReport(statsData) {
const assets = statsData.assets.sort((a, b) => b.size - a.size);
const chunks = statsData.chunks;
return `
<!DOCTYPE html>
<html>
<head>
<title>构建分析报告</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.section { margin-bottom: 30px; }
.asset-table { width: 100%; border-collapse: collapse; }
.asset-table th, .asset-table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
.asset-table th { background-color: #f2f2f2; }
.size { text-align: right; }
</style>
</head>
<body>
<h1>构建分析报告</h1>
<div class="section">
<h2>构建信息</h2>
<p>时间: ${new Date().toLocaleString()}</p>
<p>版本: ${statsData.version}</p>
<p>耗时: ${statsData.time}ms</p>
</div>
<div class="section">
<h2>资源大小(前20个)</h2>
<table class="asset-table">
<thead>
<tr>
<th>文件名</th>
<th class="size">大小</th>
</tr>
</thead>
<tbody>
${assets.slice(0, 20).map(asset => `
<tr>
<td>${asset.name}</td>
<td class="size">${this.formatSize(asset.size)}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</body>
</html>
`;
}
formatSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}
module.exports = BuildAnalyzerPlugin;使用方法:
javascript
const BuildAnalyzerPlugin = require('./plugins/BuildAnalyzerPlugin');
module.exports = {
plugins: [
new BuildAnalyzerPlugin()
]
};4. 代码规范检查插件
javascript
// CodeLintPlugin.js
const eslint = require('eslint');
const fs = require('fs');
const path = require('path');
class CodeLintPlugin {
constructor(options) {
this.options = {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
configFile: '.eslintrc.js',
ignoreFile: '.eslintignore',
failOnError: true,
...options
};
}
apply(compiler) {
compiler.hooks.afterEmit.tapAsync('CodeLintPlugin', (compilation, callback) => {
try {
const eslintOptions = {
extensions: this.options.extensions,
configFile: this.options.configFile,
ignoreFile: this.options.ignoreFile
};
const eslint = new eslint.ESLint(eslintOptions);
const results = eslint.lintFiles(['src/**/*']);
results.then(lintResults => {
const formatter = eslint.loadFormatter('stylish');
const resultText = formatter.format(lintResults);
console.log(resultText);
const errorCount = lintResults.reduce((sum, result) => sum + result.errorCount, 0);
if (this.options.failOnError && errorCount > 0) {
callback(new Error(`代码规范检查失败,发现${errorCount}个错误`));
} else {
callback();
}
});
} catch (error) {
console.error('代码规范检查失败:', error);
callback(error);
}
});
}
}
module.exports = CodeLintPlugin;使用方法:
javascript
const CodeLintPlugin = require('./plugins/CodeLintPlugin');
module.exports = {
plugins: [
new CodeLintPlugin({
failOnError: process.env.NODE_ENV === 'production'
})
]
};构建工具子菜单配置
在项目的构建工具菜单中,已包含以下内容:
总结与最佳实践
- 配置分离:将开发、测试、生产环境的配置分离,提高可维护性
- 模块化:将复杂配置拆分为多个模块,便于管理和复用
- 性能优化:使用缓存、代码分割、Tree Shaking等技术提高构建性能和运行效率
- 插件定制:根据业务需求开发自定义插件,提升开发效率
- 监控与分析:使用构建分析工具,持续优化构建流程
- 版本控制:使用内容哈希和版本号,确保缓存有效性和可回滚性
Webpack作为前端工程化的核心工具,需要根据业务需求进行合理配置和定制开发。企业级配置需要考虑性能、可维护性、可扩展性等多个方面,同时结合自定义插件满足特定业务场景的需求。