tapable
Tapable exports nine hooks
- SyncHook
- SyncBailHook
- SyncWaterfallHook
- SyncLoopHook
- AsyncParallelHook
- AsyncParallelBailHook
- AsyncSeriesHook
- AsyncSeriesBailHook
- AsyncSeriesWaterfallHook
All nine hooks inherit from the Hook class
Tapable Hook parsing
Hook externally provides isUsed Call Promise callAsync compile tap tapAsync tapPromise Intercept methods
The tap method isUsed to subscribe to events, call promise callAsync isUsed to trigger events, and isUsed returns a Boolean value indicating whether the event registered in the current hook has been executed.
IsUsed source
isUsed() { return this.taps.length > 0 || this.interceptors.length > 0; } Copy the code
Tap tapAsync tapPromise These three methods can support passing in a string (usually the name of the plugin) or a TAP type as the first argument, while the second argument is a callback to receive the call when the event is emitted.
export interface Tap {
name: string; // The event name, usually the name of the plugin
type: TapType; // Support three types: 'sync' 'async' 'promise'
fn: Function;
stage: number;
context: boolean;
}
Copy the code
The call Promise callAsync methods depend on the number of arGS placeholders passed in when the hook is instantiated, as shown in the following example:
const sync = new SyncHook(['arg1'.'arg2']) // 'arg1' and 'arg2' are placeholders for arguments
sync.tap('Test', (arg1, arg2) => {
console.log(arg1, arg2) // a2
})
sync.call('a'.'2')
Copy the code
The PROMISE call returns a PROMISE, and callAsync supports passing in a callback by default.
Sync-starting hooks do not support tapAsync and tapPromise. See the SyncHook source code below for an example
const TAP_ASYNC = (a)= > {
throw new Error("tapAsync is not supported on a SyncHook");
};
const TAP_PROMISE = (a)= > {
throw new Error("tapPromise is not supported on a SyncHook");
};
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = SyncHook;
hook.tapAsync = TAP_ASYNC;
hook.tapPromise = TAP_PROMISE;
hook.compile = COMPILE;
return hook;
}
SyncHook.prototype = null;
Copy the code
In this we can see that tapAsync and tapPromise are overwritten with throw errors
A simple use demonstration
The following example will give you a simple demonstration
class TapableTest {
constructor() {
this.hooks = {
sync: new SyncHook(['context'.'hi']),
syncBail: new SyncBailHook(),
syncLoop: new SyncLoopHook(),
syncWaterfall: new SyncWaterfallHook(['syncwaterfall']),
asyncParallel: new AsyncParallelHook(),
asyncParallelBail: new AsyncParallelBailHook(),
asyncSeries: new AsyncSeriesHook(),
asyncSeriesBail: new AsyncSeriesBailHook(),
asyncSeriesWaterfall: new AsyncSeriesWaterfallHook(['asyncwaterfall'])
}
}
emitSync() {
this.hooks.sync.call(this, err => {
console.log(this.hooks.sync.promise)
console.log(err)
})
}
emitAyncSeries() {
this.hooks.asyncSeries.callAsync(err= > {
if (err) console.log(err)
})
}
}
const test = new TapableTest()
test.hooks.sync.tap('TestPlugin', (context, callback) => {
console.log('trigger: ', context)
callback(new Error('this is sync error'))
})
test.hooks.asyncSeries.tapAsync('AsyncSeriesPlugin', callback => {
callback(new Error('this is async series error'))
})
test.emitSync()
test.emitAyncSeries()
Copy the code
The above results can be viewed here at Runkit
Let’s talk about how plug-ins in WebPack rely on Tapable
When the WebPack plug-in is injected
When we define the webPack configuration file, WebPack will generate one or more compiler based on these configurations, and the plug-in is added to the entire run of WebPack during the creation of the compiler. (The source code can be found in webpack.js in webpack lib.)
const createCompiler = rawOptions= > {
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
const compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};
Copy the code
We can see that the options.plugins section is iterated over, and that there are two cases in which plug-ins are inserted
- Our plugin can be functionally modified
webpack
Call, which means we can write the plug-in with a function whose scope is currentcompiler
, the function will also receive onecompiler
- You can pass in an include
apply
Method object instance,apply
Methods are passed incompiler
So that explains why our plugin needs to come out new and be passed into Webpack
Enter Compiler to find out
In the previous section we saw when plugins were injected. We can see that the Compiler currently instantiated was passed in when plugins were injected, so now we need to know what is being done in the Compiler
In compiler.js (also in lib) we can see for the first time that Compiler constructor defines a huge hooks:
this.hooks = Object.freeze({
/** @type {SyncHook<[]>} */
initialize: new SyncHook([]),
/** @type {SyncBailHook<[Compilation], boolean>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<[Stats]>} */
done: new AsyncSeriesHook(["stats"]),
/** @type {SyncHook<[Stats]>} */
afterDone: new SyncHook(["stats"]),
/** @type {AsyncSeriesHook<[]>} */
additionalPass: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<[Compiler]>} */
beforeRun: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<[Compiler]>} */
run: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
emit: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<[string, AssetEmittedInfo]>} */
assetEmitted: new AsyncSeriesHook(["file"."info"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
afterEmit: new AsyncSeriesHook(["compilation"])... })Copy the code
See these hooks are not familiar, all tapable hooks, Webpack is relying on these complex build hooks to complete our code construction, so when we write plugin can use these hooks to complete our special needs.
For example, we often use HtmlWebpackPlugin, we can see how it runs, in the apply of HtmlWebpackPlugin we can find such a code:
compiler.hooks.emit.tapAsync('HtmlWebpackPlugin', (compiler, callback) => {
...
})
Copy the code
HtmlWebpackPlugin is completed by using the EMIT hook of Compiler
By digging deeper, WebPack runs on huge plug-ins, many of them built in itself
Please correct any errors in the above content