Meet Tapable

Webpack’s success lies not only in its powerful packaging build capability, but also in its flexible plug-in mechanism.

Big question? 🤔 ️

  1. Is the WebPack plug-in configuration in order?
  2. When you write a plug-in, do you always write it as a class?
  3. Why do plug-in prototypes have to have apply methods?

Webpack has its own event flow mechanism, which works by connecting plug-ins together, and Tapable is at the heart of this.

1. Webpack plugin mechanism

1.1 When the Webpack plug-in is inserted

When the Webpack configuration file is defined, the Compiler object is instantiated and is globally unique. The Compiler contains the configuration of the currently running Webpack, and the plug-in is added to the running flow of Webpack when the Compiler object is instantiated. Look at the source code (lib/webpack.js:61) :

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);
    // Check whether the options.plugins configuration exists
    if (Array.isArray(options.plugins)) {
        // Iterate over the plugin
        for (const plugin of options.plugins) {
            // There are two ways to write plug-ins: functions and object instances
            if (typeof plugin === "function") {
                plugin.call(compiler, compiler);
            } else {
                // In the case of an object instance, you need to define an apply method in the constructor as an entry point for webPack to call the plug-in. The Compiler object is then passed as a parameter.
                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

Starting with traversing options.plugins, you can see that plug-ins are called in two ways:

  • A plug-in can be a function that is scoped to the current compiler, which is also passed as a parameter.
  • Object, and the apply method is on the prototype chain. Apply is called and the current Compiler is passed in.

So, this explains why plug-ins need to use new, and they define the Apply method.

1.2 What is done in Compiler?

As you can see from the above, the plugin passes in the currently instantiated compiler instance at injection time, so what does that do? (Source location: lib/Compiler.js:117)

class Compiler {
  constructor(context) {
    // object. freeze Freeze objects, which cannot be modified, deleted, or added, including prototypes.
    this.hooks = Object.freeze({
      // ...
      
      / * *@type {SyncBailHook<[Compilation], boolean>} * /
      shouldEmit: new SyncBailHook(["compilation"]),
      / * *@type {SyncHook<[CompilationParams]>} * /
      compile: new SyncHook(["params"]),
      / * *@type {AsyncParallelHook<[Compilation]>} * /
      make: new AsyncParallelHook(["compilation"]),
      / * *@type {AsyncSeriesHook<[Compilation]>} * /
      emit: new AsyncSeriesHook(["compilation"])
      
      // ...})}emitAssets(compilation, callback) {
    let outputPath;
    // If a plugin listens for the EMIT hook, this will be emitted.
    this.hooks.emit.callAsync(compilation, err= > {
      if (err) return callback(err);
      outputPath = compilation.getPath(this.outputPath, {});
      mkdirp(this.outputFileSystem, outputPath, emitFiles); }); }}Copy the code

It can be seen from the above source code that each Compiler instance contains many hooks. Webpack formally relies on these hooks to complete the code construction, and different hooks can be used to complete many special customization requirements when writing plugin.

2. Tapable

Tapable’s core idea is similar to EventEmitter from Node.js, a basic publish/subscribe model.

const events = require('events');
const emitter = new events.EventEmitter();

// Register event listeners and corresponding callback functions
emitter.on('demo'.params= > {
  console.log('Input result', params);
})
// Trigger the event and pass in the argument
emitter.emit('demo'.'meet Tapable');
Copy the code

3. Hook introduction of Tapable

Tapable has the following 10 hooks:

How to use

Let’s start with the simplest hooks. The hooks are pretty much the same. Understand one and the rest are easy to understand.

3.1 Usage of each Hook
  • Basic: Does not care about the return value of the callback function. SyncHook, AsyncParallelHook, AsyncSeriesHook
  • Bail: Execution is terminated if the return value of one of the listening functions is not undefined. SyncBailHook, AsyncParallelBailHook, AsyncSeriesBailHook
  • Waterfall: if the return value of the previous listener function is not undefined, it acts as the first parameter of the next listener function. SyncWaterfallHook, AsyncSeriesWaterfallHook
  • Loop: If the return value of any listener function is not undefined, the execution stops and starts from the beginning until all listeners return undefined. SyncLoopHook, AsyncSeriesLoopHook

3.2 SyncHook
const { SyncHook } = require('tapable');

let hook = new SyncHook(['name']);
hook.tap('demo'.function(params) {
  console.log('demo', params);
});
hook.tap('demo2'.function(params) {
  console.log('demo2', params);
  return true;
});
hook.tap('demo3'.function(params) {
  console.log('demo3', params);
});

hook.call('hello SyncHook'); The output/* * demo hello SyncHook * demo2 hello SyncHook * demo3 hello SyncHook */
Copy the code

According to the above examples, the implementation principle is simply written down:

  class SyncHook {
    constructor(args = []) {
      this._args = args;
      this.tasks = [];
    }
    tap(name, task) {
      this.tasks.push(task);
    }
    call(. args) {
      const params = args.slice(0.this._args.length);
      this.tasks.forEach(task= >task(... params)) } }Copy the code
3.3 SyncBailHook
const { SyncBailHook } = require('tapable');
let hook = new SyncBailHook(['name']);

hook.tap('demo'.function(params) {
  console.log('demo', params);
})
hook.tap('demo2'.function(params) {
  console.log('demo2', params);
  return true;
})
hook.tap('demo3'.function(params) {
  console.log('demo3', params);
})

hook.call('hello SyncBailHook'); The output/* * demo hello SyncBailHook * demo2 hello SyncBailHook */
Copy the code

As shown above, SyncBailHook skips all of the following logic when it encounters a callback that does not return undefined. The implementation principle is as follows:

class SyncBailHook {
  constructor(args) {
    this._args = args;
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call() {
    const args = Array.from(arguments).slice(0.this._args.length);
    for (let i = 0; i < this.tasks.length; i++) {
      const result = this.tasks[i](... args);if(result ! = =undefined) break; }}}Copy the code

3.4 summarize

With the above two hooks, you can see that Tapable provides a variety of hooks to manage how events are executed. Tapable’s core function is to control the execution flow between a series of registered events. For example, three registered events can be executed asynchronously (serial), synchronously (sequential), or through the return value of the callback function to control the execution flow, which can be implemented through the Hook provided by Tapable.

4. Application examples

4.1 the plugin for

SRC /index.js:607 example: the copyWebpackPlugin that we often use.

class CopyPlugin {
  // ...
  apply(compiler) {
    compiler.hooks.thisCompilation.tap(pluginName, (compilation) = > {
      // ...})}// ...
}
Copy the code

The CopyPlugin has a apply method that listens to thisCompilation in the compiler, triggering thisCompilation when the hook’s call method is executed in the webpack.

5. Force jokes

  • Once listeners are added, they cannot be removed. The official team believes that Tapable is for static plugins, and removing listeners is not consistent with the design philosophy, so this greatly limits tapable’s expansion.
  • The same/asynchronous hooks can be mixed, which is a bit confusing.

So here, the end of sharing, you ~ learn waste? Just give it a thumbs up and go