introduce

How does Webpack work with various plug-ins to achieve packaging?

The main thing is the Tapable library. Below I refer to tapable source code, simple implementation of part of the hook.

SyncHook synchronization hook

Use the sample

const SyncHook = require("./lib/syncHook").default;

const hook = new SyncHook(["name"."age"]);

hook.tap("fn1".(name, age) = > {
  console.log("fn1->>>", name, age);
});

hook.tap("fn2".(name, age) = > {
  console.log("fn2->>>", name, age);
});

hook.call("kww".16);

// Console prints
fn1->>> kww 16
fn2->>> kww 16
Copy the code

The source code to achieve

Abstract base class Hook

I will simply implement an abstract class Hook. SyncHook, asyncParallelHook and other hooks in Tapable inherit from this Hook.

/** tap structure {name: string; fn: Function; type: 'sync' | 'async; } * /

abstract class Hook {
  // List of formal parameters, such as ["name", "age"] in new SyncHook(["name", "age"])
  public args: string[];
  // List of registered subscription objects
  public taps: object[];
  // A list of callback functions for the subscription object
  protected _x: Function[];

  constructor(args = []) {
    this.args = args;
    this.taps = [];
    this._x = undefined;
  }
    
  // hook.tap('fn1', () => {})
  tap(options, fn) {
    if (typeof options === "string") {
      options = {
        name: options,
      };
    }
    // tap = { fn: f(){}, name: "fn1" }
    const tap = Object.assign(
      {
      // The tap object in the source code also has type: "sync", and interceptors, WHICH I omit here
        fn,
      },
      options
    );
    this._insert(tap);
  }

  / / register the tap
  private _insert(tap) {
    this.taps[this.taps.length] = tap; // this.taps.push(tap);
  }

  call(. args) {
    // Create the structure of the function code to be executed in the future
    let callFn = this._createCall();
    // Call the above function
    return callFn.apply(this, args);
  }

  private _createCall(type) {
    return this.compile({
      taps: this.taps,
      args: this.args,
      type}); }abstract compile(data: { taps: any[]; args: string[]; type: "sync" | "async"|"promise"}) :Function;
}

export default Hook;
Copy the code

Implement the subclass SyncHook

The final execution function is dynamically generated by different CodeFactories. Implement SyncHookCodeFactory first, then SyncHook.

import Hook from "./hook";
SyncHookCodeFactory also inherits from a base class factory named HookCodeFactory
// Let's simplify this a little bit and write it right here
class SyncHookCodeFactory {
  public options: {
    taps: any[]; // [{ name:'fn1', fn: f(){} }, { name:'fn2', fn: f(){} },]
    args: string[]; // ["name", "age"]
  };

  setup(instance, options) {
    // This is done in the source code via init
    this.options = options;
    // Get all the registered tap callback functions
    instance._x = options.taps.map((o) = > o.fn);
  }

  /** * Create a code problem that can be executed, then return ** the source code is written in the base class factory, through switch(this.options.type) to execute the different logic, * this.options. Type is sync, async, promise *@param options* /
  create(options) {
    let fn;
    // Use new Function to convert strings into functions
    fn = new Function(
      / / / "name", "age" into "the name, age," as a function of the generated form parameters, such as f (name, age) {... }
      this.args(),
      `
        The ${this.head()}
        The ${this.content()}
    `
    );
    return fn;
  }

  // Fixed method in base class
  head() {
    return "var _x = this._x;";
  }

  // Subclass the method to generate a string similar to the following
  // 
  // var _fn0 = _x[0]; _x is the callback function for all registered taps
  // _fn0(name, age);
  //
  content() {
    let code = "";
    for (var i = 0; i < this.options.taps.length; i++) {
      code += `
          var _fn${i} = _x[${i}]; 
          _fn${i}(The ${this.args()});
          `;
    }
    return code;
  }

  args() {
    return this.options.args.join(","); }}let factory = new SyncHookCodeFactory();

class SyncHook extends Hook {
  constructor(args) {
    super(args);
  }

  compile(options) {
    factory.setup(this, options);
    returnfactory.create(options); }}export default SyncHook;
Copy the code

AsyncParallelHook Asynchronous parallel hook

Use the sample

const AsyncParallelHook = require("./src/asyncParallelHook").default;

const hook = new AsyncParallelHook(["name"."age"]);

console.time('time')
hook.tapAsync("fn1".(name, age, callback) = > {
  console.log("fn1->>>", name, age);
  setTimeout(() = >{
    callback();
  },1000)}); hook.tapAsync("fn2".(name, age, callback) = > {
  console.log("fn2->>>", name, age);
  setTimeout(() = >{
    callback();
  },2000)}); hook.callAsync("kww".16.function () {
  console.log("end");
  console.timeEnd('time')});// Terminal print
fn1->>> kww 16
fn2->>> kww 16
end
time: 2009.890ms
Copy the code

The source code to achieve

Implement subclass AsyncParallelHook

AsyncParallelHook and SyncHook are not very different, mainly due to their codeFactory differences.

class AsyncParallelHookCodeFactory {...create(options) {
    let fn;
    // f(name,age, _callback){... }
    fn = new Function(
      this.args({ after: "_callback" }), // "name,age,_callback"
      `
        The ${this.head()}
        The ${this.content()}
    `
    );
    return fn;
  }
  
  // Generate code similar to the following
  // var _counter = 2;
  // var _done = function(){_callback(); };
  //
  // var _fn0 = _x[0];
  // _fn0(name, age, function() {
  // if(--_counter === 0) _done();
  // });
  //
  // var _fn1 = _x[1];
  // _fn1(name, age, function() {
  // if(--_counter === 0) _done();
  // });
  //
  content() {
    let code = `var _counter = The ${this.options.taps.length}; var _done = function(){_callback(); }; `;
    
    for (var i = 0; i < this.options.taps.length; i++) {
      code += `
          var _fn${i} = _x[${i}]; 
          _fn${i}(The ${this.args()}, function(){
              if(--_counter === 0) _done();
          });
          `;
    }
    return code;
  }
  
  args(data? : { before? :string; after? :string }) {
    let args = this.options.args.slice();
    //if (data? .before) {
    // args = [data.before].concat(args);
    / /}
    if(data? .after) { args = args.concat([data.after]);// ["name", "age", "_callback"]
    }
    return args.join(","); }... }var factory = new AsyncParallelHookCodeFactory();

class AsyncParallelHook extends Hook {
  compile(options) {
    factory.setup(this, options);
    returnfactory.create(options); }}Copy the code