Ding Nan, the front-end engineer of The Wedoctor Cloud service team, a senior CV engineer, loves code, and is a very good Dao Silin

preface

After using Webpack for so long, you must be curious about loader and plugin, which are important parts of its ecosystem. Have you tried to write your own plug-in? Have you understood the plug-in mechanism of Webpack? No, then don’t hurry to get on the bus to learn a wave!

1, tapable

Webpack is like a production line that goes through a series of processes to convert source files into output. Each process on this production line has a single responsibility. There are dependencies between multiple processes. Only after the current process is completed can the next process be handled. A plug-in is like a function that plugs into a production line and works with resources on the production line at a specific time. Webpack organizes this complex production line through Tapable. Webpack broadcasts events as it runs, and the plug-in only needs to listen for the events it cares about to join the production line and change how the production line works. Webpack’s event flow mechanism ensures the orderliness of plug-ins and makes the whole system very extensible. – “Simple Webpack”

As the core library of Webpack, Tabpable contract Webpack’s most important event working mechanism, including the two high-frequency objects in Webpack source code (Compiler, compilation) are inherited from Tapable class objects, Each of these objects has Tapable’s ability to register and call plug-ins, exposing their respective execution order and hook types, as detailed in the documentation

2. Tapable hooks

const {
	SyncHook,
	SyncBailHook,
	SyncWaterfallHook,
	SyncLoopHook,
	AsyncParallelHook,
	AsyncParallelBailHook,
	AsyncSeriesHook,
	AsyncSeriesBailHook,
	AsyncSeriesWaterfallHook
 } = require("tapable");
Copy the code

The main types of hooks are synchronous, asynchronous, cascading, serial, parallel, circular, etc. The purpose of a hook is to explicitly declare the parameters that are passed in when a call is triggered. And the arguments received by the callback function subscribing to the hook, for example 

const sync = new SyncHook(['arg']) // 'arg' is the parameter placeholder
sync.tap('Test'.(arg1, arg2) = > {
  console.log(arg1, arg2) / / a, undefined
})
sync.call('a'.'2')
Copy the code

This code defines a synchronous serial hook and declares the number of parameters to be received. You can subscribe to the event through the tap method of the hook instance (SyncHook itself inherits from the Hook class), and then use the call function to trigger the subscription event and execute the callback function. It is important to note that the number of arguments passed to call needs to match the length of the array passed to the hook class constructor at instantiation; otherwise, even if multiple arguments are passed, only the number of arguments defined at instantiation will be received.

The serial number The name of the hook Implement way Use the point
1 SyncHook Synchronous serial Does not care about the return value of the listening function
2 SyncBailHook Synchronous serial If one of the listening functions does not return null, the residual logic is skipped
3 SyncWaterfallHook Synchronous serial The return value of the previous listener is passed as an argument to the next listener
4 SyncLoopHook Synchronous serial When fired, the listener is repeated if it returns true, and exits the loop if it returns undefined
5 AsyncParallelHook Asynchronous parallel Does not care about the return value of the listening function
6 AsyncParallelBailHook Asynchronous parallel As long as the return value of the listener function is not null, the subsequent listener function execution is ignored, and the callback function that triggers the function binding, such as callAsync, is directly skipped, and the bound callback function is executed
7 AsyncSeriesHook Asynchronous serial port Do not care about arguments to callback()
8 AsyncSeriesBailHook Asynchronous serial port Callback () does not have a null argument, and calls such as callAsync that trigger the function binding are executed directly
9 AsyncSeriesWaterfallHook Asynchronous serial port The second argument to the callback(err, data) in the previous listener can be used as an argument to the next listener

The above table lists all hook usage methods and key points.

Register event callbacks

There are three methods for registering event callbacks: tap, tapAsync, and tapPromise, where tapAsync and tapPromise cannot be used for hook classes starting with Sync and will report an error if used by force. In contrast to tap, tapAsync requires a callback function to ensure that the process goes to the next plug-in.

myCar.hooks.calculateRoutes.tapAsync("BingMapsPlugin".(source, target, routesList, callback) = > {
	bing.findRoute(source, target, (err, route) = > {
		if(err) return callback(err);
		routesList.add(route);
		// call the callback
		callback();
	});
});
Copy the code

4. Trigger events

The three methods that trigger events correspond to the methods that register event callbacks, as can be seen from the names of the methods: Call for TAP, callAsync for tapAsync, and Promise for tapPromise. In general, it is best to use whatever method we use to register event callbacks when they are triggered. Note that callAsync has a callback function that needs to be executed when the logic is complete.

5. Understand the mechanics

So how exactly do you use Tapable to invoke these plugins in Webpack?

Let’s start with an example of writing a plugin from the official website

class HelloWorldPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('Hello World Plugin'.(
      compilation /* compilation is passed as an argument when done hook is tapped. */
    ) = > {
      console.log('Hello World! '); }); }}module.exports = HelloWorldPlugin;
Copy the code

The above code blocks write a class called HelloWorldPlugin, which provides a method called apply, in which we can obtain a single compiler instance in the whole process of Webpack execution from the outside. We can tap a listener event at the done node of the Webpack lifecycle. That is, when the Webpack process is complete, the listener event will be triggered and stat statistics will be passed in. In the event, We can do the kind of data analysis that we want to do through STAT. In general, to use a Webpack plugin, you need to import the plugin’s class in the Webpack configuration file, new an instance, like this:

// Webpack.config.js
var HelloWorldPlugin = require('hello-world');

module.exports = {
  // ... configuration settings here ...
  plugins: [new HelloWorldPlugin({ options: true}})];Copy the code

Webpack reads the configuration file, obtains the HelloWorldPlugin instance, calls the apply method, and adds listening events to the Done node. Yes, let’s go back to the source code for Webpack, which we can see in the lib/ webpack.js file of the Webpack project

if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
		if (typeof plugin === "function") {
			plugin.call(compiler, compiler);
		} else{ plugin.apply(compiler); }}}Copy the code

In this code, the options refer to the entire object exported from the configuration file. You can see that the Webpack loop iterates through the plugins and calls their respective apply methods. This is the general exception I mentioned above. If your plugin logic is simple, you can simply write a function in the configuration file to implement your logic, rather than writing a class or using a more pure Prototype method to define the class’s methods. So far, we have seen how plug-in listener events are registered with Webpack’s compile and compilation (tapable) classes. How and when are listener events triggered? We then find this code in the run function of the Compiler class in lib/ compiler.js

const onCompiled = (err, compilation) = > {
	if (err) return finalCallback(err);

	if (this.hooks.shouldEmit.call(compilation) === false) {...this.hooks.done.callAsync(stats, err= > {
			if (err) return finalCallback(err);
			return finalCallback(null, stats);
		});
		return;
	}

	this.emitAssets(compilation, err= > {
		if (err) return finalCallback(err);

		if (compilation.hooks.needAdditionalPass.call()) {
			...
			this.hooks.done.callAsync(stats, err= >{... });return;
		}

		this.emitRecords(err= > {
			if (err) returnfinalCallback(err); .this.hooks.done.callAsync(stats, err= > {
				if (err) return finalCallback(err);
				return finalCallback(null, stats); }); }); }); }; .this.compile(onCompiled);
Copy the code

OnCompiled callback function will be at the end of the compile process is called, no matter go to which the if logic, enclosing hooks. Done. CallAsync will be implemented, that is registered in the done nodes to monitor events will be executed in accordance with the order. Then we can trace back further, the run function that wraps onCompiled is executed in lib/ webpack.js

if (Array.isArray(options)) {
    ...
} else if (typeof options === "object") {... compiler =new Compiler(options.context);
	compiler.options = options;
	if (options.plugins && Array.isArray(options.plugins)) {
		for (const plugin of options.plugins) {
			if (typeof plugin === "function") {
				plugin.call(compiler, compiler);
			} else{ plugin.apply(compiler); }}}}else{... }if (callback) {
	...
	compiler.run(callback);
}
Copy the code

Just inplugin.apply()So it is in accordance with the logical order of first register listening events, and then trigger.If it’s a little confusing already, let’s do it. Let’s use a flow chart.

A graphical representation of the plug-in’s registration execution process

6, summary

Tapable, as the core library of Webpack, undertakes the operation of the most important event flow of Webpack. Its clever hook design decouples the implementation from the process, and truly realizes the plug-and-pull function module. The most core Compiler and Compilation of bundles created in Webpack are both examples of Tapable. It can be said that Tapable knowledge reserve is essential to truly understand Webpack. Some of its design ideas are also very worthy of our reference, this article is just some of the TAPable API and Webpack how to use Tapable string up the whole plug-in flow mechanism to do the introduction, if the tapable implementation principle and source code interested friends, You can move to Tapable’s Github repository to learn from the above statement. Please correct it

reference

A simple Webpack: http://webpack.wuhaolin.cn/

How can Webpack just use tapable, the core of the core? : https://juejin.cn/post/6844903646480580615

The use and principle of Webpack core library Tapable analytic: https://juejin.cn/post/6844904037624578061#heading-21