1. Tapable是什么?
Tapable是Webpack的插件系统核心,提供了强大的事件发布-订阅机制。它类似于Node.js的EventEmitter,但功能更强大,支持:
-
多种执行模式(同步/异步、串行/并行)
-
返回值传递(瀑布流)
-
执行控制(可中断、可循环)
2. 9种钩子类型详解
2.1 SyncHook - 同步钩子
所有回调依次执行,互不影响。
javascript
const { SyncHook } = require('tapable');
const hook = new SyncHook(['name', 'age']);
// 注册回调
hook.tap('plugin1', (name, age) => {
console.log(`plugin1: ${name}, ${age}`);
});
hook.tap('plugin2', (name, age) => {
console.log(`plugin2: ${name}, ${age}`);
});
// 触发
hook.call('Alice', 25);
// 输出:
// plugin1: Alice, 25
// plugin2: Alice, 25
2.2 SyncBailHook - 保险钩子
任何回调返回非undefined,则停止后续执行
javascript
const { SyncBailHook } = require('tapable');
const hook = new SyncBailHook(['num']);
hook.tap('plugin1', (num) => {
console.log(`plugin1: ${num}`);
return undefined; // 继续执行
});
hook.tap('plugin2', (num) => {
console.log(`plugin2: ${num}`);
return 'stop'; // 停止后续
});
hook.tap('plugin3', (num) => {
console.log(`plugin3: ${num}`); // 不会执行
});
hook.call(10);
// 输出: plugin1: 10, plugin2: 10
2.3 SyncWaterfallHook - 瀑布钩子
上一个回调的返回值会传给下一个回调。
javascript
const { SyncWaterfallHook } = require('tapable');
const hook = new SyncWaterfallHook(['value']);
hook.tap('plugin1', (value) => {
return value + 1;
});
hook.tap('plugin2', (value) => {
return value * 2;
});
hook.tap('plugin3', (value) => {
return value - 3;
});
const result = hook.call(5);
console.log(result); // 9
// 流程: 5 -> +1=6 -> *2=12 -> -3=9
2.4 SyncLoopHook - 循环钩子
如果回调返回true,则从头重新执行所有回调。
javascript
const { SyncLoopHook } = require('tapable');
const hook = new SyncLoopHook(['name']);
let count = 0;
hook.tap('plugin1', (name) => {
console.log(`plugin1: ${name}, count: ${count}`);
count++;
return count < 3; // 前3次返回true,重新循环
});
hook.tap('plugin2', (name) => {
console.log(`plugin2: ${name}, count: ${count}`);
return false; // 不再循环
});
hook.call('Loop');
2.5 AsyncParallelHook - 异步并行钩子
多个异步任务同时执行。
javascript
const { AsyncParallelHook } = require('tapable');
const hook = new AsyncParallelHook(['name']);
// 方式1: 回调方式
hook.tapAsync('plugin1', (name, callback) => {
setTimeout(() => {
console.log(`plugin1: ${name}`);
callback();
}, 1000);
});
// 方式2: Promise方式
hook.tapPromise('plugin2', (name) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`plugin2: ${name}`);
resolve();
}, 500);
});
});
// 触发
hook.callAsync('Parallel', () => {
console.log('所有任务完成');
});
2.6 AsyncSeriesHook - 异步串行钩子
多个异步任务依次执行。
javascript
const { AsyncSeriesHook } = require('tapable');
const hook = new AsyncSeriesHook(['name']);
hook.tapAsync('plugin1', (name, callback) => {
setTimeout(() => {
console.log(`plugin1: ${name}`);
callback();
}, 1000);
});
hook.tapAsync('plugin2', (name, callback) => {
setTimeout(() => {
console.log(`plugin2: ${name}`);
callback();
}, 500);
});
hook.callAsync('Series', () => {
console.log('所有任务完成');
});
// 输出顺序: plugin1 (1秒后), plugin2 (再0.5秒后), 完成
3. 钩子使用对比表
| 钩子类型 | 执行方式 | 返回值处理 | 适用场景 |
|---|---|---|---|
| SyncHook | 同步顺序 | 忽略返回值 | 日志、统计、通知 |
| SyncBailHook | 同步顺序,遇非undefined停止 | 返回第一个非undefined | 权限验证、条件检查 |
| SyncWaterfallHook | 同步顺序,传递返回值 | 上一个返回值作为下一个参数 | 代码转换、数据处理 |
| SyncLoopHook | 循环执行 | 返回true则重新循环 | 重试机制、验证 |
| AsyncSeriesHook | 异步串行 | 等待每个完成 | 文件操作、网络请求 |
| AsyncParallelHook | 异步并行 | 等待所有完成 | 并行任务、资源加载 |
| AsyncSeriesBailHook | 异步串行,遇错停止 | 错误时中断 | 关键任务链 |
| AsyncSeriesWaterfallHook | 异步串行,传递结果 | 结果传递 | 流水线处理 |
5. 自定义Tapable实现(理解原理)
javascript
// 实现SyncHook
class MySyncHook {
constructor(args = []) {
this.args = args;
this.taps = [];
}
tap(name, fn) {
this.taps.push({ name, fn });
}
call(...args) {
const params = args.slice(0, this.args.length);
for (const tap of this.taps) {
tap.fn(...params);
}
}
}
// 实现SyncBailHook
class MySyncBailHook {
constructor(args = []) {
this.args = args;
this.taps = [];
}
tap(name, fn) {
this.taps.push({ name, fn });
}
call(...args) {
const params = args.slice(0, this.args.length);
for (const tap of this.taps) {
const result = tap.fn(...params);
if (result !== undefined) return result;
}
}
}
// 实现SyncWaterfallHook
class MySyncWaterfallHook {
constructor(args = []) {
this.args = args;
this.taps = [];
}
tap(name, fn) {
this.taps.push({ name, fn });
}
call(...args) {
const params = args.slice(0, this.args.length);
let result = params[0];
for (const tap of this.taps) {
result = tap.fn(result, ...params.slice(1));
}
return result;
}
}
从0构建Webpack
Compiler核心实现
javascript
const {
SyncHook, SyncBailHook, SyncWaterfallHook,
AsyncSeriesHook, AsyncParallelHook
} = require('tapable');
const fs = require('fs');
const path = require('path');
class Compiler {
constructor(config) {
this.config = config;
this.entry = config.entry;
this.output = config.output;
this.modules = new Map();
// 定义生命周期钩子
this.hooks = {
// 初始化
init: new SyncHook(['compiler']),
// 编译流程
beforeRun: new AsyncSeriesHook(['compiler']),
run: new AsyncSeriesHook(['compiler']),
beforeCompile: new AsyncSeriesHook(['compiler']),
compile: new SyncHook(['compiler']),
// 模块处理
beforeResolve: new SyncBailHook(['request']),
resolve: new SyncHook(['request']),
afterResolve: new SyncHook(['request']),
beforeBuildModule: new SyncHook(['module']),
buildModule: new AsyncSeriesHook(['module']),
afterBuildModule: new SyncHook(['module']),
// 代码转换
load: new SyncWaterfallHook(['source', 'filePath']),
transform: new SyncWaterfallHook(['code', 'filePath']),
// 依赖解析
parseDependencies: new SyncHook(['module']),
// 输出生成
beforeEmit: new AsyncSeriesHook(['compilation']),
emit: new AsyncSeriesHook(['compilation']),
afterEmit: new AsyncSeriesHook(['compilation']),
// 完成
done: new SyncHook(['stats']),
failed: new SyncHook(['error'])
};
// 应用插件
if (config.plugins) {
config.plugins.forEach(plugin => {
if (plugin.apply) {
plugin.apply(this);
}
});
}
this.hooks.init.call(this);
}
async run() {
try {
await this.hooks.beforeRun.promise(this);
await this.hooks.run.promise(this);
const compilation = await this.compile();
await this.emit(compilation);
this.hooks.done.call({
startTime: Date.now(),
modules: this.modules.size
});
return compilation;
} catch (error) {
this.hooks.failed.call(error);
throw error;
}
}
async compile() {
await this.hooks.beforeCompile.promise(this);
this.hooks.compile.call(this);
const compilation = {
modules: [],
assets: {},
entry: this.entry
};
// 构建入口模块
const entryModule = await this.buildModule(this.entry, process.cwd());
compilation.modules.push(entryModule);
// 递归构建依赖
await this.buildDependencies(entryModule, compilation);
return compilation;
}
async buildModule(filePath, context) {
const absolutePath = path.resolve(context, filePath);
if (this.modules.has(absolutePath)) {
return this.modules.get(absolutePath);
}
this.hooks.beforeBuildModule.call({ filePath: absolutePath });
// 读取文件
let source = fs.readFileSync(absolutePath, 'utf-8');
// 应用loader(瀑布流)
source = this.hooks.load.call(source, absolutePath);
let code = this.hooks.transform.call(source, absolutePath);
// 解析依赖
const dependencies = this.parseDependencies(code);
// 转换ES6代码
code = this.transformCode(code);
const module = {
filePath: absolutePath,
code,
dependencies,
rawSource: source
};
this.hooks.afterBuildModule.call(module);
this.modules.set(absolutePath, module);
return module;
}
async buildDependencies(module, compilation) {
for (const dep of module.dependencies) {
// 保险钩子:可跳过依赖
const skip = this.hooks.beforeResolve.call({
request: dep,
parent: module.filePath
});
if (skip === false) continue;
this.hooks.resolve.call({ request: dep, parent: module.filePath });
const depPath = this.resolvePath(dep, path.dirname(module.filePath));
this.hooks.afterResolve.call({ request: dep, resolved: depPath });
const depModule = await this.buildModule(depPath, path.dirname(module.filePath));
if (!compilation.modules.find(m => m.filePath === depModule.filePath)) {
compilation.modules.push(depModule);
await this.buildDependencies(depModule, compilation);
}
}
}
parseDependencies(code) {
const dependencies = [];
const importRegex = /import\s+.*?\s+from\s+['"](.*?)['"]/g;
const requireRegex = /require\(['"](.*?)['"]\)/g;
let match;
while ((match = importRegex.exec(code)) !== null) {
dependencies.push(match[1]);
}
while ((match = requireRegex.exec(code)) !== null) {
dependencies.push(match[1]);
}
this.hooks.parseDependencies.call({ code, dependencies });
return dependencies;
}
transformCode(code) {
// import -> require
code = code.replace(/import\s+(\w+)\s+from\s+['"](.*?)['"]/g, (match, name, path) => {
return `const ${name} = require('${path}');`;
});
// export default -> module.exports
code = code.replace(/export\s+default\s+(\w+)/g, (match, name) => {
return `module.exports = ${name};`;
});
// export const/function -> exports.xxx
code = code.replace(/export\s+(const|function|let|var)\s+(\w+)/g, (match, type, name) => {
return `${type} ${name}; exports.${name} = ${name};`;
});
return code;
}
resolvePath(request, baseDir) {
if (request.startsWith('.') || request.startsWith('/')) {
const extensions = ['.js', '.json', ''];
for (const ext of extensions) {
const fullPath = path.resolve(baseDir, request + ext);
if (fs.existsSync(fullPath)) {
return fullPath;
}
}
return path.resolve(baseDir, request);
}
return request;
}
async emit(compilation) {
await this.hooks.beforeEmit.promise(compilation);
const chunks = this.createChunks(compilation);
for (const chunk of chunks) {
const code = this.generateCode(chunk);
const filename = this.output.filename || 'bundle.js';
const outputPath = path.resolve(this.output.path, filename);
compilation.assets[filename] = code;
await this.hooks.emit.promise(compilation);
fs.writeFileSync(outputPath, code);
console.log(`✅ 生成文件: ${outputPath}`);
}
await this.hooks.afterEmit.promise(compilation);
}
createChunks(compilation) {
return [{
name: 'main',
modules: compilation.modules,
entry: compilation.entry
}];
}
generateCode(chunk) {
const modulesMap = {};
chunk.modules.forEach(module => {
modulesMap[module.filePath] = module.code;
});
return `
(function(modules) {
const installedModules = {};
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
const module = installedModules[moduleId] = {
exports: {}
};
modules[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
return __webpack_require__('${chunk.entry}');
})({
${Object.entries(modulesMap).map(([id, code]) => `
"${id}": function(module, exports, require) {
${code}
}
`).join(',\n')}
});
`;
}
}
module.exports = Compiler;