1、基础
2、相关配置
3、手写 Loader 和 Plugin
如何编写一个自定义的 Webpack Loader?
案例:自动提取代码中的中文文案并生成多语言映射文件
/**
* webpack 中的配置
*/
module.exports = {
module: {
rules: [
{
test: /\.(jsx|tsx)$/,
use: [
'babel-loader',
{
loader: './i18n-loader',
options: { outputDir: 'src/locales' }
}
]
}
]
}
};
/**
* i18n-loader
*/
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
module.exports = function(source) {
// 异步
const callback = this.async();
const resourcePath = this.resourcePath;
const outputDir = path.resolve(process.cwd(), 'locales');
const messages = {};
// 解析 JSX/TSX 文件
const ast = parser.parse(source, {
sourceType: 'module',
plugins: ['jsx', 'typescript']
});
traverse(ast, {
// 字符串字面量的节点:const str = "你好世界";
StringLiteral(path) {
const { value } = path.node;
// value 如果为中文,存储在 messages 中
if (/[\u4e00-\u9fa5]/.test(value)) {
messages[value] = '';
}
},
// 本节点的类型:<p>你好,世界!</p>
JSXText(path) {
const value = path.node.value.trim();
if (value && /[\u4e00-\u9fa5]/.test(value)) {
messages[value] = '';
}
}
});
// 生成语言包文件
if (Object.keys(messages).length > 0) {
const filename = path.basename(resourcePath, path.extname(resourcePath));
const outputPath = path.join(outputDir, `${filename}.json`);
fs.mkdirSync(outputDir, { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(messages, null, 2));
}
callback(null, source);
};
自动为图片资源添加CDN前缀并生成WebP格式的Webpack
/**
* webpack 中的配置
*/
.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g)$/,
use: [
{
loader: './cdn-webp-loader',
options: {
cdn: 'https://your-cdn.com/assets',
quality: 80
}
}
]
}
]
}
};
/**
* cdn-webp
*/
const path = require('path');
const sharp = require('sharp');
const loaderUtils = require('loader-utils');
module.exports = async function(content) {
const callback = this.async();
const options = loaderUtils.getOptions(this) || {};
const cdnBase = options.cdn || 'https://cdn.example.com';
const quality = options.quality || 75;
try {
// 生成WebP格式
const webpBuffer = await sharp(content)
.webp({ quality })
.toBuffer();
// 生成CDN路径
const filename = loaderUtils.interpolateName(
this,
'[contenthash].[ext]',
{ content: webpBuffer }
);
const publicPath = `${cdnBase}/${filename}`;
// 输出文件到dist目录
this.emitFile(filename, webpBuffer);
callback(null, `module.exports = "${publicPath}";`);
} catch (err) {
callback(err);
}
};
module.exports.raw = true; // 获取原始Buffer
如何编写一个自定义的 Webpack Plugin?
自动清理旧构建产物并保留指定版本
/**
* webpack 配置
*/
const CleanHistoryPlugin = require('./CleanHistoryPlugin');
module.exports = {
plugins: [
new CleanHistoryPlugin({
maxVersions: 3,
pattern: /^build-\d{8}/ // 匹配build-20250101格式
})
]
};
/**
* CleanHistoryPlugin
*/
const fs = require('fs');
const path = require('path');
class CleanHistoryPlugin {
constructor(options = {}) {
this.maxVersions = options.maxVersions || 5;
this.outputPath = options.outputPath || 'dist';
this.pattern = options.pattern || /^app-\d+\.\d+\.\d+/;
}
apply(compiler) {
compiler.hooks.afterEmit.tap('CleanHistoryPlugin', () => {
const files = fs.readdirSync(this.outputPath);
const matched = files.filter(f => this.pattern.test(f));
if (matched.length > this.maxVersions) {
matched
.sort()
.slice(0, matched.length - this.maxVersions)
.forEach(f => {
fs.rmSync(path.join(this.outputPath, f), { recursive: true });
console.log(`Cleaned old version: ${f}`);
});
}
});
}
}
module.exports = CleanHistoryPlugin;
自动清除console.log的Webpack插件实现
/**
* webpack 配置
*/
ConsoleCleanPlugin = require('./ConsoleCleanPlugin');
module.exports = {
mode: 'production',
plugins: [
new ConsoleCleanPlugin()
]
};
/**
* ConsoleCleanPlugin
*/
const { ConcatSource } = require('webpack-sources');
class ConsoleCleanPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('ConsoleCleanPlugin', compilation => {
compilation.hooks.processAssets.tap({
name: 'ConsoleCleanPlugin',
stage: compilation.PROCESS_ASSETS_STAGE_ADDITIONS
}, assets => {
Object.keys(assets).forEach(filename => {
if (/\.js$/.test(filename)) {
let source = assets[filename].source();
source = source.replace(/console\.log\(.*?\);?/g, '');
compilation.updateAsset(
filename,
new ConcatSource(source)
);
}
});
});
});
}
}
module.exports = ConsoleCleanPlugin;
4、性能优化
- 通用环境优化
- include/exclude 限定 Loader 作用范围
- 持久化缓存(Webpack 5 内置支持)
- 多进程/多实例构建
- 代码拆分与 Tree Shaking
- 动态导入(懒加载)
- 开发环境优化
- 热更新(HMR)
- 缓存
- 模块热替换(HMR)
- Source Map 优化
- 优化 Source Map
- 避免生产环境工具(移除
TerserPlugin、[hash]等生产环境专用配置) - DLL 预编译(大型项目)
- 跳过大型库处理
- 生产环境专项优化
- 代码压缩
- 长效缓存策略
- 按需加载 Polyfill
- 代码分割
- 压缩
- 长效缓
- Tree Shaking。
- 跳过解析与预编译
noParse忽略已模块化库DllPlugin预编译静态库
- 代码分割(Code Splitting)
- 动态导入(懒加载)
- 通过
import()实现按需加载,减少首屏体积37。
- 通过
- Webpack 5 新特性优化
- 模块联邦(Module Federation)
- 资源模块(Asset Modules)
- 构建性能提升
- 持久化缓存
- 改进的 Tree Shaking
- 更优的 Chunk 分割算法
4.3 生产环境优化(开发/生产均适用)
代码分割 (Code Splitting)
Tree Shaking (移除未使用代码)
使用缓存 (cache-loader, hard-source-webpack-plugin)
缩小文件搜索范围 (resolve.modules, resolve.extensions)
多线程/并行构建 (thread-loader, happypack)
使用 DLLPlugin 预编译不常变化的模块
如何提高 Webpack 的构建速度?
- 使用
speed-measure-webpack-plugin分析构建速度 - 减少 loader/plugin 的使用
- 使用
cache-loader或hard-source-webpack-plugin缓存 - 使用
thread-loader或happypack多线程构建 - 使用
DllPlugin预编译不常变化的模块
5、一些包的原理
Webpack 的 HMR 原理
6、源码
Webpack 的构建流程是一个串行的事件流,其核心阶段可分为以下几个关键环节
-
初始化阶段(Preparation)
- beforeRun:清除缓存,准备编译器环境。
- run:启动编译,合并配置参数,创建 Compiler 实例。
- initialize/environment:注册内置插件(如 EntryOptionPlugin),解析入口文件配置
钩子作用:适合动态修改入口配置或注入全局变量
-
编译阶段(Compilation)
- compile:创建 Compilation 对象(管理模块依赖、资源生成的核心实例)。
- make(核心依赖分析阶段)
- 调用 ModuleFactory(如 NormalModuleFactory)创建模块对象
- buildModule:对每个模块应用 Loader 转换源码,生成 AST 并解析依赖。
- succeedModule/finishModules:模块构建完成后触发,标记依赖图完成。
- afterCompile:模块依赖图构建完毕,进入优化阶段。
性能瓶颈:此阶段最耗时,因需递归处理所有模块
-
优化与封装阶段(Sealing)
- seal:不可再修改模块依赖关系,执行 Chunk 优化:
- 拆分代码块(Code Splitting)。
- Tree Shaking 移除未用代码
- 合并重复模块,生成 Chunk 对象和运行时(Runtime)逻辑
钩子作用:适合代码压缩或自定义 Chunk 策略(如分包优化)
- seal:不可再修改模块依赖关系,执行 Chunk 优化:
-
输出阶段(Emission)
- emit(生成资源到内存):生成资源文件(如 Bundle.js),可修改输出内容
- afterEmit:资源已写入内存,准备输出到磁盘,适合清理临时文件
钩子作用:插件可在此阶段干预最终输出(如生成报告)
-
完成阶段(Completion)
- done:编译成功时触发,生成 stats 对象(含耗时、资源大小等数据)
- failed:编译失败时处理错误
钩子作用:适合分析构建报告或发送通知