Previous articles:

  • [10,000 words summary] One article thoroughly understand the core principle of Webpack

If you liked this post, please like it, follow it, retweet it, and there will be more in-depth webpack articles to come.

Most of the paper focuses on Tapable framework, enumerates in detail the hooks provided by Tapable and the characteristics of various types of hooks, running logic, implementation principle, and further discusses the role of Tapable framework in Webpack. Then reveal the core logic of webPack plug-in architecture.

Reading this article, you will:

  • Understand the basics of the WebPack plug-in architecture
  • Understand the characteristics of different hooks and why WebPack needs to incorporate multiple callback schemes
  • The next time you look at the official WebPack documentation or source code, you can quickly infer the hook’s purpose simply by looking at its type name

Introduction to the

There’s a lot of literature on the web that categorizes WebPack’s plug-in architecture as an “event/subscription” model, which I think is a bit biased. The subscription pattern is a loosely coupled architecture in which publishers only publish event messages at specific times, and subscribers do not or rarely interact directly with events. For example, when we use HTML events, most of the time we only trigger business logic at this time and rarely invoke context operations.

The plug-in system of Webpack is a strong coupling architecture based on Tapable implementation. It will attach enough context information when triggering hooks at a specific time. The hook callback defined by the plug-in can or can only produce side effect with the data structure and interface interaction behind these contexts. This in turn affects compilation status and subsequent processes.

This article will focus on Tapable, in-depth explanation of Tapable hook types, features, and what logic is used to handle callbacks respectively, and further deduce from this

What is a plug-in

Morphologically, a plug-in is usually a class with the apply function:

class SomePlugin {
    apply(compiler) {
    }
}
Copy the code

After startup, Webpack will call the apply function of the plug-in object one by one in the order registered, and pass in the compiler object at the same time. The plug-in developer can use this as a starting point to reach any hooks defined in Webpack, for example:

class SomePlugin {
    apply(compiler) {
        compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => {
        })
    }
}
Copy the code

Observe the core statement compiler. Hooks. ThisCompilation. Tap, including thisCompilation tapable warehouse provide hooks object; Tap is a subscription function that registers callbacks.

Webpack’s plug-in architecture is based on the hooks that Tapable provides, so it’s important to familiarize yourself with the hook types and features that Tapable provides.

Tapable full resolution

Tapable is the core support of Webpack plug-in architecture, but its source code is actually very small. In essence, it is superimpose various specialized logic around subscription/publication mode, and adapt to the complex interaction requirements between event source and processor under Webpack system. For example, some scenarios need to support passing the results of the previous handler to the next callback handler; Some scenarios need to support asynchronous parallel invocation of these callback handlers.

To understand the plug-in architecture of WebPack, we must first understand what types of hooks Tapable provides, what features each type has, and which application scenarios are suitable. Fortunately, this logic is not complicated, so let’s expand it.

Basic usage

The following steps are usually required to use Tapable:

  • Creating a Hook instance
  • Call the subscription interface to register callbacks, including:Tap, tapAsync, tapPromise
  • Invoking the publish interface triggers a callback, including:Call, callAsync, Promise

Here’s an example:

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

// 1. Create hook instance
const sleep = new SyncHook();

// 2. Invoke the subscription interface to register the callback
sleep.tap("test".() = > {
  console.log("callback A");
});

// 3. Invoke the publish interface to trigger the callback
sleep.call();

// Result:
// callback A
Copy the code

Tap is used to register callbacks, call is used to trigger callbacks, and asynchronous style tapAsync/callAsync and promise style tapPromise/ PROMISE can also be used in some hooks, depending on the hook type.

Tapable Hook type

Tabable provides the following types of hooks:

The name of the
Introduction to the
statistical
SyncHook
Synchronous hooks
Webpack appear 71 times, such as Compiler.hooks.com pilation
SyncBailHook
Synchronous fusing hook
Webpack appear 66 times, such as Compiler. Hooks. ShouldEmit
SyncWaterfallHook
Synchronous waterfall flow hook
Webpack there were 37, such as Compilation. The hooks. AssetPath
SyncLoopHook
Sync loop hook
Not used in Webpack
AsyncParallelHook
Asynchronous parallel hook
Webpack appears only once: Compiler.hooks. Make
AsyncParallelBailHook
Asynchronous parallel fuse hook
Not used in Webpack
AsyncSeriesHook
Asynchronous serial hook
Webpack appears 16 times, as in Compiler.hooks. Done
AsyncSeriesBailHook
Asynchronous serial fuse hook
Not used in Webpack
AsyncSeriesLoopHook
Asynchronous serial loop hook
Not used in Webpack
AsyncSeriesWaterfallHook
Asynchronous serial waterfall flow hook
Webpack a total of five times, such as NormalModuleFactory hooks. BeforeResolve

The Tapable repository readme has two rules for classifying hooks:

  • According to the callback logic, it can be divided into:
    • Basic type, without nameWaterfall/Bail/LoopKeywords, and usuallySubscribe/callbackThe pattern is similar, calling callbacks one by one in hook registration order
  • waterfallType: The return value of the previous callback is carried into the next callback
  • bailType: calls callbacks one by one. Returns not if any callbacks are calledundefinedValue terminates subsequent calls
  • loopType: successive, circular calls until all callback functions returnundefined
  • The second dimension, in parallel with the way callbacks are executed, is divided into:
    • sync: Synchronous execution. Callbacks are executed one by one after startup. Supportedcall/tapCall statement
    • async: Executes asynchronously and supports incomingcallbackpromiseStyle of asynchronous callback functions supportedcallAsync/tapAsyncpromise/tapPromiseTwo kinds of call statements

All hooks can be named within these two rules. For plug-in developers, different types of hooks directly affect how callbacks are written and how plug-ins interact with other plug-ins, but some basic capabilities and concepts are common: tap/ Call, Intercept, Context, dynamic compilation, etc. Next, we examine the characteristics of each hook in both synchronous and asynchronous dimensions.

Synchronous hooks

SyncHookhook

Basic logic

SyncHook is a simple hook that invokes callbacks one by one in the order in which they were registered, regardless of their return value.

function syncCall() {
  const callbacks = [fn1, fn2, fn3];
  for (let i = 0; i < callbacks.length; i++) {
    constcb = callbacks[i]; cb(); }}Copy the code

The sample

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

class Somebody {
  constructor() {
    this.hooks = {
      sleep: new SyncHook(),
    };
  }
  sleep() {
    // Trigger the callback
    this.hooks.sleep.call(); }}const person = new Somebody();

// Register a callback
person.hooks.sleep.tap("test".() = > {
  console.log("callback A");
});
person.hooks.sleep.tap("test".() = > {
  console.log("callback B");
});
person.hooks.sleep.tap("test".() = > {
  console.log("callback C");
});

person.sleep();
// Output result:
// callback A
// callback B
// callback C
Copy the code

In this example, Somebody initializes a sleep hook and registers three consecutive callbacks by calling sleep.tap. After sleep.call is triggered by calling person.sleep(), Tapable performs three callbacks in order of registration.

Asynchronous style

In the above example, the call function of the hook is used to trigger the callback. We can also choose an asynchronous style callAsync. Choosing call or callAsync does not affect the execution logic of the callback: The only difference between the two is that callAsync requires a callback function to handle exceptions that the callback queue might throw:

/ / call style
try {
  this.hooks.sleep.call();
} catch (e) {
    // Error handling logic
}
/ / callAsync style
this.hooks.sleep.callAsync((err) = > {
  if (err) {
    // Error handling logic}});Copy the code

Since the invocation method does not follow the rules of the hook itself, there is no need for hook users to care whether the provider is using Call or callAsync. The above example can be adapted to the callAsync scenario with simple modifications:

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

class Somebody {
  constructor() {
    this.hooks = {
      sleep: new SyncHook(),
    };
  }
  sleep() {
    // Trigger the callback
    this.hooks.sleep.callAsync((err) = > {
      if (err) {
        console.log(`interrupt with "${err.message}"`); }}); }}const person = new Somebody();

// Register a callback
person.hooks.sleep.tap("test".(cb) = > {
  console.log("callback A");
  throw new Error("I just want to make a mistake.");
});
// If the first callback fails, subsequent callbacks will not be executed
person.hooks.sleep.tap("test".() = > {
  console.log("callback B");
});

person.sleep();

// Output result:
// callback A
// Interrupt with "I'm sorry"
Copy the code

SyncBailHookhook

Basic logic

The Bail hook is a type of hook that returns a value other than undefined in a callback queue. If the bail hook returns a value other than undefined in a callback queue, the hook will interrupt the subsequent processing and return the value directly, which is represented by a pseudo-code:

function bailCall() {
  const callbacks = [fn1, fn2, fn3];
  for (let i in callbacks) {
    const cb = callbacks[i];
    const result = cb(lastResult);
    if(result ! = =undefined) {
      / / fuse
      returnresult; }}return undefined;
}
Copy the code

The sample

The sequence and rules of SyncBailHook calls are similar to those of SyncHook. The first difference is that SyncBailHook adds circuit breaker logic, such as:

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

class Somebody {
  constructor() {
    this.hooks = {
      sleep: new SyncBailHook(),
    };
  }
  sleep() {
    return this.hooks.sleep.call(); }}const person = new Somebody();

// Register a callback
person.hooks.sleep.tap("test".() = > {
  console.log("callback A");
  / / striking point
  Return any value other than undefined interrupts the callback queue
  return 'Return value: tecvan'
});
person.hooks.sleep.tap("test".() = > {
  console.log("callback B");
});

console.log(person.sleep());

// Result:
// callback A
// Return value: tecvan
Copy the code

Second, compared to SyncHook, SyncBailHook returns the fuse value to the call function when it is finished running. For example, in line 20, the return value returned by callback A: tecvan becomes the result of the call to this.links.sleep.call.

Webpack scenario parsing

SyncBailHook typically used in publishers need to care about to subscribe to the callback scenario run results, webpack inside have 99 places use the hooks, for example: compiler. Hooks. ShouldEmit, corresponding call statements:

class Compiler {
  run(callback) {
    / /...

    const onCompiled = (err, compilation) = > {
      if (this.hooks.shouldEmit.call(compilation) === false) {
        // ...}}; }}Copy the code

Here webpack determines whether to perform subsequent operations based on the result of the shouldEmit hook. Other scenarios have similar logic, such as:

  • NormalModuleFactory.hooks.createModule: Expected to return a newmoduleobject
  • Compilation.hooks.needAdditionalSeal: Expected returnboolValue to determine whether to enterunsealstate
  • Compilation.hooks.optimizeModules: Expected returnboolValue to determine whether the optimization operation continues

SyncWaterfallHook hooks

Basic logic

The execution logic of the Waterfall hook is similar to that of the Lodash flow function, which basically passes the return value of the previous function as a parameter to the next function. It can be simplified into the following code:

function waterfallCall(arg) {
  const callbacks = [fn1, fn2, fn3];
  let lastResult = arg;
  for (let i in callbacks) {
    const cb = callbacks[i];
    // The result of the last execution is passed as an argument to the next function
    lastResult = cb(lastResult);
  }
  return lastResult;
}
Copy the code

After understanding the above logic, the characteristics of SyncWaterfallHook are clear:

  1. The result of the previous function is carried over to the next function
  2. The result of the last callback is returned as the result of the call call

The sample

Here’s an example:

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

class Somebody {
  constructor() {
    this.hooks = {
      sleep: new SyncWaterfallHook(["msg"]),}; }sleep() {
    return this.hooks.sleep.call("hello"); }}const person = new Somebody();

// Register a callback
person.hooks.sleep.tap("test".(arg) = > {
  console.log('call incoming:${arg}`);
  return "tecvan";
});

person.hooks.sleep.tap("test".(arg) = > {
  console.log('A callback returns:${arg}`);
  return "world";
});

console.log("Final result:" + person.sleep());
// Result:
// Call is passed in: hello
// A callback returns: tecvan
// Final result: world
Copy the code

In this example, the sleep hook is of type SyncWaterfallHook, and two callbacks are registered. You can see from the processing results that the first callback receives arg = hello, which is the argument passed in the call at line 10. The second callback receives the result of the first callback, tecvan; The call then returns the result of the second callback, world.

When using SyncWaterfallHook, there are a few considerations:

  • Parameters must be provided during initialization, as in the above examplenew SyncWaterfallHook(["msg"])Arguments must be passed in the constructor["msg"]For dynamic compilationcallParameter dependency, which will be covered laterDynamic compilationThe details.
  • Release callscallYou need to pass in the initial parameter

Webpack scenario parsing

SyncWaterfallHook in webpack appeared a total of 55 times, of which more representative example is NormalModuleFactory hooks. The factory, in webpack internal implementation, Resolve displays the corresponding Module object within the hook based on the resource type:

class NormalModuleFactory {
  constructor() {
    this.hooks = {
      factory: new SyncWaterfallHook(["filename"."data"]),};this.hooks.factory.tap("NormalModuleFactory".() = > (result, callback) = > {
      let resolver = this.hooks.resolver.call(null);

      if(! resolver)return callback();

      resolver(result, (err, data) = > {
        if (err) return callback(err);

        // direct module
        if (typeof data.source === "function") return callback(null, data);

        // ...
      });
    });
  }

  create(data, callback) {
    / /...
    const factory = this.hooks.factory.call(null);
    // ...}}Copy the code

Basically, it is building modules, outsourcing the module creation process through factory hooks, and deducing the final Module object step by step according to waterfall’s characteristics in the hook callback queue.

SyncLoopHookhook

Basic logic

Loop hooks loop until all callbacks return undefined, but the dimension of loop is a single callback function. For example, if there is a callback queue [fn1, fn2, fn3], loop hooks execute fn1 first. If fn1 returns a non-undefined value, fn1 continues to be executed until undefined is returned and then fn2 is advanced. Pseudo code:

function loopCall() {
  const callbacks = [fn1, fn2, fn3];
  for (let i in callbacks) {
    const cb = callbacks[i];
    // Repeat the execution
    while(cb() ! = =undefined) {}}}Copy the code

The sample

Due to the nature of loop loop execution, care must be taken when using the loop to avoid falling into an infinite loop. Example:

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

class Somebody {
  constructor() {
    this.hooks = {
      sleep: new SyncLoopHook(),
    };
  }
  sleep() {
    return this.hooks.sleep.call(); }}const person = new Somebody();
let times = 0;

// Register a callback
person.hooks.sleep.tap("test".(arg) = > {
  ++times;
  console.log(The first `${times}Callback A 'is executed this time);
  if (times < 4) {
    returntimes; }}); person.hooks.sleep.tap("test".(arg) = > {
  console.log('Perform callback B');
});

person.sleep();
// Run the result
// Execute callback A for the first time
// Execute callback A for the second time
// Perform callback A for the third time
// Execute callback A for the fourth time
// Execute callback B
Copy the code

As you can see in the example, callback A is executed until times >= 4 is met and A returns undefined before callback B is executed.

Although Tapable provides SyncLoopHook hooks, they are not used in the WebPack source code, so the reader should understand the usage without delving into the details.

Asynchronous hooks

Sync hooks have the advantage of being executed in a relatively simple order, one after the other. The disadvantage is that you cannot perform asynchronous operations in the callback. In addition to synchronous hooks, Tapable also provides a series of asynchronous hooks starting with Async, which can perform asynchronous operations in callback functions.

AsyncSeriesHookhook

Basic logic

AsyncSeriesHook features:

  • Supports asynchronous callbacks, which can be written in the callback functioncallbackpromiseStyle asynchronous operation
  • The callback queue is executed sequentially, and the next one is executed only after the previous one has finished
  • withSyncHookAgain, you don’t care about the execution result of the callback

This is represented by a piece of pseudocode:

function asyncSeriesCall(callback) {
  const callbacks = [fn1, fn2, fn3];
  // Perform callback 1
  fn1((err1) = > {
    if (err1) {
      callback(err1);
    } else {
      // Perform callback 2
      fn2((err2) = > {
        if (err2) {
          callback(err2);
        } else {
          // Perform callback 3
          fn3((err3) = > {
            if(err3) { callback(err2); }}); }}); }}); }Copy the code

The sample

Let’s start with an example of the callback style:

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

const hook = new AsyncSeriesHook();

// Register a callback
hook.tapAsync("test".(cb) = > {
  console.log("callback A");
  setTimeout(() = > {
    console.log("Callback A asynchronous operation completed");
    When the callback ends, cb is called to notify Tapable that the current callback has ended
    cb();
  }, 100);
});

hook.tapAsync("test".() = > {
  console.log("callback B");
});

hook.callAsync();
// Result:
// callback A
// Callback A The asynchronous operation is complete
// callback B
Copy the code

As can be seen from the output result of the code, tapable considers that the current callback is completed and starts to execute the B callback only after the setTimeout inside the A callback is completed and cb function is called.

In addition to the callback style, tap/call functions can also be called in the promise style.

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

const hook = new AsyncSeriesHook();

// Register a callback
hook.tapPromise("test".() = > {
  console.log("callback A");
  return new Promise((resolve) = > {
    setTimeout(() = > {
      console.log("Callback A asynchronous operation completed");
      resolve();
    }, 100);
  });
});

hook.tapPromise("test".() = > {
  console.log("callback B");
  return Promise.resolve();
});

hook.promise();
// Result:
// callback A
// Callback A The asynchronous operation is complete
// callback B
Copy the code

There are three changes:

  • willtapAsyncChange totapPromise
  • TapThe callback needs to returnpromiseObject, line 8 of the previous example
  • callAsyncCall changed topromise

Webpack scenario analysis

The AsyncSeriesHook hook appears in WebPack 34 times in total, which are relatively easy to understand times, such as when compiler.hooks. Done hooks are fired after a build has finished, to inform that a single build has ended:

class Compiler {
  run(callback) {
    if (err) return finalCallback(err);

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

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

          this.hooks.additionalPass.callAsync((err) = > {
            if (err) return finalCallback(err);
            this.compile(onCompiled);
          });
        });
        return;
      }

      this.emitRecords((err) = > {
        if (err) return finalCallback(err);

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

AsyncParallelHookhook

Similar to AsyncSeriesHook, AsyncParallelHook supports asynchronous style callbacks, but AsyncParallelHook executes all callbacks in the callback queue in parallel, logically similar to:

function asyncParallelCall(callback) {
  const callbacks = [fn1, fn2];
  // A counter is maintained internally
  var _counter = 2;

  var _done = function() {
    _callback();
  };
  if (_counter <= 0) return;
  // Execute the callback sequentially
  var _fn0 = callbacks[0];
  _fn0(function(_err0) {
    if (_err0) {
      if (_counter > 0) {
        // If an error occurs, ignore subsequent callbacks and exit directly
        _callback(_err0);
        _counter = 0; }}else {
      if (--_counter === 0) _done(); }});if (_counter <= 0) return;
  Start executing the next callback without waiting for the previous callback to finish
  var _fn1 = callbacks[1];
  _fn1(function(_err1) {
    if (_err1) {
      if (_counter > 0) {
        _callback(_err1);
        _counter = 0; }}else {
      if (--_counter === 0) _done(); }}); }Copy the code

AsyncParallelHook Hook features:

  • Support for asynchronous styles
  • Execute the callback queue in parallel without doing any waiting
  • As with SyncHook, you don’t care about the result of the callback execution

other

Some hook types are defined in Tapable, but are not exemplified in WebPack:

  • AsyncParallelBailHook: asynchronous + parallel + fusing: All callbacks are executed at the same time. If any callback returns a value, the result is directly returned, ignoring the remaining callbacks
  • AsyncSeriesBailHook: asynchronous + serial + fusing: Callbacks are executed one by one after startup. If any callbacks return a non-undefined value, subsequent calls are stopped and the result is directly returned
  • AsyncSeriesLoopHook: asynchronous + serial + cyclic: executes callbacks one by one after startup. If any callbacks return noundefinedValue, the callback is repeated until it returnsundefinedBefore proceeding to the next callback

Dynamic compilation

Basic logic

Tapable’s biggest secret is its internal implementation of a very bold design: dynamic compilation, the so-called synchronous, asynchronous, Bail, Waterfall, loop and other callback rules are based on dynamic compilation ability, so it is inevitable to learn Tapable in-depth around the dynamic compilation feature.

When the user executes the hook publishing function Call /callAsync/ PROMISE, Tapable dynamically generates the executing function based on the hook type, parameters, callback queue, etc., as in the following example:

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

const sleep = new SyncHook();

sleep.tap("test".() = > {
  console.log("callback A");
});
sleep.call();
Copy the code

When sleep.call is called, tapable’s internal processing flow is roughly as follows:

The compilation process mainly involves three entities:

  • tapable/lib/SyncHook.jsDefinition:SyncHookEntry file of
  • tapable/lib/Hook.jsSyncHookIt’s just a simple interface that’s actually called internallyHookClass,HookImplement hook logic – other hooks follow the same pattern
  • tapable/lib/HookCodeFactory.js: Dynamically compiledCall, callAsync, PromiseFunction content factory class, note that other hooks are also usedHookCodeFactoryFactory function.

After SyncHook (similar to other hooks) calls Call, the Hook base class collects context information and calls the Compiler function passed in by createCall and its subclasses. Compiler calls HookCodeFactory and then uses the new Function method to dynamically concatenate the callback execution Function. The corresponding generating function for the above example:

(function anonymous(
) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0();

})
Copy the code

A more complicated example

Dynamic compilation capability has performance and security issues in common scenarios, so the community rarely sees such designs. Going back to the example above, SyncHook’s callback logic is pretty simple. Is dynamic compilation really necessary? Let’s look at a more complicated example:

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

const sleep = new AsyncSeriesWaterfallHook(["name"]);

sleep.tapAsync("test1".(name, cb) = > {
  console.log('Perform A callback: parameter name=${name}`);
  setTimeout(() = > {
    cb(undefined."tecvan2");
  }, 100);
});

sleep.tapAsync("test".(name, cb) = > {
  console.log('Perform the B callback: parameter name=${name}`);
  setTimeout(() = > {
    cb(undefined."tecvan3");
  }, 100);
});

sleep.tapAsync("test".(name, cb) = > {
  console.log('Perform the C callback: parameter name=${name}`);
  setTimeout(() = > {
    cb(undefined."tecvan4");
  }, 100);
});

sleep.callAsync("tecvan".(err, name) = > {
  console.log('Callback ends, name=${name}`);
});

// Result:
// Execute the A callback: parameter name=tecvan
// Perform the B callback: parameter name=tecvan2
// Perform the C callback: parameter name=tecvan3
// End of callback, name=tecvan4
Copy the code

AsyncSeriesWaterfallHook: AsyncSeriesWaterfallHook: AsyncSeriesWaterfallHook: AsyncSeriesWaterfallHook: AsyncSeriesWaterfallHook

(function anonymous(name, _callback) {
  "use strict";
  var _context;
  var _x = this._x;
  function _next1() {
    var _fn2 = _x[2];
    _fn2(name, function(_err2, _result2) {
      if (_err2) {
        _callback(_err2);
      } else {
        if(_result2 ! = =undefined) {
          name = _result2;
        }
        _callback(null, name); }}); }function _next0() {
    var _fn1 = _x[1];
    _fn1(name, function(_err1, _result1) {
      if (_err1) {
        _callback(_err1);
      } else {
        if(_result1 ! = =undefined) { name = _result1; } _next1(); }}); }var _fn0 = _x[0];
  _fn0(name, function(_err0, _result0) {
    if (_err0) {
      _callback(_err0);
    } else {
      if(_result0 ! = =undefined) { name = _result0; } _next0(); }}); });Copy the code

This generating function has several characteristics:

  • The generator encapsulates the items in the callback queue as_next0/_next1Functions, thesenextThe intrinsic logic of functions is highly similar
  • Callbacks are executed in the order defined by callbacks, and the next callback is invoked after the last callback, for example, lines 39 and 27 in the generated code

Compared with the implementation of AsyncSeriesWaterfallHook by means of recursion and loop, this section of generating function logic is indeed clearer and easier to understand.

Most of the features offered by Tapable are implemented based on Hook + HookCodeFactory, if readers are interested, There are several functions in tapable/lib/ hook. js called \_DELEGATE/CALL\_ASYNC\_DELEGATE/PROMISE\_DELEGATE to break points:

After that, debug the NDB command breakpoint to see the dynamically compiled code from the publish action:

Advanced features: Intercept

In addition to the usual TAP /call, Tapable also provides a simple middleware mechanism — the Intercept interface, for example

const sleep = new SyncHook();

sleep.intercept({
  name: "test".context: true.call() {
    console.log("before call");
  },
  loop(){
    console.log("before loop");
  },
  tap() {
    console.log("before each callback");
  },
  register() {
    console.log("every time call tap"); }});Copy the code

Intercept supports the registration of the following types of middleware:

The signature
explain
call
(… args) => void
Triggered when call/callAsync/ PROMISE is called
tap
(tap: Tap) => void
Fired before each call callback after a call to the call class function
loop
(… args) => void
Only loop hooks are valid and fire before the loop starts
register
(tap: Tap) => Tap | undefined
Triggered when tap/tapAsync/tapPromise is called

Where register is called each time tap is called; The trigger timing of the other three middleware is roughly as follows:

  var _context;
  const callbacks = [fn1, fn2];
  var _interceptors = this.interceptors;
  // Call the call function immediately
  _interceptors.forEach((intercept) = > intercept.call(_context));
  var _loop;
  var cursor = 0;
  do {
    _loop = false;
    // Trigger 'loop' at the start of each loop
    _interceptors.forEach((intercept) = > intercept.loop(_context));
    / / trigger ` tap `
    var _fn0 = callbacks[0];
    _interceptors.forEach((intercept) = > intercept.tap(_context, _fn0));
    var _result0 = _fn0();
    if(_result0 ! = =undefined) {
      _loop = true;
    } else {
      var _fn1 = callbacks[1];
      // Tap again
      _interceptors.forEach((intercept) = > intercept.tap(_context, _fn1));
      var _result1 = _fn1();
      if(_result1 ! = =undefined) {
        _loop = true; }}}while (_loop);
Copy the code

Intercept feature in webpack mainly used as a progress indication, such as webpack/lib/ProgressPlugin plug-in, Middleware functions to record progress are applied to compiler.hooks. Emit and Compiler.hooks. AfterEmit hooks, respectively. Other types of plug-ins are used less often.

Advanced features: HookMap

Tapable also has a feature worth noting — HookMap. HookMap provides a collection manipulation capability that reduces the complexity of creating and using collections and is relatively simple to use:

const { SyncHook, HookMap } = require("tapable");

const sleep = new HookMap(() = > new SyncHook());

// Filter specific hooks in the collection through the for function
sleep.for("statement").tap("test".() = > {
  console.log("callback for statement");
});

// Triggers a statement hook
sleep.get("statement").call();
Copy the code

In Webpack, HookMap is concentrated in webpack/lib/parser.js file. The parser file mainly parses the resource content into AST collection. For example, the Parser.hooks. Expression hook is triggered when an expression is encountered. The problem is that the AST structure and content are very complex, and if all scenarios are implemented as separate hooks, the amount of code can balloon dramatically.

This scenario is suitable for HookMap, using expression as an example:

class Parser {
  constructor() {
    this.hooks = {
      // Define the hook
      // HookMap is used here, so there is no need to iterate through all expression scenarios in advance
      expression: new HookMap(() = > new SyncBailHook(["expression"]})), }// Trigger hooks in different scenarios
  walkMemberExpression(expression) {
    const exprName = this.getNameForExpression(expression);
    if (exprName && exprName.free) {
      // Triggers a specific type of hook
      const expressionHook = this.hooks.expression.get(exprName.name);
      if(expressionHook ! = =undefined) {
        const result = expressionHook.call(expression);
        if (result === true) return; }}// ...
  }

  walkThisExpression(expression) {
    const expressionHook = this.hooks.expression.get("this");
    if(expressionHook ! = =undefined) { expressionHook.call(expression); }}}// Hook consumption logic
// Select CommonJsStuffPlugin for example only
class CommonJsStuffPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap(
      "CommonJsStuffPlugin".(compilation, { normalModuleFactory }) = > {
        const handler = (parser, parserOptions) = > {
          // Consume hooks precisely with for
          parser.hooks.expression
            .for("require.main.require")
            .tap(
              "CommonJsStuffPlugin",
              ParserHelpers.expressionIsUnsupported(
                parser,
                "require.main.require is not supported by webpack.")); parser.hooks.expression .for("module.parent.require")
            .tap(
              "CommonJsStuffPlugin",
              ParserHelpers.expressionIsUnsupported(
                parser,
                "module.parent.require is not supported by webpack.")); parser.hooks.expression .for("require.main")
            .tap(
              "CommonJsStuffPlugin",
              ParserHelpers.toConstantDependencyWithWebpackRequire(
                parser,
                "__webpack_require__.c[__webpack_require__.s]"));// ...}; }); }}Copy the code

Webpack plug-in architecture

After understanding tapable, we can move on and talk about the core design of the WebPack plug-in architecture.

Many well-known frameworks in the front-end community have their own plug-in architecture, such as axios, quill, vscode, webpack, vue, rollup, and so on. Plug-in architecture is highly flexible and extensible, but it usually requires very strong architectural capability to solve at least three problems:

  • Interfaces: You need to provide a set of logic access methods that allow developers to insert logic into specific locations at specific times
  • Input: How can context information be efficiently transmitted to plug-ins
  • Output: how the plug-in internally affects the entire operating system

To address these issues, WebPack provides developers with a plug-in solution based on tapable hooks:

  1. Specific nodes of the compile process inform the plug-in, in the form of hooks, of what is happening at the moment;
  2. Context information is passed as a parameter through the callback mechanism provided by Tapable;
  3. There are a number of side Effect interaction interfaces that come with the context parameter object, through which plug-ins can be changed

All of this is possible without Tapable, such as:

class Compiler {
  // In the constructor, the hook object is initialized
  constructor() {
    this.hooks = {
      thisCompilation: new SyncHook(["compilation"."params"]),}; }compile() {
    // Trigger a specific hook at a specific time
    const compilation = new Compilation();
    this.hooks.thisCompilation.call(compilation); }}Copy the code

The Compiler type defines the thisCompilation hook internally and issues an event message after the compilation has been created so that the plug-in developer can retrieve the newly created Compilation object based on this hook:

class SomePlugin {
  apply(compiler) {
    compiler.hooks.thisCompilation.tap("SomePlugin".(compilation, params) = > {
        // Compilation, params}); }}Copy the code

The Compilation/Params parameters passed by the hook callback are the context information that WebPack wants to pass to the plug-in and the input the plug-in can take. Different hooks pass different context objects, which is determined when the hook is created, for example:

class Compiler {
    constructor() {
        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>} * /
            assetEmitted: new AsyncSeriesHook(["file"."content"]),
            / * *@type {AsyncSeriesHook<Compilation>} * /
            afterEmit: new AsyncSeriesHook(["compilation"]),}; }}Copy the code
  • shouldEmitWill be introduced tocompilationparameter
  • doneWill be introduced tostatsparameter
  • addtionalPassNo parameters
  • .

Common parameter objects are compilation/module/stats/compiler/file/chunks, etc., in the hook callback can change the state of these objects, affect webpack compilation logic. The meanings, functions and interfaces of these types are complicated. It is suggested that readers have a thorough understanding of the core principles of Webpack in [Ten thousand Words Summary].

conclusion

Tapable offers a total of 10 hooks, supporting synchronous, asynchronous, fusing, cycling, waterfall and other features to support the complex compilation function of Webpack. Familiarity with these 10 hooks is just a starting point for quickly identifying the basic pattern of callbacks when writing plug-ins.

In addition, you also need to learn more webpack built-in object functions, features, interface and other content to successfully write a plug-in that meets the needs, the author will focus on the field of Webpack recently, remember to click attention to this interested students.

Continue to output in-depth front-end technology articles, follow the wechat public account: