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 modifiedwebpackCall, 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 includeapplyMethod object instance,applyMethods 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