编写自定义 Plugin
约 1235 字大约 4 分钟
webpackPlugin自定义
2026-04-15
Plugin 基础概念
什么是 Plugin
Plugin 是 Webpack 的扩展机制,用于在构建流程的各个阶段执行自定义逻辑。
核心特点:
- Plugin 是一个类或函数
- 必须实现
apply方法 - 通过 Tapable 钩子与 Webpack 交互
- 可以访问编译过程的每个阶段
Plugin vs Loader 的区别
| 特性 | Loader | Plugin |
|---|---|---|
| 职责 | 转换文件内容 | 执行范围更广的任务 |
| 执行时机 | 模块加载时 | 整个构建流程 |
| 作用域 | 单个文件 | 整个编译过程 |
| 实现 | 函数 | 类(带 apply 方法) |
Tapable 事件流
Webpack 使用 Tapable 实现插件系统:
// Plugin 通过 tap 方法注册到钩子上
compiler.hooks.emit.tap('MyPlugin', (compilation) => {
console.log('开始输出文件...');
});Plugin 开发 API
基本结构
class MyPlugin {
apply(compiler) {
// 在这里注册钩子
}
}
module.exports = MyPlugin;Compiler 和 Compilation 对象
Compiler:Webpack 运行的整个生命周期
class MyPlugin {
apply(compiler) {
console.log('Webpack 选项:', compiler.options);
console.log('输出路径:', compiler.outputPath);
}
}Compilation:一次新的编译过程
class MyPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
console.log('模块数量:', compilation.modules.length);
console.log('chunk 数量:', compilation.chunks.length);
});
}
}常用钩子列表
Compiler 钩子:
run:开始编译compile:创建 compilation 之前compilation:创建 compilationemit:输出文件到 output 目录之前afterEmit:输出文件之后done:编译完成
Compilation 钩子:
buildModule:构建模块之前succeedModule:构建模块成功finishModules:所有模块构建完成seal:开始封装 chunkoptimize:优化 chunk
实战:手写 html-webpack-plugin
分析源码结构
html-webpack-plugin 的核心功能:
- 读取 HTML 模板
- 注入打包后的 JS/CSS 文件
- 输出 HTML 文件
基础版本实现
// simple-html-webpack-plugin.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
class SimpleHtmlWebpackPlugin {
constructor(options = {}) {
this.options = {
template: options.template || 'index.html',
filename: options.filename || 'index.html',
inject: options.inject !== false,
title: options.title || 'Webpack App',
...options
};
}
apply(compiler) {
// 在 emit 阶段生成 HTML
compiler.hooks.emit.tapAsync(
'SimpleHtmlWebpackPlugin',
(compilation, callback) => {
this.generateHtml(compilation, callback);
}
);
}
generateHtml(compilation, callback) {
const { template, filename, inject, title } = this.options;
// 获取所有 chunk
const chunks = compilation.chunks;
// 读取模板
const templatePath = template.startsWith('/')
? template
: `${compiler.options.context}/${template}`;
let html = '';
try {
html = compilation.assets[template]
? compilation.assets[template].source()
: this.getDefaultTemplate(title);
} catch (err) {
html = this.getDefaultTemplate(title);
}
// 注入 JS 和 CSS
if (inject) {
html = this.injectAssets(html, compilation);
}
// 输出 HTML 文件
compilation.assets[filename] = {
source: () => html,
size: () => html.length
};
callback();
}
injectAssets(html, compilation) {
// 注入 CSS
let cssLinks = '';
compilation.chunks.forEach(chunk => {
chunk.files
.filter(file => file.endsWith('.css'))
.forEach(file => {
cssLinks += `<link href="${file}" rel="stylesheet">\\n`;
});
});
// 注入 JS
let scriptLinks = '';
compilation.chunks.forEach(chunk => {
chunk.files
.filter(file => file.endsWith('.js'))
.forEach(file => {
scriptLinks += `<script src="${file}"></script>\\n`;
});
});
// 替换模板中的占位符
html = html.replace('</head>', `${cssLinks}</head>`);
html = html.replace('</body>', `${scriptLinks}</body>`);
return html;
}
getDefaultTemplate(title) {
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${title}</title>
</head>
<body>
<div id="app"></div>
</body>
</html>`;
}
}
module.exports = SimpleHtmlWebpackPlugin;支持 template 配置
// 读取真实模板文件
const fs = require('fs');
const path = require('path');
class SimpleHtmlWebpackPlugin {
generateHtml(compilation, callback) {
const { template } = this.options;
if (template) {
const templatePath = path.resolve(
compiler.options.context || process.cwd(),
template
);
// 将模板文件添加到 compilation
if (fs.existsSync(templatePath)) {
const templateContent = fs.readFileSync(templatePath, 'utf-8');
compilation.assets[template] = {
source: () => templateContent,
size: () => templateContent.length
};
}
}
// ... 其余逻辑
}
}实战:手写清理构建目录插件
类似 clean-webpack-plugin 的实现:
// clean-webpack-plugin.js
const fs = require('fs');
const path = require('path');
class CleanWebpackPlugin {
constructor(options = {}) {
this.options = {
cleanOnceBeforeBuildPatterns: ['**/*', '!.*'],
...options
};
}
apply(compiler) {
compiler.hooks.emit.tapAsync(
'CleanWebpackPlugin',
(compilation, callback) => {
const outputPath = compiler.options.output.path;
if (fs.existsSync(outputPath)) {
this.cleanDir(outputPath);
}
callback();
}
);
}
cleanDir(dir) {
const files = fs.readdirSync(dir);
files.forEach(file => {
// 跳过隐藏文件
if (file.startsWith('.')) return;
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
// 递归删除子目录
this.cleanDir(filePath);
fs.rmdirSync(filePath);
} else {
fs.unlinkSync(filePath);
}
});
console.log(`已清理目录: ${dir}`);
}
}
module.exports = CleanWebpackPlugin;实战:手写版权信息插件
在打包结束后添加版权信息:
// copyright-plugin.js
const webpack = require('webpack');
class CopyrightWebpackPlugin {
constructor(options = {}) {
this.options = {
author: options.author || 'Unknown',
license: options.license || 'MIT',
filename: options.filename || 'COPYRIGHT.txt',
...options
};
}
apply(compiler) {
compiler.hooks.emit.tapAsync(
'CopyrightWebpackPlugin',
(compilation, callback) => {
const { author, license, filename } = this.options;
// 生成版权信息
const copyright = `
Copyright (c) ${new Date().getFullYear()} ${author}
License: ${license}
Generated by Webpack at ${new Date().toLocaleString()}
Bundled Files:
${this.getFileList(compilation)}
`.trim();
// 添加到输出文件
compilation.assets[filename] = {
source: () => copyright,
size: () => copyright.length
};
callback();
}
);
}
getFileList(compilation) {
return Object.keys(compilation.assets)
.map(file => ` - ${file}`)
.join('\n');
}
}
module.exports = CopyrightWebpackPlugin;使用方式
// webpack.config.js
const CopyrightWebpackPlugin = require('./copyright-plugin');
module.exports = {
plugins: [
new CopyrightWebpackPlugin({
author: 'ZhenYu',
license: 'MIT'
})
]
};Plugin 调试技巧
1. 添加 console.log
class MyPlugin {
apply(compiler) {
console.log('Plugin 已加载');
compiler.hooks.compile.tap('MyPlugin', () => {
console.log('开始编译...');
});
}
}2. 使用 Node.js 调试器
node --inspect-brk node_modules/.bin/webpack3. 查看 Compilation 对象
compiler.hooks.emit.tap('MyPlugin', (compilation) => {
console.log('模块:', compilation.modules);
console.log('Chunks:', compilation.chunks);
console.log('资源:', Object.keys(compilation.assets));
});发布和使用自定义 Plugin
发布到 npm
创建 package.json
{ "name": "my-webpack-plugin", "version": "1.0.0", "main": "index.js" }导出 Plugin
module.exports = MyPlugin;发布
npm publish
使用自定义 Plugin
const MyPlugin = require('my-webpack-plugin');
module.exports = {
plugins: [
new MyPlugin({
option1: 'value1',
option2: 'value2'
})
]
};最佳实践
1. 提供默认值
constructor(options = {}) {
this.options = {
option1: options.option1 || 'default',
option2: options.option2 || true,
...options
};
}2. 错误处理
apply(compiler) {
try {
// Plugin 逻辑
} catch (err) {
compilation.errors.push(err);
}
}3. 支持异步操作
compiler.hooks.emit.tapAsync(
'MyPlugin',
async (compilation, callback) => {
try {
await doAsyncWork();
callback();
} catch (err) {
callback(err);
}
}
);4. 文档注释
/**
* MyPlugin - Webpack 插件示例
*
* @param {Object} options - 插件选项
* @param {string} options.option1 - 选项 1
* @param {boolean} options.option2 - 选项 2
*/
class MyPlugin {
// ...
}总结
Plugin 是 Webpack 最强大的功能之一,通过掌握 Plugin 开发:
- 可以自定义构建流程
- 可以优化打包过程
- 可以生成额外的资源
- 可以实现任何你需要的功能
Webpack 的很多功能本质上都是 Plugin!
