Plug-ins are a tapable feature of Webpack. Webpack itself is built on top of the same plugin system you use in your WebPack configuration! Plug-ins are designed to solve other things that loader cannot implement.

Here’s what the website explains, and it sounds pretty cool. So how to use and write the desired plugin? Then I found the following demo on the official website.

const pluginName = 'ConsoleLogOnBuildWebpackPlugin'; class ConsoleLogOnBuildWebpackPlugin { apply(compiler) { compiler.hooks.run.tap(pluginName, Compilation => {console.log(' Webpack build process started! '); }); } } module.exports = ConsoleLogOnBuildWebpackPlugin;Copy the code

After reading it, I fell into a big doubt

  • applyWhen was it called?
  • compilerIs what?
  • The incomingcompilationWhat is the

The webpack plug-in is a JavaScript object with the Apply method. The Apply method is called by the Webpack Compiler and the Compiler object is accessible throughout the compile life cycle.

To understand these issues in more depth, go back to the source and first look at when Apply is implemented

When is apply invoked?

All the way from webpack bin/webpack. Js – > webpack – cli bin/cli. Js – > webpack lib/webpack. Js – >

Then I found the following code. How to find the destination file in the source code? Please read main and bin in package.json for yourself.

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

Options is the webpack configuration we passed in. Plugin is a class. Add the apply method to the custom plugin because webPack will execute the apply method. Pass in the compiler object.

So we have the second problem what is compiler?

The compiler is what?

Compiler inherits from Tapable and contains 26 life cycles in the hooks attribute, all of which are instances of functions in Tapable.

options.context = process.cwd();        
compiler = new Compiler(options.context);
Copy the code

bin/Compiler.js

const {
    Tapable,
    ...
    AsyncSeriesHook
} = require("tapable");

class Compiler extends Tapable {
    constructor(context) {
        super();
        this.hooks = {
            ...
            /** @type {AsyncSeriesHook<Compiler>} */
            run: new AsyncSeriesHook(["compiler"]),
            ...
}
Copy the code

To better understand how these life cycles work, take a look at tapable, the core of WebPack.

What does Tapable do?

Open Tapable and you’ll see a bunch of methods…

exports.__esModule = true;
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
Copy the code

Let’s take SyncHook as an example

How is Tapble used?

All hook constructors take one argument, which is an array.

const hook = new SyncHook(["arg1", "arg2", "arg3"]);
Copy the code

The official recommendation is to place the hooks on the hooks attribute of the class instance

const {SyncHook} = require("tapable"); class Car { constructor() { this.hooks = { accelerate: new SyncHook(["newSpeed"]), brake: new SyncHook() }; } } const myCar = new Car(); Tap ("WarningLampPlugin", () => console.log(" slow down ")); mycar.links.brake. Tap ("WarningLampPlugin", () => console.log(" slow down ")); MyCar. Hooks. Accelerate. Tap (" LoggerPlugin newSpeed = > console. The log (` speed to ${newSpeed} `)) myCar. Hooks. The brake. The call (); myCar.hooks.accelerate.call("100km/h"); // Slow down // speed to 100km/hCopy the code

Tapable hooks are based on publish/subscribe in nature, registering events through tap, tapAsync, tapPromise and other methods, and distributing events through Call, callAsync, promise and other methods

Deep understanding of Tapble

These methods in Tapble follow certain rules:

  • If the hook is attachedSeries“, the registered event function is executed serially.SeriesComes with serial meaning.
  • If the hook is attachedparallelThe registered event functions are executed in parallel.parallelIt means parallel by itself.
  • If the hook hasBailWhen any function being executed returns anything, it will stop executing the rest of the hook if it returnsundefinedThe execution will continue.B``ailInsurance means.
  • If the hook hasWaterfall“, which passes the return value of each function to the next.WaterfallWaterfall means waterfall.
  • If the hook hasLoopTo indicate that the function will be executed in a loop under certain circumstances,LoopIt means cycle.

Understanding synchronization methods

Tapable contains four synchronous hooks

Synchronized hooks support registering events using TAP and distributing events using Call.

exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
Copy the code

SyncBailHook,

Execution of the remaining hooks stops when any function being executed returns anything, and continues if undefined is returned.

const {SyncBailHook} = require("tapable"); class Car { constructor() { this.hooks = { accelerate: new SyncBailHook(), }; } } const myCar = new Car(); myCar.hooks.accelerate.tap("100km/h", () => { console.log("100km/h"); Return "return value "; }); myCar.hooks.accelerate.tap("110km/h", () => { console.log("110km/h"); return }) myCar.hooks.accelerate.call(); // 100km/hCopy the code

SyncWaterfallHook,

The SyncWaterfallHook will pass the return value to the next function when it is run.

const {SyncWaterfallHook} = require("tapable"); class Car { constructor() { this.hooks = { accelerate: new SyncWaterfallHook(["speed"]), }; } } const myCar = new Car(); myCar.hooks.accelerate.tap("100km/h", (speed) => { console.log(speed); Return "accelerate to 110km/h"; }); myCar.hooks.accelerate.tap("110km/h", (speed) => { console.log(speed); return }) myCar.hooks.accelerate.call("100km/h"); // 100km/h // accelerate to 110km/hCopy the code

SyncLoopHook,

The SyncLoopHook hook makes it possible to execute functions repeatedly. If a function does not return undefined, the entire task queue will start again until all functions return undefined.

const {SyncLoopHook} = require("tapable");

class Car {
    constructor() {
        this.hooks = {
            accelerate: new SyncLoopHook(),
        };
    }
}

const myCar = new Car();
myCar.hooks.accelerate.tap("100km/h", () => {
    console.log("100km/h");
    return Math.random() > 0.5 ? "" : undefined
});
myCar.hooks.accelerate.tap("110km/h", () => {
    console.log("110km/h");
    return Math.random() > 0.5 ? "" : undefined
})

myCar.hooks.accelerate.call();
const {SyncLoopHook} = require("tapable");

class Car {
    constructor() {
        this.hooks = {
            accelerate: new SyncLoopHook(),
        };
    }
}

const myCar = new Car();
myCar.hooks.accelerate.tap("100km/h", () => {
    console.log("100km/h");
    return Math.random() > 0.5 ? "" : undefined
});
myCar.hooks.accelerate.tap("110km/h", () => {
    console.log("110km/h");
    return Math.random() > 0.5 ? "" : undefined
})

myCar.hooks.accelerate.call();
// 100km/h
// 110km/h
// 100km/h
// 100km/h
// 100km/h
// 110km/h
// 100km/h
// 110km/h
Copy the code

Understanding asynchronous methods

Asynchronous hooks support registering events using tapAsync and tapPromie, and issuing events using callAsync and Promise.

The last argument to the callAsync method is a callback function that is executed after the entire asynchronous task has completed.

All hook functions accept a done function as an argument at runtime. Only after these done functions are called will the asynchronous task be considered complete and the callAsync callback function be executed.

exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
Copy the code

AsyncParallelHook understand

In order to show the difference between serial and parallel, setTimeout is used as a helper.

const {AsyncParallelHook} = require("tapable"); class Car { constructor() { this.hooks = { accelerate: new AsyncParallelHook(["speed"]), }; } } const myCar = new Car(); myCar.hooks.accelerate.tapAsync("100km/h", (speed, done) => { console.log(1, speed); setTimeout(() => { console.log(1, "done"); done() }, 2000) }); myCar.hooks.accelerate.tapAsync("110km/h", (speed, done) => { console.log(2, speed); setTimeout(() => { console.log(2, "done"); done() }, 2000) }) myCar.hooks.accelerate.tapAsync("120km/h", (speed, done) => { console.log(3, speed); setTimeout(() => { console.log(3, "done"); Done ()}, 2000)}) myCar. Hooks. Accelerate. CallAsync (110 km/h, () = > {the console. The log (" rapid completion "); }); / / 1 '110 km/h' 2 '110 km/h / / / / 3' 110 km/h '1' done '/ / / / 2' done '3' done '/ / / / speed up to completeCopy the code

AsyncParallelBailHook understand

Parallel hook is a hook for Parallel hook. Parallel Hook is a hook for Parallel Hook.

const {AsyncParallelBailHook} = require("tapable"); class Car { constructor() { this.hooks = { accelerate: new AsyncParallelBailHook(["speed"]), }; } } const myCar = new Car(); myCar.hooks.accelerate.tapAsync("100km/h", (speed, done) => { console.log(1, speed); setTimeout(() => { console.log(1, "done"); done() }, 2000) }); myCar.hooks.accelerate.tapAsync("110km/h", (speed, done) => { console.log(2, speed); setTimeout(() => { console.log(2, "done"); done() }, 2000) }) myCar.hooks.accelerate.tapAsync("120km/h", (speed, done) => { console.log(3, speed); setTimeout(() => { console.log(3, "done"); Done ()}, 2000)}) myCar. Hooks. Accelerate. CallAsync (110 km/h, () = > {the console. The log (" rapid completion "); }); / / 1 '110 km/h' 2 '110 km/h / / / / 3' 110 km/h / / 1 'done' 2 'done' / / / / rapid completion / / 3 'done'Copy the code

AsyncSeriesHook understand

It should be easier to understand by now that the done argument will do the Bail effect, suspending the current pass and calling the callAsync callback directly.

const {AsyncSeriesHook} = require("tapable"); class Car { constructor() { this.hooks = { accelerate: new AsyncSeriesHook(["speed"]), }; } } const myCar = new Car(); myCar.hooks.accelerate.tapAsync("100km/h", (speed, done) => { console.log(1, speed); setTimeout(() => { console.log(1, "done"); done() }, 2000) }); myCar.hooks.accelerate.tapAsync("110km/h", (speed, done) => { console.log(2, speed); setTimeout(() => { console.log(2, "done"); done() }, 2000) }) myCar.hooks.accelerate.tapAsync("120km/h", (speed, done) => { console.log(3, speed); setTimeout(() => { console.log(3, "done"); Done ()}, 2000)}) myCar. Hooks. Accelerate. CallAsync (110 km/h, () = > {the console. The log (" rapid completion "); }); / / 1 '110 km/h / / 1' done '2' 110 km/h / / / / 2 / / 3 'done' '110 km/h' 3 'done' / / / / speed up to completeCopy the code

AsyncSeriesWaterfallHook understand

Serial plus parameter passing

const {AsyncSeriesWaterfallHook} = require("tapable"); class Car { constructor() { this.hooks = { accelerate: new AsyncSeriesWaterfallHook(["speed"]), }; } } const myCar = new Car(); myCar.hooks.accelerate.tapAsync("100km/h", (speed, done) => { console.log(1, speed); setTimeout(() => { console.log(1, "done"); done(null, "110km/h") }, 2000) }); myCar.hooks.accelerate.tapAsync("110km/h", (speed, done) => { console.log(2, speed); setTimeout(() => { console.log(2, "done"); done(null, "120km/h") }, 2000) }) myCar.hooks.accelerate.tapAsync("120km/h", (speed, done) => { console.log(3, speed); setTimeout(() => { console.log(3, "done"); Done ()}, 2000)}) myCar. Hooks. Accelerate. CallAsync (100 km/h, () = > {the console. The log (" rapid completion "); }); / / 1 '10 km/h / / 1' done '2' 110 km/h / / / / 2 / / 3 'done' '120 km/h' 3 'done' / / / / speed up to completeCopy the code

The combination of tapPromise and promise is just a promise that needs to be returned. You can understand this by following the example below

const {AsyncSeriesWaterfallHook} = require("tapable"); class Car { constructor() { this.hooks = { accelerate: new AsyncSeriesWaterfallHook(["speed"]), }; } } const myCar = new Car(); myCar.hooks.accelerate.tapPromise("100km/h", (speed) => { return new Promise((res) => { console.log(1, speed); setTimeout(() => { console.log(1, "done"); res("110km/h") }, 2000) }) }); myCar.hooks.accelerate.tapPromise("110km/h", (speed) => { return new Promise((res) => { console.log(2, speed); setTimeout(() => { console.log(2, "done"); res("120km/h") }, 2000) }) }) myCar.hooks.accelerate.tapPromise("120km/h", (speed) => { return new Promise((res) => { console.log(3, speed); setTimeout(() => { console.log(3, "done"); Res ()}, 2000)})}) myCar. Hooks. Accelerate. Promise (" 100 km/h) ". Then (() = > {the console. The log (" rapid completion "); });Copy the code

It’s easy to look back at the lifecycle hooks in WebPack.

Look at the Compiler

this.hooks = {
            ...
            /** @type {SyncBailHook<Compilation>} */
            shouldEmit: new SyncBailHook(["compilation"]),
            /** @type {AsyncSeriesHook<Stats>} */
            done: new AsyncSeriesHook(["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, Buffer>} */
           ...
    };
Copy the code

In conjunction with [Compiler], we now know the general process of WebPack execution. The plugin we wrote will be called by Call Promise callAsync at some stage of the life cycle of WebPack execution.

But to make your own custom components more powerful, you also need to understand the parameter compilation that is passed in.

Compilation

The Compilation object represents a resource version build. When running the WebPack development environment middleware, each time a file change is detected, a new compilation is created, resulting in a new set of compilation resources. A compilation object represents the current module resources, the compile-generated resources, the changing files, and the state information of the dependencies being tracked. The Compilation object also provides many callbacks to key steps that plug-ins can choose to use when doing custom processing.

Compilation also inherits from Tapable and has its own lifecycle methods, which have more lifecycle methods but are understood in a similar way to Compiler because they are based on Tapble hooks. “Compilation”

Custom Components

Open a browser after the compilation and pass in the URL for the response.

const pluginName = 'OpenBrowserWebpackPlugin'; var child_process = require("child_process"); class OpenBrowserWebpackPlugin { constructor(options) { this.options = options } apply(compiler) { compiler.hooks.done.tap(pluginName, compilation => { let cmd = ""; switch (process.platform) { case 'wind32': cmd = 'start'; break; case 'linux': cmd = 'xdg-open'; break; case 'darwin': cmd = 'open'; break; } console.log(' Open browser! '); child_process.exec(cmd + ' ' + this.options.url); }); } } module.exports = OpenBrowserWebpackPlugin;Copy the code

Other more complex plug-in development, explore together!!

If you find it helpful, please leave a like!!