likes
comments
collection
share

webpack-Tapable事件流机制的使用和插件的模拟实现

作者站长头像
站长
· 阅读数 17

Webpack的本质

Webpack可以将其理解为是一种基于事件流的编程范例,一系列的插件运行。

它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,Webpack 中最核心的负责编译的 Compiler 和负责创建 bundles 的 Compilation 都是 Tapable 的子类,并且实例内部的生命周期也是通过 Tapable 库提供的钩子类实现的。

核心对象 Compiler 继承 Tapable

class Compiler extends Tapable {
  // ...
}

核心对象 Compilation 继承 Tapable

class Compilation extends Tapable {
  // ...
}

Tapable 是什么?

Tapable 是一个类似node EventEmitter 的发布订阅模式,但是更加强大,它包含了多种不同的监听和触发事件的方式。主要是控制钩子函数的发布 与订阅,控制着 webpack 的插件系统。

The tapable package expose many Hook classes,which can be used to create hooks for plugins.

tapable 提供了一些用于创建插件的钩子类。

Tapable 的钩子

const {
	SyncHook,                 //同步钩子
	SyncBailHook,             //同步熔断钩子
	SyncWaterfallHook,        //同步流水钩子
	SyncLoopHook,             //同步循环钩子
	AsyncParallelHook,        //异步并发钩子
	AsyncParallelBailHook,    //异步并发熔断钩子
	AsyncSeriesHook,          //异步串行钩子
	AsyncSeriesBailHook,      //异步串行熔断钩子
	AsyncSeriesWaterfallHook  //异步串行流水钩子
 } = require("tapable");

Tapable hooks 类型

webpack-Tapable事件流机制的使用和插件的模拟实现

Tapable 的使用

Tapable 暴露出来的都是类方法, new 一个类方法获得我们需要的钩子

举个最简单的例子🌰

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

const hook = new SyncHook(['arg1', 'arg2', 'arg3']);

hook.tap('hook1', (arg1, arg2, arg3) => {
    console.log(arg1, arg2, arg3);
});

hook.call(1, 2, 3);

上述代码定义了一个同步钩子,可以通过hook实例对象(SyncHook本身也是继承自Hook类的)的tap方法订阅事件,然后利用call函数触发订阅事件,执行 callback 函数。

值得注意的是 call 传入参数的数量需要与实例化时传递给钩子类构造函数的数组长度保持一致,否则,即使传入了多个,也只能接收到实例化时定义的参数个数。

webpack-Tapable事件流机制的使用和插件的模拟实现

模拟实现webpack和plugin

既然我们已经知道了webpack内部的事件流机制,那么他具体是怎么实现的呢?🤔

让我们通过一个插件例子来模拟实现下,首先来看官网给出的编写一个 plugin 的示例

Webpack.config.js

var HelloWorldPlugin = require('hello-world');

module.exports = {
  // ... configuration settings here ...
  plugins: [new HelloWorldPlugin({ options: true })]
};
class HelloWorldPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('Hello World Plugin', (
      compilation /* compilation is passed as an argument when done hook is tapped.  */
    ) => {
      console.log('Hello World!');
    });
  }
}

module.exports = HelloWorldPlugin;

上述编写了一个叫 HelloWorldPlugin 的类,里面存在 apply方法,和 compiler 参数,这些都是插件的固定写法,看到 compiler.hooks.done.tap 聪明的你一定想到了,这是使用了 Tapable 的订阅模式。

通过阅读源码,webpack 在循环 options.plugins 配置项的时候会调用插件里面的 apply 方法,在生命周期的 done 节点(也就是上面我们提到的hooktap 一个监听事件,当 Webpack 全部流程执行完毕时,监听事件将会被触发。

下面我们来模拟实现 webpackCompiler,通过新建一个插件 plugin,内部利用 Tapable 机制实现生命周期。

新建一个webpack-tapable文件夹

npm init -y
npm i tapable -D

最终的目录结构如下

webpack-tapable
  └─node_modules
  └─index.js
  └─myPlugin.js
  └─compiler.js
  └─package.json

compiler.js

const { SyncHook, AsyncSeriesHook } = require('tapable');

module.exports = class Compiler {
    constructor() {
        this.hooks = {
            accelerate: new SyncHook(['newspeed']),
            brake: new SyncHook(),
            calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"])
        }
    }
    // webpack生命周期,集合加速、刹车、计算路由
    // ❗❗注意这里都是call
    run(){
        this.accelerate(10)
        this.break()
        this.calculateRoutes('Async', 'hook', 'demo')
    }
    // 加速 
    accelerate(speed) {
        this.hooks.accelerate.call(speed);
    }
    // 刹车
    break() {
        this.hooks.brake.call();
    }
    // 计算路由
    calculateRoutes() {
        this.hooks.calculateRoutes.promise(...arguments).then(() => {
        }, err => {
            console.error(err);
        });
    }
}

上面的 Compiler 类定义了一些(伪)编译方法,方法里面调用 Tapablehook 钩子函数。

注意: run里的方法都是call,发布hook

MyPlugin.js

class MyPlugin {
  constructor() {}
  apply(compiler) {
    // ❗❗注意这里都是tap
    compiler.hooks.brake.tap("WarningLampPlugin", () =>
      console.log("WarningLampPlugin")
    );
    compiler.hooks.accelerate.tap("LoggerPlugin", (newSpeed) =>
      console.log(`Accelerating to ${newSpeed}`)
    );
    compiler.hooks.calculateRoutes.tapPromise(
      "calculateRoutes tapAsync",
      (source, target, routesList) => {
        // 返回promise
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            console.log(`tapPromise to ${source} ${target} ${routesList}`);
            resolve();
          }, 1000);
        });
      }
    );
  }
}

定义一个 MyPlugin 的插件,compiler 实例对象就是 webpack 里面的 new Compilerhooks.xxx 是上文 constructor 中定义的方法。

注意: 插件里都是tap,订阅hook

回到 webpack.config.js 中配置

// index.js
const Compiler = require('./Compiler');
const MyPlugin = require('./myPlugin');

// 这里类比webpack.config.js里面的插件配置
const options = {
  // 其他配置,比如entry,devServer....
  plugins: [new MyPlugin()],
};

webpack源码中,循环 options 配置,并进行相应处理

// index.js
// ...
const compiler = new Compiler();

for (const plugin of options.plugins) {
  if (typeof plugin === "function") {
    plugin.call(compiler, compiler);
  } else {
    plugin.apply(compiler);
  }
}

pluginfunction类型时候,调用函数本身的 call 方法,否则调用 MyPlugin 中的 apply方法。

也就是说,如果你的插件逻辑很简单,你可以直接在插件的配置文件里写一个function,去执行你的逻辑,也是可以通过的,不用非得按照 class 类的写法来实现插件😲。

还记得我们的两个注意嘛

  1. run里的方法都是call,发布hook
  2. plugin插件里都是tap,订阅hook

怎么样触发呢? 对,就是最后的 run方法。

// index.js
// ...

compiler.run();

插件中的 tap 监听事件注册到 Webpackcompiler、compilation(tapable类)上,在最上层(源码中的 lib/Webpack.js),调用 Compiler 类的 run 函数,完成触发,这也符合发布订阅的先监听后触发的逻辑顺序。

最后完整的 index.js 代码如下

// index.js
const Compiler = require('./Compiler');
const MyPlugin = require('./myPlugin');

// 这里类比webpack.config.js里面的插件配置
const options = {
  // 其他配置,比如entry,devServer....
  plugins: [new MyPlugin()],
};

const compiler = new Compiler();

for (const plugin of options.plugins) {
  if (typeof plugin === "function") {
    plugin.call(compiler, compiler);
  } else {
    plugin.apply(compiler);
  }
}

compiler.run();

总结

tapable 作为 Webpack 的核心库,承接了 Webpack 最重要的事件流的运转,它巧妙的钩子设计很好的将实现与流程解耦开来,真正实现了插拔式的功能模块。

以上代码属于源码的模拟实现,既然我们已经知道了原理,就可以动手实现一个简易版的 mini-webpack,还等什么,赶快敲起来~