Why doesn’t CleanWebpackPlugin clean out the HTML generated by HtmlWebpackPlugin

What is Tapable

Tapable’s official description is:

Just a little module for plugins.

Tapable is a publish-subscribe event system. Compared with Node native Events, Tapable focuses more on the process processing of subscribers in publish-subscribe.

Tapable is the foundation on which webpack plug-ins can run. It is the microphone through which WebPack communicates with developers and enhances the basic functionality of WebPack.

Take a look at the official WebPack example

const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

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

In the example above, we subscribed to the Compiler object’s Run node, and console.log is printed when the Webpack process runs there.

Tapable is subscribed by tap, published by call, corresponding to on and EMIT in native Events.

The types of Tapable

Tapable hooks can be classified in different ways, which is what sets Tapable apart.

const {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,

  AsyncParallelHook,
  AsyncParallelBailHook,

  AsyncSeriesHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook,
  AsyncSeriesLoopHook,
} = require("tapable");
Copy the code

The timing of Tapable hooks can be divided into two categories, synchronous and asynchronous hooks. In asynchronous hooks, there are serial and parallel hooks.

From the function of Tapable hook, it can be divided into four hook types: normal type, interrupt type, flow type and circulation type.

Depending on these classifications, there are also differences in hook registration and invocation.

Hook subscriptions are divided into TAP, tapAsync, and tapPromise.

Hooks are published as Call, callAsync, and Promise.

Synchronous and asynchronous hooks

Synchronous hooks

SyncHook is a generic SyncHook that is subscribed and executed sequentially after publication.

Example a

const sh = new SyncHook(["name"]);

sh.tap("one".name= > {
  console.log(name);
});
sh.tap("second".name= > {
  console.log(name);
});
sh.call("tapable");
// tapable
// tapable
Copy the code

A synchronized hook can only subscribe using TAP. If you subscribe using tapAsync or tapPromise, an error will be reported and the hook will be overwritten when inherited

const TAP_ASYNC = () = > {
  throw new Error("tapAsync is not supported on a SyncHook");
};

const TAP_PROMISE = () = > {
  throw new Error("tapPromise is not supported on a SyncHook");
};


function SyncHook(args = [], name = undefined) {
  const hook = new Hook(args, name);
  hook.constructor = SyncHook;
  hook.tapAsync = TAP_ASYNC;
  hook.tapPromise = TAP_PROMISE;
  return hook;
}
Copy the code

Synchronous subscriptions can use asynchronous publishing, such as callAsync and Promise, as opposed to synchronous publishing, where the publisher is notified when all the subscribers have executed.

sh.callAsync('tapable'.() = > {
    console.log('all done')})// or

sh.promise().then(() = > {
    console.log('all done')})Copy the code

Node’s native event system requires some code extensions of its own.

Asynchronous hooks

There are two types of asynchronous hooks, serial and parallel.

Example two, asynchronous parallelism
const sh = new AsyncParallelHook(["name"]);

console.time("AsyncParallelHook");

sh.tapAsync("one".(name, cb) = > {
  console.log("one start");
  setTimeout(() = > {
    console.log("one done ", name);
    cb(null);
  }, 4000);
});
sh.tapPromise("two".name= > {
  return new Promise((resolve, reject) = > {
    console.log("two start");
    setTimeout(() = > {
      console.log("two done ", name);
      resolve();
    }, 1000);
  });
});
sh.promise("tapable").then(() = > {
  console.log("all done");
  console.timeEnd("AsyncParallelHook");
});

//one start
//two start
//two done tapable
//one done tapable
//all done
/ / AsyncParallelHook: 4.015 s
Copy the code

Parallel hooks subscribe in sequence, publish at the same time, and execute a unified callback when an error is reported or all are executed. Note that the cb function takes an error as its first argument. Passing a non-true value will terminate the cb function, performing a uniform callback but not affecting the rest of the hook execution because the asynchronous hook cannot be terminated once executed.

Example 3: Asynchronous serial
const sh = new AsyncSeriesHook(["name"]);

console.time("AsyncSeriesHook");

sh.tapAsync("one".(name, cb) = > {
  console.log("one start");
  setTimeout(() = > {
    console.log("one done ", name);
    cb(null);
  }, 4000);
});
sh.tapPromise("two".name= > {
  return new Promise((resolve, reject) = > {
    console.log("two start");
    setTimeout(() = > {
      console.log("two done ", name);
      resolve();
    }, 1000);
  });
});
sh.callAsync("tapable".error= > {
  if (error) console.log(error);
  console.log("all done");
  console.timeEnd("AsyncSeriesHook");
});

//one start
//one done tapable
//two start
//two done tapable
//all done
/ / AsyncParallelHook: 5.013 s
Copy the code

A serial hook is similar to a parallel hook, except that the serial hook executes in subscription order (if no order is specified) and the next hook waits for the previous hook to finish executing.

Normal, interrupted, flowing and circulating

Compared with other event systems, Tapable’s most powerful feature is its various functional hooks. Functional hooks can be divided into synchronous and asynchronous modes respectively. The above hooks are common ones, and the remaining three hooks will be introduced next.

Break hook

const sh = new SyncBailHook(["name"]);

sh.tap("one".(name) = > {
  console.log("one");
});
sh.tap("two".name= > {
  console.log("two");
  return null;
});
sh.tap("three".name= > {
  console.log("three");
});
sh.callAsync("tapable".error= > {
  if (error) console.log(error);
  console.log("all done");
});

//one
//two
//all done
Copy the code

If one of the sequentially executed subscriber functions returns a non-undefined value, the execution of subsequent subscriber functions is interrupted and goes directly to the call callback function.

The above is a synchronous example. For an asynchronous interrupt hook, the call callback function is reached directly, but the execution of subsequent subscribers is not interrupted.

Flow type hook

const sh = new SyncWaterfallHook(["type"]);

sh.tap("createBody".type= > {
  return type= = ="add" ? `a + b` : "a - b";
});
sh.tap("createReturn".str= > {
  return `return ${str}`;
});
sh.tap("exFn".str= > {
  return new Function("a"."b", str);
});

sh.callAsync("add".(error, fn) = > {
  if (error) console.log(error);
  const res = fn(3.6);
  console.log(res);
});
/ / 9
Copy the code

The subscriber of a stream hook executes depending on the value returned by the previous subscriber. If the previous subscriber did not return a value, it will look up until it finds a value that does not return undefined. This code takes arguments to verify that a summation or differential function is constructed.

Circular hook

const sh = new SyncLoopHook(["type"]);

let count = 0;
function random() {
  return Math.floor(Math.random() * 10) + "";
}
sh.tap("one".res= > {
  count++;
  console.log("I am trying one");
  if (res[0] !== random()) {
    return true; }}); sh.tap("two".res= > {
  count++;
  console.log("I am trying two");
  if (res[1] !== random()) {
    return true; }}); sh.tap("three".res= > {
  count++;
  console.log("I am trying three");
  if (res[2] !== random()) {
    return true; }}); sh.callAsync("777".error= > {
  if (error) console.log(error);
  console.log(count);
});
Copy the code

A circular hook means that any subscription event that does not return undefined will always be reexecuted from the subscription order until all subscription events return undefined. The above function computes the number of times a random 777 needs to be executed.

Thinking and Realizing

Basic type

Implementing a simple publish subscription is simple. Here is a basic publish subscription:

class Hook {
  constructor() {
    this.taps = [];
  }
  tap(fn) {
    this.taps.push(fn);
  }
  call(done) {
    for (let i = 0; i < this.taps.length; i++) {
      const fn = this.taps[i]; fn(); } done(); }}const hook = new Hook();
hook.tap(() = > {
  console.log("one");
});
hook.tap(() = > {
  console.log("two");
});
hook.call(() = > {
  console.log("done");
});
// one 
// two
// done

Copy the code

Think about it, how do you implement other types

Interrupt type

Implement an interrupted publish subscription:

.callBail(done) {
    for (let i = 0; i < this.taps.length; i++) {
      const fn = this.taps[i];
      const res = fn();
      if(res ! = =undefined) {
        done();
        return; } } done(); }...Copy the code

The water type

Implement a pipelining publish subscription

  callWaterfall(done) {
    let res = "init value";
      for (let i = 0; i < this.taps.length; i++) {
        const fn = this.taps[i];
        const r = fn(res);
        if(r ! = =undefined) {
          res = r;
        }
     }
     done(res);
}
Copy the code

circular

Implement a circular publish subscription:

  callLoop(done) {
    for (let i = 0; i < this.taps.length; ) {
      const fn = this.taps[i];
      constres = fn(); i = res ! = =undefined ? 0 : i + 1;
    }
    done();
  }
Copy the code

Source code analysis

As you can see, the above synchronous functional hooks look easy to implement, but the main difference between hooks is the call function. So implementing the Call function is the first problem to solve.

In addition, the code for Hooks is the same except for the call function, which would be redundant if Hooks implemented by different Hooks, so Tapable declares a base class Hook to handle some general logic.

The realization of the Hook

Hooks define several properties and methods that are common to all Hooks:

Properties:

  • _args: represents the subscriber’s input parameter.

  • Taps / _X: List of subscribers.

  • .

Methods:

  • _TAP (TAP, tapAsync, tapPromise): Subscriber registration method that taps the subscribers one by one by calling _INSERT

  • _INSERT: Adds subscribers. The order of calls is determined by the parameters of the subscribers.

  • Createcall: Creates different call functions. Different types of hooks are mainly call functions. The createcall function generates call functions through complie functions.

  • Compile: is a method, but the base class does not implement, will be given to the subclass implementation. If the subclass has no implementation, an error is reported.

The base Hook class defines common methods and attributes that are inherited by Hooks to override.

function SyncHook(args = [], name = undefined) {
  const hook = new Hook(args, name);
  hook.constructor = SyncHook;
  hook.tapAsync = TAP_ASYNC;
  hook.tapPromise = TAP_PROMISE;
  hook.compile = COMPILE;
  return hook;
}

SyncHook.prototype = null;

module.exports = SyncHook;
Copy the code

The realization of the HookCodeFactory

Different Hooks are implemented primarily by subclassing the compile method. HookCodeFactory constructs different call methods.

Even though we find that the call function differs from hook to hook, and we delegate its implementation to hook, we find that even the different call function implementations have most of the same code. For example, for the for loop, each synchronized call function has its own. In addition, The call function’s handling of errors, callbacks, and callback results is pretty much the same, and still redundant.

On the other hand, there are so many hooks to consider, such as when we are thinking and implementing hooks without considering asynchrony, that it is very difficult to implement in regular programming.

So Tapable uses string concatenation to implement various call functions.