Webpack配置详解与实战:从入门到精通
目录
- 前言
- Webpack基础
- 核心概念深度解析
- 基础配置详解
- Loader开发实战
- Plugin开发实战
- 高级配置与优化
- 性能优化策略
- 多场景实战配置
- 原理与源码分析
- 常见问题与解决方案
- 最佳实践总结
- 总结
前言
Webpack是现代前端开发中最重要的构建工具之一,它通过强大的模块打包能力,将复杂的项目结构转化为可部署的静态资源。本篇文章将从Webpack的基本概念开始,逐步深入到Loader和Plugin的开发,最后通过实战案例和性能优化技巧,帮助你全面掌握Webpack的使用。
Webpack基础
什么是Webpack
Webpack是一个模块打包器,它的主要功能是分析项目结构,找到JavaScript模块以及其他一些浏览器不能直接运行的扩展语言(如TypeScript、Sass等),并将它们打包为合适的格式供浏览器使用。
核心特性
- 模块打包:将模块化的代码打包为浏览器可执行的格式
- 代码分割:将代码分割成多个chunk,按需加载
- 插件系统:强大的插件系统用于扩展功能
- 模块转换:支持多种模块格式(ES6、CommonJS、AMD等)
- 热更新:开发时支持热模块替换(HMR)
Webpack工作原理
Entry入口文件
依赖图
Loader转换
Module模块
Plugin插件
Bundle打包输出
Webpack通过从入口文件开始,递归地构建一个依赖图,然后将所有模块打包成一个或多个bundle。
核心概念深度解析
1. Entry(入口)
入口是Webpack构建的起始点,它告诉Webpack从哪里开始构建依赖图。
javascript
// 单入口
module.exports = {
entry: './src/index.js'
};
// 对象形式(多入口)
module.exports = {
entry: {
main: './src/main.js',
admin: './src/admin.js',
vendor: ['react', 'react-dom']
}
};
// 函数形式(动态入口)
module.exports = {
entry: () => {
return {
main: './src/main.js',
vendor: './src/vendor.js'
};
}
};
2. Output(输出)
Output指定Webpack如何输出打包后的文件。
javascript
module.exports = {
output: {
// 输出文件路径(绝对路径)
path: path.resolve(__dirname, 'dist'),
// 输出文件名
filename: '[name].js', // [name]会被替换为chunk名
// 输出文件名包含contenthash,用于缓存
filename: '[name].[contenthash].js',
// 非入口chunk文件名
chunkFilename: '[name].[contenthash].chunk.js',
// 公共路径
publicPath: '/',
// 或者CDN路径
publicPath: 'https://cdn.example.com/',
// 清理输出目录
clean: true,
// 静态资源文件名模板
assetModuleFilename: 'assets/[hash][ext][query]'
}
};
3. Module(模块)
Module定义了如何处理项目中的各种模块。
javascript
module.exports = {
module: {
rules: [
// Loader规则
{
test: /\.js$/, // 匹配文件
exclude: /node_modules/, // 排除文件
include: path.resolve(__dirname, 'src'), // 包含文件
use: [ // 使用Loader
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
]
}
]
}
};
4. Resolve(解析)
Resolve配置如何解析模块路径。
javascript
module.exports = {
resolve: {
// 文件扩展名
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
// 路径别名
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
'$': path.resolve(__dirname, 'public')
},
// 模块查找目录
modules: [
path.resolve(__dirname, 'src'),
'node_modules'
],
// 主文件
mainFiles: ['index'],
// 主字段
mainFields: ['browser', 'module', 'main']
}
};
5. Plugin(插件)
插件用于扩展Webpack功能,处理各种任务。
javascript
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
plugins: [
// 生成HTML文件
new HtmlWebpackPlugin({
template: './src/index.html',
inject: 'head',
minify: true
}),
// 提取CSS为独立文件
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[name].[contenthash].chunk.css'
})
]
};
6. Mode(模式)
Mode指定运行环境,影响Webpack的优化策略。
javascript
module.exports = {
mode: 'development', // 或 'production' 或 'none'
// 开发模式:启用调试、源码映射、快速的增量构建
// 生产模式:启用所有优化,如压缩、Tree-shaking等
// none:不启用任何优化
};
基础配置详解
1. 开发环境配置
javascript
// webpack.dev.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].js',
publicPath: '/',
clean: true
},
devtool: 'eval-cheap-module-source-map',
devServer: {
static: {
directory: path.join(__dirname, 'dist')
},
port: 3000,
hot: true, // 热更新
open: true, // 自动打开浏览器
compress: true, // 启用gzip压缩
historyApiFallback: true, // SPA路由支持
proxy: { // 代理配置
'/api': {
target: 'http://localhost:4000',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
},
module: {
rules: [
// JavaScript/JSX
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { useBuiltIns: 'usage', corejs: 3 }],
['@babel/preset-react', { runtime: 'automatic' }]
],
plugins: ['react-refresh/babel']
}
}
},
// CSS
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
// SCSS/SASS
{
test: /\.(scss|sass)$/,
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
},
// 图片和字体
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 8KB
}
},
generator: {
filename: 'images/[name].[hash][ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[hash][ext]'
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
inject: 'body'
})
],
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {
'@': path.resolve(__dirname, 'src')
}
}
};
2. 生产环境配置
javascript
// webpack.prod.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
mode: 'production',
entry: {
main: './src/index.js',
vendor: ['react', 'react-dom']
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash].js',
chunkFilename: 'js/[name].[contenthash].chunk.js',
assetModuleFilename: 'assets/[hash][ext][query]',
publicPath: '/',
clean: true
},
devtool: 'source-map',
module: {
rules: [
// JavaScript/JSX
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: { browsers: ['> 1%', 'last 2 versions'] },
useBuiltIns: 'usage',
corejs: 3
}],
['@babel/preset-react', { runtime: 'automatic' }]
],
plugins: [
'@babel/plugin-transform-react-constant-elements',
'@babel/plugin-transform-react-inline-elements'
]
}
}
},
// CSS
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
['autoprefixer', {}],
['cssnano', { preset: 'default' }]
]
}
}
}
]
},
// SCSS/SASS
{
test: /\.(scss|sass)$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['autoprefixer']
}
}
},
'sass-loader'
]
},
// 图片
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024
}
},
generator: {
filename: 'images/[name].[hash][ext]'
}
},
// 字体
{
test: /\.(woff2?|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[hash][ext]'
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
}
}),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css',
chunkFilename: 'css/[name].[contenthash].chunk.css'
}),
// 分析打包结果
process.env.ANALYZE && new BundleAnalyzerPlugin()
].filter(Boolean),
optimization: {
minimize: true,
splitChunks: {
chunks: 'all',
cacheGroups: {
// 第三方库分离
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
// React相关
react: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
name: 'react',
chunks: 'all',
priority: 20,
enforce: true
},
// 公共代码
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true
}
}
},
// 运行时提取
runtimeChunk: {
name: 'runtime'
},
// 压缩配置
minimizer: [
(compiler) => {
const TerserPlugin = require('terser-webpack-plugin');
new TerserPlugin({
extractComments: false,
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.info', 'console.debug']
},
mangle: {
safari10: true
},
format: {
safari10: true
}
}
}).apply(compiler);
}
]
},
performance: {
maxAssetSize: 250000,
maxEntrypointSize: 250000,
hints: 'warning'
}
};
3. 完整配置文件
javascript
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
// 基本配置
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction ? '[name].[contenthash].js' : '[name].js',
clean: true,
publicPath: '/'
},
// 模式
mode: isProduction ? 'production' : 'development',
// 源码映射
devtool: isProduction ? 'source-map' : 'eval-cheap-module-source-map',
// 开发服务器
devServer: {
static: path.resolve(__dirname, 'dist'),
port: 3000,
hot: true,
open: true
},
// 模块规则
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: 'defaults',
useBuiltIns: 'usage',
corejs: 3
}],
['@babel/preset-react', {
runtime: 'automatic'
}]
],
plugins: isProduction ? [] : ['react-refresh/babel']
}
}
},
{
test: /\.css$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024
}
}
}
]
},
// 插件
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html'
}),
...(isProduction ? [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css'
})
] : [])
],
// 解析配置
resolve: {
extensions: ['.js', '.jsx', '.json'],
alias: {
'@': path.resolve(__dirname, 'src')
}
},
// 优化配置
optimization: {
splitChunks: {
chunks: 'all'
},
runtimeChunk: {
name: 'runtime'
}
}
};
};
Loader开发实战
Loader是Webpack的核心概念之一,它用于对模块的源代码进行转换。Loader可以将任何类型的文件转换为Webpack能够处理的有效模块。
1. Loader基础
Loader本质是一个函数,接收源文件内容,返回转换后的内容。
javascript
// 基本Loader结构
module.exports = function(source, sourceMap) {
// source: 源文件内容
// sourceMap: 源码映射(可选)
// 转换逻辑
const result = transform(source);
// 如果有源码映射,需要返回它
if (sourceMap) {
return result + '\n//# sourceMappingURL=' + sourceMap;
}
return result;
};
2. 简单示例:日志Loader
创建一个在控制台输出文件路径的Loader。
javascript
// loaders/logger-loader.js
const path = require('path');
module.exports = function(source) {
// 获取文件信息
const filePath = this.resourcePath;
const fileName = path.basename(filePath);
console.log(`[Logger Loader] Processing file: ${fileName}`);
console.log(`[Logger Loader] Path: ${filePath}`);
// 返回原内容
return source;
};
使用方式:
javascript
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve(__dirname, 'loaders/logger-loader.js')
}
]
}
]
}
};
3. 异步Loader
javascript
// loaders/async-loader.js
const { getOptions } = require('loader-utils');
module.exports = function(source) {
const callback = this.async();
// 异步处理
const options = getOptions(this);
setTimeout(() => {
// 处理结果
const result = source.replace(/console\.log/g, '// console.log');
callback(null, result);
}, 100);
};
4. 实战案例:自定义Markdown Loader
将Markdown文件转换为HTML。
javascript
// loaders/markdown-loader.js
const marked = require('marked');
const hljs = require('highlight.js');
module.exports = function(source) {
// 配置marked
marked.setOptions({
highlight: function(code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value;
}
return hljs.highlightAuto(code).value;
},
gfm: true,
breaks: true,
headerIds: true,
mangle: false
});
// 转换为HTML
const html = marked.parse(source);
// 返回模块代码
return `
export default \`${html}\`;
`;
};
使用:
javascript
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.md$/,
use: [
{
loader: path.resolve(__dirname, 'loaders/markdown-loader.js')
}
]
}
]
}
};
// 在代码中使用
import content from './readme.md';
console.log(content); // HTML字符串
5. 实战案例:国际化Loader
自动提取字符串进行国际化。
javascript
// loaders/i18n-loader.js
const { getOptions } = require('loader-utils');
module.exports = function(source) {
const options = getOptions(this);
const locale = options.locale || 'zh';
// 正则匹配 t('key') 格式
const regex = /t\(['"](.+?)['"]\)/g;
let result = source;
const keys = [];
// 提取所有key
let match;
while ((match = regex.exec(source)) !== null) {
keys.push(match[1]);
}
// 替换为对应的翻译
result = source.replace(regex, (match, key) => {
const translations = {
zh: {
'hello': '你好',
'welcome': '欢迎'
},
en: {
'hello': 'Hello',
'welcome': 'Welcome'
}
};
return translations[locale][key] || key;
});
return result;
};
6. 实战案例:CSS变量注入Loader
javascript
// loaders/css-vars-loader.js
const { getOptions } = require('loader-utils');
module.exports = function(source) {
const options = getOptions(this);
const vars = options.vars || {};
// 替换CSS变量
let result = source;
Object.keys(vars).forEach(key => {
const regex = new RegExp(`var\\(--${key}\\)`, 'g');
result = result.replace(regex, vars[key]);
});
return result;
};
使用:
javascript
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
{
loader: path.resolve(__dirname, 'loaders/css-vars-loader.js'),
options: {
vars: {
'primary-color': '#007bff',
'font-size-base': '14px'
}
}
}
]
}
]
}
};
7. 完整Loader开发指南
javascript
// loaders/advanced-loader.js
const { getOptions, stringifyRequest } = require('loader-utils');
const path = require('path');
module.exports = function(source, sourceMap) {
// 异步模式
const callback = this.async();
try {
// 获取配置
const options = getOptions(this) || {};
// Loader上下文
console.log('Loader Context:');
console.log('- Resource Path:', this.resourcePath);
console.log('- Resource Query:', this.resourceQuery);
console.log('- Context Directory:', this.context);
console.log('- Current Loader:', this.loaders[this.loaderIndex]);
// 源文件信息
console.log('Source Info:');
console.log('- Source Length:', source.length);
console.log('- Has Source Map:', !!sourceMap);
// 转换逻辑
let result = source;
// 示例:添加头部注释
if (options.addHeader) {
result = `// Compiled by Webpack\n${result}`;
}
// 示例:自定义处理
if (options.replace) {
result = result.replace(
new RegExp(options.replace.search, 'g'),
options.replace.replace
);
}
// 示例:添加源码映射
if (options.generateSourceMap && sourceMap) {
const consumer = new sourceMap.SourceMapConsumer(sourceMap);
consumer.then(consumerInstance => {
// 修改源码映射
consumerInstance._absolutePosition = true;
callback(null, result, sourceMap);
});
} else {
callback(null, result);
}
} catch (error) {
callback(error);
}
};
8. Loader工具函数
javascript
// loaders/utils.js
const { getOptions } = require('loader-utils');
const schemaUtils = require('schema-utils');
/**
* 获取Loader配置
*/
function getLoaderOptions(loaderName, schema) {
return function() {
const options = getOptions(this);
if (schema) {
schemaUtils.validate(schema, options, {
name: loaderName,
baseDataPath: 'options'
});
}
return options;
};
}
/**
* 路径解析
*/
function absolutify(context, request) {
const requestSplit = request.split('!');
const result = requestSplit.pop();
const prefix = requestSplit.length ? requestSplit.join('!') + '!' : '';
return prefix + path.posix.resolve(context, result);
}
/**
* 相对路径转换
*/
function relative(context, absolutePath) {
if (!path.isAbsolute(absolutePath)) {
return absolutePath;
}
return path.relative(context, absolutePath).replace(/\\/g, '/');
}
module.exports = {
getLoaderOptions,
absolutify,
relative
};
9. Loader测试
javascript
// loaders/__tests__/markdown-loader.test.js
const path = require('path');
const { runLoader } = require('webpack');
describe('Markdown Loader', () => {
test('should convert markdown to HTML', (done) => {
const loaderPath = path.resolve(__dirname, '../markdown-loader.js');
runLoader(
{
resource: path.resolve(__dirname, 'fixtures/test.md'),
resourcePath: path.resolve(__dirname, 'fixtures/test.md'),
context: {},
loaders: [loaderPath],
loaderIndex: 0,
value: '# Hello World',
options: {}
},
(err, result, sourceMap) => {
if (err) {
return done(err);
}
expect(result).toContain('<h1>Hello World</h1>');
done();
}
);
});
});
Plugin开发实战
Plugin是Webpack的核心特性,它扩展了Webpack的功能。Plugin可以在Webpack的整个生命周期中钩入,执行各种任务。
1. Plugin基础
Plugin是一个类,包含apply方法。
javascript
// plugins/basic-plugin.js
class BasicPlugin {
// 构造函数,接收配置
constructor(options = {}) {
this.options = options;
}
// apply方法在Webpack compiler安装此plugin时调用
apply(compiler) {
// 钩子列表:
// https://webpack.js.org/api/compiler-hooks/
// 示例:在emit钩子中执行
compiler.hooks.emit.tap('BasicPlugin', (compilation) => {
console.log('BasicPlugin: emit hook called');
console.log('Number of assets:', Object.keys(compilation.assets).length);
});
}
}
module.exports = BasicPlugin;
2. 使用Plugin
javascript
// webpack.config.js
const BasicPlugin = require('./plugins/basic-plugin');
module.exports = {
plugins: [
new BasicPlugin({
// 配置项
})
]
};
3. 实战案例:清理输出目录Plugin
javascript
// plugins/clean-output-plugin.js
const fs = require('fs');
const path = require('path');
class CleanOutputPlugin {
constructor(options = {}) {
this.options = {
exclude: [],
...options
};
}
apply(compiler) {
const outputPath = compiler.options.output.path;
compiler.hooks.beforeRun.tap('CleanOutputPlugin', (compiler) => {
if (fs.existsSync(outputPath)) {
console.log(`Cleaning output directory: ${outputPath}`);
const files = fs.readdirSync(outputPath);
files.forEach(file => {
const filePath = path.join(outputPath, file);
// 跳过排除的文件
if (this.options.exclude.includes(file)) {
return;
}
if (fs.statSync(filePath).isDirectory()) {
fs.rmSync(filePath, { recursive: true, force: true });
} else {
fs.unlinkSync(filePath);
}
});
}
});
}
}
module.exports = CleanOutputPlugin;
4. 实战案例:生成文件清单Plugin
javascript
// plugins/manifest-plugin.js
const fs = require('fs');
const path = require('path');
class ManifestPlugin {
constructor(options = {}) {
this.options = {
filename: 'manifest.json',
...options
};
}
apply(compiler) {
compiler.hooks.emit.tap('ManifestPlugin', (compilation) => {
const manifest = {
name: compiler.options.output.filename,
timestamp: new Date().toISOString(),
assets: [],
chunks: {}
};
// 收集资源文件信息
Object.keys(compilation.assets).forEach(assetName => {
const asset = compilation.assets[assetName];
const info = asset.info || {};
manifest.assets.push({
name: assetName,
size: asset.size(),
isCompressed: !!info.compressed,
sourceMap: !!info.sourceMap
});
});
// 收集chunk信息
Object.keys(compilation.chunks).forEach(chunkId => {
const chunk = compilation.chunks[chunkId];
manifest.chunks[chunkId] = {
files: Array.from(chunk.files),
hash: chunk.renderedHash,
size: chunk.size()
};
});
// 生成清单文件
const manifestJson = JSON.stringify(manifest, null, 2);
compilation.assets[this.options.filename] = {
source() {
return manifestJson;
},
size() {
return manifestJson.length;
}
};
});
}
}
module.exports = ManifestPlugin;
5. 实战案例:进度显示Plugin
javascript
// plugins/progress-plugin.js
class ProgressPlugin {
constructor(options = {}) {
this.options = {
activeModules: true,
entries: true,
profiles: true,
modules: true,
dependencies: true,
...options
};
this.lastModuleCount = 0;
this.startTime = Date.now();
}
apply(compiler) {
this.options;
// 编译开始
compiler const options =.hooks.beforeCompile.tap('ProgressPlugin', () => {
this.log('Starting compilation...');
});
// 模块处理
compiler.hooks.normalModuleFactory.tap('ProgressPlugin', (factory) => {
factory.hooks.beforeResolve.tap('ProgressPlugin', (data) => {
if (options.dependencies) {
this.log(`Resolving module: ${data.request}`);
}
});
});
// 模块完成
compiler.hooks.compilation.tap('ProgressPlugin', (compilation) => {
compilation.hooks.buildModule.tap('ProgressPlugin', (module) => {
if (options.modules) {
this.log(`Building module: ${module.resource}`);
}
});
// 进度统计
compilation.hooks.afterOptimizeModuleIds.tap('ProgressPlugin', (modules) => {
this.updateProgress(modules.length, 'modules');
});
});
// 打包完成
compiler.hooks.afterEmit.tap('ProgressPlugin', () => {
const duration = Date.now() - this.startTime;
this.log(`Compilation completed in ${duration}ms`);
});
}
log(message) {
console.log(`[Progress] ${message}`);
}
updateProgress(current, total, label) {
const percentage = Math.round((current / total) * 100);
const bar = '='.repeat(percentage / 2) + ' '.repeat(50 - percentage / 2);
process.stdout.write(`\r[Progress] [${bar}] ${percentage}% ${label || ''}`);
if (percentage === 100) {
console.log('');
}
}
}
module.exports = ProgressPlugin;
6. 实战案例:环境变量注入Plugin
javascript
// plugins/env-inject-plugin.js
const DefinePlugin = require('webpack/lib/DefinePlugin');
class EnvInjectPlugin {
constructor(options = {}) {
this.options = {
// 从环境变量获取
fromEnv: process.env.NODE_ENV,
// 自定义变量
custom: {},
// 条件注入
conditional: {},
...options
};
}
apply(compiler) {
const definitions = {};
// 注入环境变量
Object.keys(process.env).forEach(key => {
definitions[`process.env.${key}`] = JSON.stringify(process.env[key]);
});
// 注入自定义变量
Object.keys(this.options.custom).forEach(key => {
definitions[key] = JSON.stringify(this.options.custom[key]);
});
// 条件注入
Object.keys(this.options.conditional).forEach(key => {
const condition = this.options.conditional[key];
if (condition.enabled) {
definitions[key] = JSON.stringify(condition.value);
}
});
// 使用DefinePlugin
new DefinePlugin(definitions).apply(compiler);
}
}
module.exports = EnvInjectPlugin;
使用:
javascript
// webpack.config.js
const EnvInjectPlugin = require('./plugins/env-inject-plugin');
module.exports = {
plugins: [
new EnvInjectPlugin({
fromEnv: process.env.NODE_ENV,
custom: {
'process.env.APP_NAME': '"My App"',
'process.env.APP_VERSION': '"1.0.0"'
},
conditional: {
'process.env.DEBUG': {
enabled: process.env.NODE_ENV === 'development',
value: true
}
}
})
]
};
7. 实战案例:自动复制文件Plugin
javascript
// plugins/copy-files-plugin.js
const fs = require('fs');
const path = require('path');
class CopyFilesPlugin {
constructor(options = {}) {
this.options = {
patterns: [],
...options
};
}
apply(compiler) {
compiler.hooks.emit.tap('CopyFilesPlugin', async (compilation) => {
for (const pattern of this.options.patterns) {
const { from, to, flatten = false } = pattern;
const srcPath = path.resolve(from);
const destPath = path.resolve(to);
if (!fs.existsSync(srcPath)) {
console.warn(`Source file not found: ${srcPath}`);
continue;
}
const fileName = flatten ? path.basename(srcPath) : srcPath.replace(path.dirname(srcPath), '');
const finalDest = path.join(destPath, fileName);
// 复制文件
const content = fs.readFileSync(srcPath);
compilation.assets[path.relative(compiler.options.output.path, finalDest)] = {
source() {
return content;
},
size() {
return content.length;
}
};
console.log(`Copied: ${srcPath} -> ${finalDest}`);
}
});
}
}
module.exports = CopyFilesPlugin;
8. 实战案例:Bundle分析Plugin
javascript
// plugins/bundle-analyzer-plugin.js
class BundleAnalyzerPlugin {
constructor(options = {}) {
this.options = {
analyzerMode: 'static',
analyzerPort: 8888,
openAnalyzer: true,
...options
};
}
apply(compiler) {
compiler.hooks.done.tap('BundleAnalyzerPlugin', (stats) => {
this.analyzeBundle(stats);
});
}
analyzeBundle(stats) {
const compilation = stats.compilation;
console.log('\n=== Bundle Analysis ===\n');
// 总览信息
console.log('Bundle Size:');
this.printSize('Total', compilation.hash);
// 各chunk大小
console.log('\nChunks:');
compilation.chunks.forEach(chunk => {
const files = Array.from(chunk.files);
files.forEach(file => {
const asset = compilation.assets[file];
console.log(` - ${chunk.name}: ${this.formatSize(asset.size())}`);
});
});
// 模块统计
console.log('\nTop 10 Modules by Size:');
const modules = Array.from(compilation.modules);
const sortedModules = modules
.filter(module => module.size)
.sort((a, b) => b.size() - a.size())
.slice(0, 10);
sortedModules.forEach(module => {
console.log(` - ${module.name}: ${this.formatSize(module.size())}`);
});
// 依赖统计
console.log('\nDependencies:');
console.log(` - Total modules: ${modules.length}`);
console.log(` - Total chunks: ${compilation.chunks.length}`);
// 警告和错误
if (stats.hasErrors()) {
console.log('\n❌ Errors:', stats.compilation.errors.length);
}
if (stats.hasWarnings()) {
console.log('\n⚠️ Warnings:', stats.compilation.warnings.length);
}
}
printSize(label, value) {
console.log(` ${label}: ${this.formatSize(value)}`);
}
formatSize(bytes) {
if (bytes < 1024) {
return `${bytes} B`;
} else if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(2)} KB`;
} else {
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}
}
}
module.exports = BundleAnalyzerPlugin;
9. 高级Plugin开发
javascript
// plugins/advanced-plugin.js
class AdvancedPlugin {
constructor(options = {}) {
this.options = options;
this.modules = new Map();
}
apply(compiler) {
const pluginName = 'AdvancedPlugin';
// 访问NormalModuleFactory
compiler.hooks.normalModuleFactory.tap(pluginName, (nmf) => {
nmf.hooks.beforeResolve.tap(pluginName, (resolveData) => {
console.log(`Resolving module: ${resolveData.request}`);
return resolveData;
});
nmf.hooks.afterResolve.tap(pluginName, (resolveData) => {
console.log(`Resolved module: ${resolveData.resource}`);
return resolveData;
});
});
// 访问Compilation
compiler.hooks.compilation.tap(pluginName, (compilation, params) => {
// 访问ModuleFactory
compilation.hooks.buildModule.tap(pluginName, (module) => {
this.trackModule(module);
});
compilation.hooks.rebuildModule.tap(pluginName, (module) => {
this.trackModule(module);
});
compilation.hooks.succeedModule.tap(pluginName, (module) => {
this.onModuleSucceed(module);
});
compilation.hooks.failedModule.tap(pluginName, (module) => {
this.onModuleFailed(module);
});
});
// 钩子类型示例
compiler.hooks.entryOption.tap(pluginName, (context, entry) => {
console.log('Entry option:', entry);
});
compiler.hooks.afterPlugins.tap(pluginName, () => {
console.log('After plugins loaded');
});
compiler.hooks.beforeRun.tap(pluginName, (compiler) => {
console.log('Before run');
});
compiler.hooks.run.tap(pluginName, (compiler) => {
console.log('Run');
});
compiler.hooks.watchRun.tap(pluginName, (compiler) => {
console.log('Watch run');
});
compiler.hooks.make.tap(pluginName, (compilation) => {
console.log('Make');
});
compiler.hooks.afterCompile.tap(pluginName, (compilation) => {
console.log('After compile');
});
compiler.hooks.emit.tap(pluginName, (compilation) => {
console.log('Emit');
this.onEmit(compilation);
});
compiler.hooks.afterEmit.tap(pluginName, (compilation) => {
console.log('After emit');
});
compiler.hooks.done.tap(pluginName, (stats) => {
console.log('Done');
this.onDone(stats);
});
compiler.hooks.failed.tap(pluginName, (error) => {
console.log('Failed:', error);
});
}
trackModule(module) {
this.modules.set(module.identifier(), {
startTime: Date.now(),
size: module.size()
});
}
onModuleSucceed(module) {
const moduleInfo = this.modules.get(module.identifier());
if (moduleInfo) {
moduleInfo.endTime = Date.now();
moduleInfo.duration = moduleInfo.endTime - moduleInfo.startTime;
}
}
onModuleFailed(module) {
const moduleInfo = this.modules.get(module.identifier());
if (moduleInfo) {
moduleInfo.failed = true;
}
}
onEmit(compilation) {
// 添加自定义资源
compilation.assets['custom-asset.txt'] = {
source() {
return 'This is a custom asset';
},
size() {
return 'This is a custom asset'.length;
}
};
}
onDone(stats) {
console.log('=== Build Stats ===');
console.log('Time:', stats.endTime - stats.startTime);
console.log('Modules:', this.modules.size);
}
}
module.exports = AdvancedPlugin;
10. 插件间通信
javascript
// plugins/plugin-communication.js
// 插件1:收集模块信息
class ModuleCollectorPlugin {
constructor() {
this.modules = [];
}
apply(compiler) {
compiler.hooks.compilation.tap('ModuleCollectorPlugin', (compilation) => {
compilation.hooks.succeedModule.tap('ModuleCollectorPlugin', (module) => {
this.modules.push({
identifier: module.identifier(),
name: module.name,
size: module.size()
});
// 通过compilation实例传递数据
compilation.pluginInfo = compilation.pluginInfo || {};
compilation.pluginInfo.modules = this.modules;
});
});
}
}
// 插件2:使用模块信息
class ModuleReporterPlugin {
apply(compiler) {
compiler.hooks.done.tap('ModuleReporterPlugin', (stats) => {
const compilation = stats.compilation;
const modules = compilation.pluginInfo?.modules || [];
console.log('=== Module Report ===');
console.log(`Total modules: ${modules.length}`);
if (modules.length > 0) {
console.log('Largest module:', modules.reduce((a, b) => a.size > b.size ? a : b).name);
}
});
}
}
module.exports = {
ModuleCollectorPlugin,
ModuleReporterPlugin
};
高级配置与优化
1. 代码分割详解
javascript
module.exports = {
optimization: {
splitChunks: {
// 分离策略
chunks: 'all', // 'initial', 'async', 'all'
// 缓存组
cacheGroups: {
// 第三方库
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
// React相关
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
chunks: 'all',
priority: 20
},
// 大型库
'large-libs': {
test: /[\\/]node_modules[\\/](lodash|axios|moment)[\\/]/,
name: 'large-libs',
chunks: 'all',
minSize: 20000,
maxSize: 244000,
priority: 5
},
// 公共模块
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 0,
reuseExistingChunk: true
}
},
// 分割条件
maxInitialRequests: Infinity,
minSize: 20000,
maxSize: 244000,
minChunks: 1,
// 自动命名
automaticNameDelimiter: '~',
// 最大异步请求数
maxAsyncRequests: 5
},
// 运行时提取
runtimeChunk: {
name: 'runtime'
},
// 模块标识符
moduleIds: 'deterministic',
// Chunk标识符
chunkIds: 'deterministic',
// 压缩配置
minimizer: [
new TerserPlugin({
extractComments: false,
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
},
mangle: true,
format: {
comments: false
}
}
})
]
}
};
2. 缓存配置
javascript
module.exports = {
// 内存缓存
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
},
cacheDirectory: path.resolve(__dirname, '.cache'),
cacheLocation: path.resolve(__dirname, '.cache/webpack'),
name: 'my-app-cache',
compression: 'gzip'
},
// 持久化缓存
infrastructureLogging: {
level: 'info'
},
// 缓存组
module: {
unknownContextModules: false,
exprContextRecursive: true,
wrappedContextCritical: true
}
};
3. 高级Loader配置
javascript
module.exports = {
module: {
rules: [
{
test: /\.js$/,
oneOf: [
{
resourceQuery: '?inline',
use: 'url-loader'
},
{
test: /\.svg$/,
use: [
'file-loader',
{
loader: 'svgo-loader',
options: {
plugins: [
{
name: 'removeViewBox',
active: false
},
{
name: 'removeEmptyAttrs',
active: false
}
]
}
}
]
}
]
},
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[name]__[local]___[hash:base64:5]'
}
}
]
}
]
}
};
4. 多页面应用配置
javascript
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: {
home: './src/pages/home/index.js',
about: './src/pages/about/index.js',
contact: './src/pages/contact/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash].js',
clean: true
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css'
}),
new HtmlWebpackPlugin({
template: './src/pages/home/index.html',
filename: 'home.html',
chunks: ['home', 'runtime', 'vendors']
}),
new HtmlWebpackPlugin({
template: './src/pages/about/index.html',
filename: 'about.html',
chunks: ['about', 'runtime', 'vendors']
}),
new HtmlWebpackPlugin({
template: './src/pages/contact/index.html',
filename: 'contact.html',
chunks: ['contact', 'runtime', 'vendors']
})
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
}
}
},
runtimeChunk: 'single'
}
};
5. TypeScript支持
javascript
module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true, // 加快编译速度
getCustomTransformers: () => ({
before: [require.resolve('typescript-plugin-styled-components').default]
})
}
}
]
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
'@': path.resolve(__dirname, 'src')
}
}
};
6. React支持
javascript
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: {
browsers: ['> 1%', 'last 2 versions']
}
}],
['@babel/preset-react', {
runtime: 'automatic' // 自动导入React
}]
],
plugins: [
'@babel/plugin-transform-runtime',
'@babel/plugin-proposal-class-properties'
]
}
}
}
]
}
};
性能优化策略
1. 构建速度优化
javascript
module.exports = {
// 1. 使用多进程
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1
}
},
'babel-loader'
]
}
]
},
// 2. 缓存
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(__dirname, '.cache'),
buildDependencies: {
config: [__filename]
}
},
// 3. 并行压缩
optimization: {
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
})
]
},
// 4. 排除不必要的处理
module: {
noParse: /^(jquery|chart\.js)$/
},
// 5. 排除node_modules
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
}
};
2. 包体积优化
javascript
module.exports = {
optimization: {
// 1. Tree Shaking
usedExports: true,
sideEffects: false,
// 2. 代码分割
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true
}
}
},
// 3. 压缩
minimize: true,
// 4. 移除未使用代码
removeEmptyChunks: true,
mergeDuplicateChunks: true
},
// 5. 分析包大小
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false
})
]
};
3. 运行时优化
javascript
module.exports = {
optimization: {
// 提取公共运行时
runtimeChunk: 'single',
// 模块标识符
moduleIds: 'deterministic',
// Chunk标识符
chunkIds: 'deterministic',
// 命名
namedChunks: 'namedModules',
namedModules: true
}
};
多场景实战配置
1. Vue项目配置
javascript
// webpack.vue.config.js
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash].js',
clean: true
},
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
]
},
{
test: /\.scss$/,
use: [
'vue-style-loader',
'css-loader',
'sass-loader'
]
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html'
})
],
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
'vue$': 'vue/dist/vue.esm.js'
}
}
};
2. 小程序项目配置
javascript
// webpack.miniprogram.config.js
const path = require('path');
const MiniProgramPlugin = require('webpack-miniprogram-plugin');
module.exports = {
entry: './src/app.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
clean: true
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
},
'miniprogram-babel-loader'
]
},
{
test: /\.wxml$/,
use: 'wxml-loader'
},
{
test: /\.wxss$/,
use: [
{
loader: 'wxss-loader',
options: {
ex: 'rpx'
}
}
]
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: 'images/[name].[ext]'
}
}
]
}
]
},
plugins: [
new MiniProgramPlugin({
appJSON: './app.json',
pageJSON: './pages/**/*.json'
})
]
};
3. Electron应用配置
javascript
// webpack.electron.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const config = {
main: {
entry: './electron/main.js',
output: {
path: path.resolve(__dirname, 'dist/main'),
filename: 'main.js'
},
target: 'electron-main',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
},
renderer: {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist/renderer'),
filename: 'renderer.js'
},
target: 'electron-renderer',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
}
};
module.exports = config;
原理与源码分析
1. Webpack执行流程
javascript
// Webpack执行步骤(简化版)
// 1. 初始化配置
const compiler = new Compiler(options);
loadConfig(options, compiler);
// 2. 加载插件
loadPlugins(pluginFactory, compiler);
// 3. 编译
compiler.run((err, stats) => {
if (err) {
console.error(err);
return;
}
if (stats.hasErrors()) {
console.error(stats.toString({
colors: true,
chunks: false
}));
return;
}
console.log(stats.toString({
chunks: false,
colors: true
}));
});
// 编译流程
compiler.compile() {
// 3.1 创建Compilation
const compilation = new Compilation(compiler);
// 3.2 构建依赖图
compilation.buildModule();
// 3.3 优化
compilation.optimize();
// 3.4 生成输出
compilation.createChunkAssets();
}
2. Tapable钩子系统
javascript
// Tapable是Webpack的核心,用于实现钩子系统
const { Tapable } = require('tapable');
// Hook类示例
class SyncHook {
constructor() {
this.taps = [];
}
tap(name, fn) {
this.taps.push({ name, fn });
}
call(...args) {
this.taps.forEach(tap => {
tap.fn(...args);
});
}
}
// 使用
const hook = new SyncHook();
hook.tap('plugin1', (arg) => {
console.log('Plugin 1:', arg);
});
hook.tap('plugin2', (arg) => {
console.log('Plugin 2:', arg);
});
hook.call('test');
// 输出:
// Plugin 1: test
// Plugin 2: test
// 异步钩子
class AsyncSeriesHook {
constructor() {
this.taps = [];
}
tapAsync(name, fn) {
this.taps.push({ name, fn });
}
callAsync(...args, callback) {
let index = 0;
const next = () => {
if (index >= this.taps.length) {
callback();
return;
}
const tap = this.taps[index++];
tap.fn(...args, next);
};
next();
}
}
3. 自定义Compiler
javascript
const { Compiler } = require('webpack');
const { Tapable } = require('tapable');
class CustomCompiler extends Compiler {
constructor() {
super();
// 自定义钩子
this.hooks = Object.assign(
Object.create(Tapable.prototype),
Compiler.prototype.hooks,
{
customHook: new SyncHook(['params'])
}
);
}
run(callback) {
// 自定义编译逻辑
const compilation = this.newCompilation();
// 调用自定义钩子
this.hooks.customHook.call(compilation.params);
// 完成编译
this.compile();
callback(null, {
compilation,
stats: this.createStats()
});
}
}
常见问题与解决方案
1. 构建速度慢
问题:构建时间过长,影响开发效率
解决方案:
javascript
// 方案1:使用缓存
module.exports = {
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(__dirname, '.cache'),
buildDependencies: {
config: [__filename]
}
}
};
// 方案2:并行处理
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1
}
},
'babel-loader'
]
}
]
}
};
// 方案3:exclude优化
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/, // 排除大型库
use: 'babel-loader'
}
]
}
};
// 方案4:noParse
module.exports = {
module: {
noParse: /jquery|chart\.js/ // 不解析某些库
}
};
2. 内存溢出
问题:构建时内存溢出
解决方案:
javascript
// 方案1:增加Node.js内存限制
// package.json
{
"scripts": {
"build": "node --max_old_space_size=4096 node_modules/.bin/webpack --mode production"
}
}
// 方案2:减少并行度
module.exports = {
parallelism: 1 // 限制并行处理的chunk数量
};
3. 热更新失效
问题:热更新不生效
解决方案:
javascript
// 检查HMR配置
module.exports = {
devServer: {
hot: true,
hotOnly: false
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
};
// 检查Loader支持
// babel-loader需要react-refresh
{
loader: 'babel-loader',
options: {
plugins: ['react-refresh/babel']
}
}
4. 动态导入错误
问题:import()语法报错
解决方案:
javascript
// 确保有正确的分隔符
// 错误
import('./pages/' + pageName)
// 正确
import('./pages/' + pageName + '.js')
// 或使用require.context
const files = require.context('./pages', false, /\.js$/);
const modules = {};
files.keys().forEach(key => {
modules[key.replace('./', '')] = files(key);
});
5. CSS重复引入
问题:CSS被多次打包
解决方案:
javascript
// 方案1:使用ExtractTextPlugin
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader'
})
}
]
}
};
// 方案2:确保模块ID正确
module.exports = {
optimization: {
namedModules: true
}
};
6. Source Map错误
问题:源码映射不正确
解决方案:
javascript
// 开发环境
module.exports = {
devtool: 'eval-cheap-module-source-map'
};
// 生产环境
module.exports = {
devtool: 'source-map'
};
// 禁用
module.exports = {
devtool: false
};
最佳实践总结
1. 配置建议
javascript
// ✅ 好的做法
module.exports = {
// 使用函数配置,支持环境变量
mode: (process.env.NODE_ENV || 'development'),
// 使用路径别名
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components')
}
},
// 清理输出目录
output: {
clean: true
},
// 使用contenthash缓存
output: {
filename: '[name].[contenthash].js'
},
// 代码分割
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
// ❌ 避免的做法
module.exports = {
// 硬编码路径
entry: '/absolute/path/to/src/index.js',
// 忘记清理
output: {
clean: false
},
// 禁用缓存优化
optimization: {
minimize: false
}
};
2. Loader使用建议
javascript
// ✅ 使用Loader配置对象
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
// ✅ 排除node_modules
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
// ✅ 使用绝对路径
{
loader: path.resolve(__dirname, 'loaders/custom-loader.js')
}
3. Plugin使用建议
javascript
// ✅ 条件性加载Plugin
const plugins = [
new HtmlWebpackPlugin()
];
if (process.env.ANALYZE) {
plugins.push(new BundleAnalyzerPlugin());
}
// ✅ 使用实例缓存
const instanceName = new SomePlugin({
instance: 'production'
});
4. 性能优化建议
javascript
// ✅ 缓存配置
module.exports = {
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(__dirname, '.cache')
}
};
// ✅ 并行处理
module.exports = {
parallelism: 10
};
// ✅ 合理拆分
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
};
5. 调试建议
javascript
// ✅ 使用Stats查看构建信息
{
"scripts": {
"build": "webpack --mode production --stats verbose"
}
}
// ✅ 分析包大小
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer');
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false
})
]
// ✅ 查看模块依赖
{
"scripts": {
"analyze": "npm run build && npx webpack-bundle-analyzer dist/main.js"
}
}
6. 项目结构建议
project/
├── public/
│ └── index.html
├── src/
│ ├── index.js
│ ├── components/
│ ├── utils/
│ └── assets/
├── webpack.config.js # 通用配置
├── webpack.dev.js # 开发环境
├── webpack.prod.js # 生产环境
└── package.json
总结
本文全面深入地讲解了Webpack的配置与实战,包括:
核心内容回顾
- 基础概念:Entry、Output、Module、Resolve、Plugin、Mode
- 基础配置:开发/生产环境配置详解
- Loader开发:从基础到高级,5个实战案例
- Plugin开发:从简单到复杂,8个实战案例
- 高级配置:代码分割、缓存、多页面应用等
- 性能优化:构建速度与包体积优化策略
- 多场景实战:Vue、小程序、Electron等
- 原理分析:执行流程、钩子系统
- 问题解决:常见问题与最佳实践
关键要点
- Loader是函数,用于转换模块源码
- Plugin是类,用于扩展Webpack功能
- 合理配置代码分割提升性能
- 缓存是提升构建速度的关键
- 根据项目选择合适的配置策略