The cause of

If you want to look at how Webpack works, I find that all the references to Tapable make me confused, so let’s take a good look at what this library is.

In the Official Webpack documentation, check out the Webpack lifecycle hook function and you will see the following figure:

As you can see, run is a hook function of type AsyncSeriesHook, which is the hook type provided by Tapable.

To understand the running flow of Webpack, you need to understand the use of this hook, and more recently, how Webpack calls various plug-ins as it runs.

Began to study

Start with the simplest possible project

As usual, let’s start with the simplest project:

Install the necessary libraries:

npm install --save-dev wepback
npm install --save-dev webpack-cli
npm install --save-dev webpack-dev-server

npm install --save tapable
Copy the code

We write our test code under SRC, run it, and see what our experiment shows. The configuration of webpack.config.js is as follows:

module.exports = { entry: { index: __dirname + "/src/index.js", }, output: { path: Filename: "[name].js", // output filename chunkFilename: '[name].js',}, mode: 'development', devtool: false, devServer: {contentBase: "./dist", True,// Skip inline: true// refresh in real time},}Copy the code

Configure the startup script in package.json and use NPM Run Server to view the results:

"scripts": {
    "start": "webpack",
    "server": "webpack-dev-server --open"
},
Copy the code

Synchronous hooks

The first hook, SyncHook

Tapable’s Github address is github.com/webpack/tap…

Here is the address of the Tapable -1 branch, which I think Webpack is currently using.

As described in its readme.md, Tapable exposes a number of Hook classes that help us create hooks for plug-ins.

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

SyncHook: SyncHook: SyncHook: SyncHook: SyncHook: SyncHook: SyncHook: SyncHook: SyncHook: SyncHook: SyncHook: SyncHook: SyncHook: SyncHook: SyncHook

import { SyncHook } from 'tapable'; const hook = new SyncHook(); / object/create hook hook. Tap (' logPlugin '() = > console. The log (' is marked for')); // the tap method registers the hook callback hook.call(); // The call method calls the hook and prints' checked 'Copy the code

Using the NPM Run Server, it runs successfully in the browser. Also successfully printed ‘ticked’. It’s easy to use.

This is the classic event registration and triggering mechanism. In practice, the code that declares and fires events is usually in one class, and the code that registers events is in another class (our plug-in). The code is as follows:

// Car.js import { SyncHook } from 'tapable'; export default class Car { constructor() { this.startHook = new SyncHook(); } start() { this.startHook.call(); }}Copy the code
// index.js import Car from './Car'; const car = new Car(); Car.starthook. tap('startPlugin', () => console.log(' I fastened my seat belt ')); car.start();Copy the code

The Car is only responsible for declaring and calling the hook, and the real execution logic is no longer in the Car, but in the index.js that registers it, outside of the Car. That’s a good decoupling.

For Car, this means registering plug-ins to enrich its own functionality.

Pass parameters to the plug-in

I hope so:

// index.js import Car from './Car'; const car = new Car(); Car. AccelerateHook. Tap ('acceleratePlugin', (speed) => console.log('accelerate to ${speed} ')); car.accelerate(100); When called, 100 is passed to the speed of the plug-in callbackCopy the code

The Car class can be written like this:

import { SyncHook } from 'tapable'; export default class Car { constructor() { this.startHook = new SyncHook(); this.accelerateHook = new SyncHook(["newSpeed"]); // This Hook needs a single parameter. } start() { this.startHook.call(); } accelerate(speed) { this.accelerateHook.call(speed); }}Copy the code

This completes the Hook with arguments. The SyncHook argument is passed as an array, which means we can also pass multiple arguments, such as new SyncHook([“arg1″,”arg2″,”arg3”]). This also allows you to pass three arguments to call and receive three arguments to call in the callback function.

Our Car class is a Tapable class, a declaration and call center for events.

The second hook is SyncBailHook

We have a general understanding of the Hook registration/call mechanism. SyncHook works fine, but Tapable also provides many hooks. What problems do these hooks solve?

The reason is that we can register multiple times for an event, as follows:

const car = new Car(); Tap ('brakePlugin1', () => console.log('brake 1')); Tap ('brakePlugin2', () => console.log('brake 2')); Tap ('brakePlugin3', () => console.log('brake 3')); car.brake(); // Print 'brake 1', 'brake 2', 'brake 3'Copy the code

Here we add hooks for the Car class. Brake, and a brake method. Brake’s hook was registered three times, and when we called Brake mode, all three plug-ins accepted the event.

We’ve refactored the Car class a little bit, which is said to be more in line with the best practice tapable uses, which is to put hooks in a single hooks field. The Car code is as follows:

import { SyncHook, SyncBailHook } from 'tapable'; export default class Car { constructor() { this.hooks = { start: new SyncHook(), accelerate: New SyncHook(["newSpeed"]), brake: new SyncBailHook(), // Here we want to use SyncBailHook hook}; } start() { this.hooks.start.call(); } accelerate(speed) { this.hooks.accelerate.call(speed); } brake() { this.hooks.brake.call(); }}Copy the code

We now need to satisfy the requirement that, no matter how many mods you register, I just want to be hit twice and not notify any other mods. SyncBailHook: SyncBailHook

import Car from './Car'; const car = new Car(); Tap ('brakePlugin1', () => console.log('brake 1')); // Just return undefined if you don't want to go further. Tap ('brakePlugin2', () => {console.log('brake 2'); return 1; }); Tap ('brakePlugin3', () => console.log('brake 3')); car.brake(); // Only 'brake 1' and 'brake 2' will be printedCopy the code

SyncBailHook is used to decide whether or not to go down based on the value returned by each step. If you return a non-undefined value, you will not go down. Note that if you return nothing, you will return undefined.

Presumably, Tabable provides hooks to handle these external plug-in relationships.

The third hook is SyncWaterfallHook

Each SyncWaterfallHook step depends on the result of the previous step. The value of the previous step is the parameter of the next step.

Accelerate hook to SyncWaterfallHook:

import { SyncHook, SyncBailHook, SyncWaterfallHook } from 'tapable'; export default class Car { constructor() { this.hooks = { start: new SyncHook(), accelerate: New SyncWaterfallHook(["newSpeed"]), // Brake: new SyncBailHook(),}; } start() { this.hooks.start.call(); } accelerate(speed) { this.hooks.accelerate.call(speed); } brake() { this.hooks.brake.call(); }}Copy the code
// index.js import Car from './Car'; const car = new Car(); Tap ('acceleratePlugin1', (speed) => {console.log('accelerate to ${speed} '); return speed + 100; }); Tap ('acceleratePlugin2', (speed) => {console.log('accelerate to ${speed} '); return speed + 100; }); Tap ('acceleratePlugin3', (speed) => {console.log('accelerate to ${speed} '); }); car.accelerate(50); // Print 'accelerate to 50' 'accelerate to 150' 'accelerate to 250'Copy the code

The fourth hook, SyncLoopHook

SyncLoopHook is a synchronous loop hook whose plugin returns a non-undefined hook. The plugin’s callback is executed until it returns undefined.

Let’s change the hook of start to SyncLoopHook.

import { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook } from 'tapable'; Export default class Car {constructor() {this.hooks = {start: new SyncLoopHook(), new SyncWaterfallHook(["newSpeed"]), brake: new SyncBailHook(), }; } start() { this.hooks.start.call(); } accelerate(speed) { this.hooks.accelerate.call(speed); } brake() { this.hooks.brake.call(); }}Copy the code
// index.js import Car from './Car'; let index = 0; const car = new Car(); Car.links.start. tap('startPlugin1', () => {console.log('start '); if (index < 5) { index++; return 1; }}); // This time we get a broken car, it takes 6 starts to start successfully. Car.hooks. Start. tap('startPlugin2', () => {console.log(' started successfully '); }); car.start(); // Print 'start' 6 times and 'start successfully' once.Copy the code

Asynchronous hooks

When there is asynchrony in the plug-in callback function. You need to use asynchronous hooks.

The fifth hook AsyncParallelHook

AsyncParallelHook A plug-in that handles asynchronous parallel execution.

We add calculateRoutes in the Car class, using AsyncParallelHook. Write a calculateRoutes method that triggers hook execution when the callAsync method is called. It is possible to pass a callback that will be called when all plug-ins have finished executing.

// Car.js import { ... AsyncParallelHook, } from 'tapable'; export default class Car { constructor() { this.hooks = { ... calculateRoutes: new AsyncParallelHook(), }; }... calculateRoutes(callback) { this.hooks.calculateRoutes.callAsync(callback); }}Copy the code
// index.js import Car from './Car'; const car = new Car(); Car. Hooks. CalculateRoutes. TapAsync (' calculateRoutesPlugin1 '(the callback) = > {setTimeout (() = > {the console. The log (' computing route 1'); callback(); }, 1000); }); Car. Hooks. CalculateRoutes. TapAsync (' calculateRoutesPlugin2 '(the callback) = > {setTimeout (() = > {the console. The log (' computing route 2'); callback(); }, 2000); }); Car.calculateroutes (() => {console.log(' final callback '); }); // Will print 'calculate route 1' at 1s. At 2s, print 'Calculate route 2'. Then print 'Final callback'Copy the code

I think the essence of AsyncParallelHook is this final callback. When all asynchronous tasks are completed, the following code is executed in the final callback. You can ensure that all plug-in code is executed before executing some logic. If you don’t need the final callback to execute some code, SyncHook is fine, because you don’t care when the code in your plug-in is finished.

AsyncParallelHook Promise method

Use AsyncParallelHook in addition to using tapAsync/callAsync. You can also use the tapPromise/promise approach.

The code is as follows:

// Car.js import { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, } from 'tapable'; export default class Car { constructor() { this.hooks = { ... calculateRoutes: new AsyncParallelHook(), }; }... calculateRoutes() { return this.hooks.calculateRoutes.promise(); }}Copy the code
// index.js import Car from './Car'; const car = new Car(); car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', () => { return new Promise((resolve, Reject) => {setTimeout(() => {console.log(' calculate route 1'); resolve(); }, 1000); }); }); car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', () => { return new Promise((resolve, Reject) => {setTimeout(() => {console.log(' calculate route 2'); resolve(); }, 2000); }); }); Car.calculateroutes ().then(() => {console.log(' final callback '); });Copy the code

TapAsync /callAsync is the same as tapAsync/callAsync.

The sixth hook AsyncParallelBailHook

This hook function is similar to AsyncParallelHook, except that the hook registered by the first plug-in is bailed and the final callback is called, regardless of whether the other plug-ins have finished executing.

// Car.js import { AsyncParallelBailHook, } from 'tapable'; export default class Car { constructor() { this.hooks = { drift: new AsyncParallelBailHook(), }; } drift(callback) { this.hooks.drift.callAsync(callback); }}Copy the code
// index.js import Car from './Car'; const car = new Car(); Car.links.drift.tapasync ('driftPlugin1', (callback) => {setTimeout(() => {console.log(' calculate route 1'); callback(1); }, 1000); }); Car.hooks. Drift.tapasync ('driftPlugin2', (callback) => {setTimeout(() => {console.log(' calculate route 2'); callback(2); }, 2000); }); Car.drift ((result) => {console.log(' final callback ', result); }); // Print 'calculated route 1' for 1s, then print' final callback 1', then print 'calculated route 2' for 2sCopy the code

I would like to thank nuggets user @xiaoquilt for asking in the comments, which made me realize my previous understanding was wrong. I thought it was based on the completion time of plug-in execution, then I went to Github to submit an issue to Webpack, and got the answer from the author of Webpack. They meant that the fuse was based on the time of plug-in registration, and the fuse would be cut after the execution of the first registered plug-in. Here’s my question on Github: github.com/webpack/tap…

AsyncParallelHook can also be used as a promise.

The seventh hook AsyncSeriesHook

Speaking of parallel, there must be serial. Plug-ins are executed one by one.

The experimental code is as follows:

// Car.js import { AsyncSeriesHook, } from 'tapable'; export default class Car { constructor() { this.hooks = { calculateRoutes: new AsyncSeriesHook(), }; } calculateRoutes() { return this.hooks.calculateRoutes.promise(); }}Copy the code
// index.js import Car from './Car'; const car = new Car(); car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', () => { return new Promise((resolve, Reject) => {setTimeout(() => {console.log(' calculate route 1'); resolve(); }, 1000); }); }); car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', () => { return new Promise((resolve, Reject) => {setTimeout(() => {console.log(' calculate route 2'); resolve(); }, 2000); }); }); Car.calculateroutes ().then(() => {console.log(' final callback '); }); // After 1s, print calculated route 1, then 2s (not 2s, but 3s), print calculated route 2, and then immediately print the final callback.Copy the code

We’re just going to use the promise format, and we’ll do the same.

The eighth hook AsyncSeriesBailHook

Execute serially, and as soon as one plug-in returns a value, the final callback is called immediately, and execution of subsequent plug-ins is not continued.

The experimental code is as follows:

// Car.js import { AsyncSeriesBailHook, } from 'tapable'; export default class Car { constructor() { this.hooks = { calculateRoutes: new AsyncSeriesBailHook(), }; } calculateRoutes() { return this.hooks.calculateRoutes.promise(); }}Copy the code
// index.js import Car from './Car'; const car = new Car(); car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', () => { return new Promise((resolve, Reject) => {setTimeout(() => {console.log(' calculate route 1'); resolve(1); }, 1000); }); }); car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', () => { return new Promise((resolve, Reject) => {setTimeout(() => {console.log(' calculate route 2'); resolve(2); }, 2000); }); }); Car.calculateroutes ().then(() => {console.log(' final callback '); }); // After 1s, computationroute 1 is printed, and the final callback is immediately printed. Computationroute 2 is no longer executed.Copy the code

The ninth hook AsyncSeriesWaterfallHook

Execute sequentially, and the return value of the first plug-in is taken as an argument of the second plug-in.

The code is as follows:

// Car.js import { AsyncSeriesWaterfallHook, } from 'tapable'; export default class Car { constructor() { this.hooks = { calculateRoutes: New AsyncSeriesWaterfallHook(['home']); } calculateRoutes() { return this.hooks.calculateRoutes.promise(); }}Copy the code
// index.js import Car from './Car'; const car = new Car(); car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', (result) => { return new Promise((resolve, Reject) => {setTimeout(() => {console.log(' calculate route 1', result); resolve(1); }, 1000); }); }); car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', (result) => { return new Promise((resolve, Reject) => {setTimeout(() => {console.log(' calculate route 2', result); resolve(2); }, 2000); }); }); Car.calculateroutes ().then(() => {console.log(' final callback '); }); // After 1s, print calculated route 1 undefined, and after 2s print calculated route 2, 1, and then print the final callback immediately.Copy the code

The printed result is shown as follows:

Encapsulation plug-in

We have encapsulated the logic for registering plug-ins separately as follows:

Export default Class CalculateRoutesPlugin {// Call apply to register Apply (CAR) { car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin', (result) => { return new Promise((resolve, Reject) => {setTimeout(() => {console.log(' calculate route 1', result); Resolve (' Beijing '); }, 1000); }); }); }}Copy the code

Call in index.js:

// index.js import Car from './Car'; import CalculateRoutesPlugin from './CalculateRoutesPlugin'; const car = new Car(); const calculateRoutesPlugin = new CalculateRoutesPlugin(); calculateRoutesPlugin.apply(car); // The key logic of this section is car.calculateroutes ().then(() => {console.log(' final callback '); }); // It works fine, it prints' calculate route 1'Copy the code

At this point, the code and Webpack work pretty much the same way, with Car resembling Compiler/Compilation in Webpack. Index.js is a running class for Webpack, using our Car (analogically Compiler/Compilation), and using the injected CalculateRoutesPlugin (analogically Webpack’s various plug-ins). Finish packing.

Tapable

The tapable readme does not introduce a class called Tapable, but it is available, as follows

const {
  Tapable
} = require("tapable");
 
export default class Car extends Tapable {
    ...
}
Copy the code

If you look at the tapable source code, you can’t see this class, but if you switch to the Tapable -1 branch, you can see it.

In the Webpack source, Compiler and Compilation inherit from Tapable, just like Car above.

What does Tapable do? If I look at the source code, IT doesn’t do anything, just a sign that this class is a class that can register plug-ins.

There are no enhancements, but Car at this point has two limitations. As follows:

const car = new Car(); car.apply(); Tapable. Apply is deprecated. Call apply on the plugin directly instead car.plugin(); // Error Tapable. Plugin is deprecated. Use new API on 'Copy the code

These two methods are not allowed. I understand this is a limitation for Webpack and remind plugin authors to upgrade their plug-ins and use the latest practices.

Hook Types

Above we looked at the use of hooks, and here are some summaries. Let’s start with the type of hook.

Hooks are divided according to the execution logic of the registered plug-in

  1. Basic hook. The registered plug-ins are executed sequentially. Such as SyncHook, AsyncParallelHook, AsyncSeriesHook.

  2. Waterfall flow hook. The return value of the first plug-in is the input parameter of the second plug-in. Such as SyncWaterfallHook, AsyncSeriesWaterfallHook.

  3. Bail hook. The Bail hook is a plug-in that returns a value other than undefined and then does not proceed with the execution of subsequent plug-ins. The Bail is one of the uygurs. For example: SyncBailHook, AsyncSeriesBailHook

  4. Loop hooks. Loop through the plug-in until its return value is undefined. Such as SyncLoopHook.

To distinguish hooks in chronological order

  1. Synchronous hooks. Sync hook
  2. Asynchronous serial hooks. The hook at the beginning of AsyncSeries.
  3. Asynchronous parallel hooks. AsyncParallel begins with a hook.

An Interception

We can also add interceptors to hooks. A plug-in goes from registering the hook, to calling the hook, to responding to the plug-in. We can all listen in with interceptors.

car.hooks.calculateRoutes.intercept({ call: (... args) => { console.log(... args, 'intercept call'); }, // the plug-in responds when called. // register: (tap) => { console.log(tap, 'ntercept register'); return tap; },// the plug-in responds when registered with the TAP method. loop: (... args) => { console.log(... Args, 'Intercept loop')},// Loop hook's plug-in responds when called. Tap: (tap) => {console.log(tap, 'Intercept tap')} // The hook plug-in responds when called. })Copy the code

Context

Both plug-ins and interceptors can pass in the parameters of a context object that can be used to pass arbitrary values to subsequent plug-ins and interceptors.

MyCar. Hooks. Accelerate the intercept ({context: true, / / configuration to enable the context object tap here: (context, tapInfo) => {if (context) {// Here we can get the context object context.hasmuffler = true; }}}); myCar.hooks.accelerate.tap({ name: "NoisePlugin", context: True}, (context, newSpeed) => {// Here we can get the context object in the interceptor, and then we can use its value in the plug-in to do something about it. if (context && context.hasMuffler) { console.log("Silence..." ); } else { console.log("Vroom!" ); }});Copy the code

Experimental project source code

Address: gitee.com/DaBuChen/ta…

Run NPM install and NPM Run Server to start the project and view the printed information in the browser tools.

conclusion

So much for the simple use of Tapable. It provides powerful support for the plug-in mechanism, allowing us not only to register various plug-ins with the body (Car), but also to control the relationship between plug-ins and their own timing.

This library is perfect for Webpack, which is a collection of plug-ins that are organized by tapable and called at the right time.