是否写过 Plugin?简单描述一下编写 Plugin 的思路?
问题解析
Plugin 是 Webpack 的支柱功能,用于在构建过程中执行更广泛的任务。与 Loader 专注于文件转换不同,Plugin 可以访问整个构建生命周期,实现更强大的功能。
核心概念
Plugin 的工作原理
Webpack 构建流程:
|
v
初始化参数 -> 开始编译 -> 确定入口 -> 编译模块 -> 完成编译
| |
v v
输出资源 <---------------------------------- 输出完成
|
v
构建完成
Plugin 在关键节点监听事件,执行自定义逻辑
核心对象
| 对象 | 作用 | 生命周期 |
|---|---|---|
| Compiler | Webpack 实例,暴露整个生命周期钩子 | 全局唯一 |
| Compilation | 当前编译过程,暴露模块和依赖相关事件 | 每次构建创建 |
Tapable 事件流机制
Webpack 使用 Tapable 库实现事件流,保证插件有序执行:
| 钩子类型 | 特点 | 使用场景 |
|---|---|---|
SyncHook |
同步串行 | 简单同步操作 |
SyncBailHook |
同步串行,可中断 | 需要提前返回 |
AsyncParallelHook |
异步并行 | 并行执行任务 |
AsyncSeriesHook |
异步串行 | 按顺序执行异步任务 |
详细解答
最简单的 Plugin
// my-plugin.js
class MyPlugin {
constructor(options) {
this.options = options || {};
}
// 必须定义 apply 方法
apply(compiler) {
// 在编译完成时触发
compiler.hooks.done.tap('MyPlugin', (stats) => {
console.log('编译完成!');
console.log('统计信息:', stats.toJson());
});
}
}
module.exports = MyPlugin;
使用 Plugin
// webpack.config.js
const MyPlugin = require('./my-plugin');
module.exports = {
plugins: [
new MyPlugin({
message: 'Hello Webpack!'
})
]
};
访问 Compilation
class MyPlugin {
apply(compiler) {
// 在编译创建时触发
compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
console.log('Compilation 创建');
// 访问 compilation 的钩子
compilation.hooks.optimize.tap('MyPlugin', () => {
console.log('资源优化中...');
});
});
}
}
异步事件处理
class AsyncPlugin {
apply(compiler) {
// 异步串行钩子
compiler.hooks.emit.tapAsync('AsyncPlugin', (compilation, callback) => {
console.log('开始输出资源...');
setTimeout(() => {
console.log('异步操作完成');
// 必须调用 callback 通知 Webpack 继续
callback();
}, 1000);
});
// 使用 Promise 的异步钩子
compiler.hooks.emit.tapPromise('AsyncPlugin', (compilation) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('Promise 异步完成');
resolve();
}, 1000);
});
});
}
}
深入理解
Compiler 生命周期钩子
class LifecyclePlugin {
apply(compiler) {
// 1. 环境初始化完成
compiler.hooks.environment.tap('LifecyclePlugin', () => {
console.log('1. environment');
});
// 2. 编译器准备就绪
compiler.hooks.afterEnvironment.tap('LifecyclePlugin', () => {
console.log('2. afterEnvironment');
});
// 3. 开始编译(创建 Compilation)
compiler.hooks.beforeCompile.tap('LifecyclePlugin', (params) => {
console.log('3. beforeCompile');
});
// 4. 编译中
compiler.hooks.compile.tap('LifecyclePlugin', (params) => {
console.log('4. compile');
});
// 5. 创建 Compilation 后
compiler.hooks.thisCompilation.tap('LifecyclePlugin', (compilation) => {
console.log('5. thisCompilation');
});
// 6. 创建 Compilation(每次构建都会触发)
compiler.hooks.compilation.tap('LifecyclePlugin', (compilation) => {
console.log('6. compilation');
});
// 7. make 阶段:分析模块依赖
compiler.hooks.make.tapAsync('LifecyclePlugin', (compilation, callback) => {
console.log('7. make');
callback();
});
// 8. 模块构建完成
compiler.hooks.afterCompile.tap('LifecyclePlugin', (compilation) => {
console.log('8. afterCompile');
});
// 9. 输出资源前
compiler.hooks.emit.tapAsync('LifecyclePlugin', (compilation, callback) => {
console.log('9. emit');
callback();
});
// 10. 输出资源后
compiler.hooks.afterEmit.tap('LifecyclePlugin', (compilation) => {
console.log('10. afterEmit');
});
// 11. 编译完成
compiler.hooks.done.tap('LifecyclePlugin', (stats) => {
console.log('11. done');
});
// 12. 编译失败
compiler.hooks.failed.tap('LifecyclePlugin', (error) => {
console.log('12. failed:', error);
});
}
}
Compilation 关键钩子
class CompilationPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('CompilationPlugin', (compilation) => {
// 1. 模块构建前
compilation.hooks.buildModule.tap('CompilationPlugin', (module) => {
console.log('构建模块:', module.identifier());
});
// 2. 模块构建后
compilation.hooks.succeedModule.tap('CompilationPlugin', (module) => {
console.log('模块构建成功:', module.identifier());
});
// 3. 密封阶段:不再接收新模块
compilation.hooks.seal.tap('CompilationPlugin', () => {
console.log('开始密封');
});
// 4. 优化阶段
compilation.hooks.optimize.tap('CompilationPlugin', () => {
console.log('优化资源');
});
// 5. 优化 chunk
compilation.hooks.optimizeChunks.tap('CompilationPlugin', (chunks) => {
console.log('优化 chunks:', chunks.size);
});
// 6. 生成资源
compilation.hooks.assets.tap('CompilationPlugin', (assets) => {
console.log('生成资源:', Object.keys(assets));
});
// 7. 生成 hash
compilation.hooks.chunkHash.tap('CompilationPlugin', (chunk, chunkHash) => {
console.log('生成 chunk hash:', chunk.name);
});
});
}
}
操作输出资源
const { RawSource } = require('webpack-sources');
class GenerateFilePlugin {
constructor(options) {
this.options = options || {};
}
apply(compiler) {
compiler.hooks.emit.tapAsync('GenerateFilePlugin', (compilation, callback) => {
// 获取输出资源
const assets = compilation.assets;
// 生成文件列表
const fileList = Object.keys(assets)
.map(filename => {
const size = assets[filename].size();
return `- ${filename}: ${size} bytes`;
})
.join('\n');
// 添加新资源
const content = `
# 构建文件列表
生成时间: ${new Date().toLocaleString()}
${fileList}
`.trim();
// 使用 webpack-sources 创建资源
compilation.assets['file-list.md'] = new RawSource(content);
callback();
});
}
}
最佳实践
实用的 Plugin 示例
1. 构建时间统计插件
class BuildTimePlugin {
constructor(options = {}) {
this.options = {
name: 'BuildTimePlugin',
...options
};
this.startTime = null;
}
apply(compiler) {
// 记录开始时间
compiler.hooks.compile.tap(this.options.name, () => {
this.startTime = Date.now();
console.log('开始编译...');
});
// 计算耗时
compiler.hooks.done.tap(this.options.name, (stats) => {
const endTime = Date.now();
const duration = (endTime - this.startTime) / 1000;
console.log(`编译完成,耗时: ${duration.toFixed(2)}s`);
if (stats.hasErrors()) {
console.error('编译失败!');
} else if (stats.hasWarnings()) {
console.warn('编译成功,但有警告');
} else {
console.log('编译成功!');
}
});
}
}
module.exports = BuildTimePlugin;
2. 复制文件插件
const fs = require('fs');
const path = require('path');
class CopyFilePlugin {
constructor(options) {
this.options = options || {};
}
apply(compiler) {
const { from, to } = this.options;
compiler.hooks.afterEmit.tapAsync('CopyFilePlugin', (compilation, callback) => {
const outputPath = compilation.outputOptions.path;
const fromPath = path.resolve(from);
const toPath = path.resolve(outputPath, to);
fs.copyFile(fromPath, toPath, (err) => {
if (err) {
compilation.errors.push(err);
} else {
console.log(`已复制: ${from} -> ${to}`);
}
callback();
});
});
}
}
module.exports = CopyFilePlugin;
3. 清理控制台插件
class ClearConsolePlugin {
constructor(options = {}) {
this.options = {
clearOnStart: true,
...options
};
}
apply(compiler) {
if (this.options.clearOnStart) {
compiler.hooks.compile.tap('ClearConsolePlugin', () => {
// 清理控制台
process.stdout.write(
process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H'
);
});
}
}
}
module.exports = ClearConsolePlugin;
4. 资源大小分析插件
class BundleAnalyzerPlugin {
constructor(options = {}) {
this.options = {
maxSize: 244 * 1024, // 244KB
...options
};
}
apply(compiler) {
compiler.hooks.emit.tap('BundleAnalyzerPlugin', (compilation) => {
const assets = compilation.assets;
const warnings = [];
console.log('\n===== 资源大小分析 =====\n');
Object.keys(assets).forEach(filename => {
const asset = assets[filename];
const size = asset.size();
const sizeInKB = (size / 1024).toFixed(2);
const isOversized = size > this.options.maxSize;
const status = isOversized ? '⚠️ 过大' : '✓';
console.log(`${status} ${filename}: ${sizeInKB} KB`);
if (isOversized) {
warnings.push(`${filename} 超过 ${(this.options.maxSize / 1024).toFixed(0)}KB`);
}
});
console.log('\n=======================\n');
// 添加警告
warnings.forEach(warning => {
compilation.warnings.push(new Error(warning));
});
});
}
}
module.exports = BundleAnalyzerPlugin;
完整的 Plugin 配置
// webpack.config.js
const BuildTimePlugin = require('./plugins/build-time-plugin');
const BundleAnalyzerPlugin = require('./plugins/bundle-analyzer-plugin');
const ClearConsolePlugin = require('./plugins/clear-console-plugin');
module.exports = {
plugins: [
// 清理控制台
new ClearConsolePlugin(),
// 统计构建时间
new BuildTimePlugin({
name: 'MyBuildTimePlugin'
}),
// 分析资源大小
new BundleAnalyzerPlugin({
maxSize: 200 * 1024 // 200KB
})
]
};
开发 Plugin 的注意事项
class BestPracticePlugin {
constructor(options) {
// 1. 合并默认选项
this.options = {
verbose: false,
...options
};
}
apply(compiler) {
const pluginName = 'BestPracticePlugin';
// 2. 使用具名函数,便于调试
compiler.hooks.compile.tap(pluginName, () => {
if (this.options.verbose) {
console.log('开始编译');
}
});
// 3. 异步钩子正确处理回调
compiler.hooks.emit.tapAsync(pluginName, (compilation, callback) => {
try {
// 执行操作
this.processAssets(compilation);
// 成功时调用 callback,不传错误
callback();
} catch (error) {
// 错误时传递错误对象
callback(error);
}
});
// 4. 或使用 tapPromise
compiler.hooks.emit.tapPromise(pluginName, (compilation) => {
return this.asyncProcess(compilation);
});
}
processAssets(compilation) {
// 处理资源
}
asyncProcess(compilation) {
return new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
resolve();
}, 100);
});
}
}
面试要点
-
Plugin 与 Loader 的区别:
- Loader:文件转换,处理单个文件
- Plugin:生命周期扩展,处理整个构建过程
-
核心概念:
Compiler:Webpack 实例,全局唯一Compilation:每次构建创建,包含模块信息Tapable:事件流机制,保证插件有序执行
-
编写 Plugin 的基本结构:
class MyPlugin { apply(compiler) { compiler.hooks.done.tap('MyPlugin', (stats) => { // 插件逻辑 }); } } -
异步事件处理:
tapAsync:使用 callbacktapPromise:返回 Promise
-
常用钩子:
compiler.hooks.emit:输出资源前compiler.hooks.done:编译完成compilation.hooks.optimize:优化阶段
-
注意事项:
- 必须定义
apply方法 - 异步事件需要调用回调或返回 Promise
- 使用
webpack-sources操作资源 compiler和compilation不是同一个引用
- 必须定义