[webpack] 核心库 - tapable 的设计思路与实现原理是什么?

Webpack Tapable 的设计思路

Webpack Tapable 的设计思路主要基于观察者模式(Observer Pattern)和发布-订阅模式(Publish-Subscribe Pattern),用于解耦各个插件之间的依赖关系,让插件能够独立作用于特定的钩子(Hook),从而实现可扩展性和灵活性。

具体来说,Tapable 采用了钩子(Hook)的概念,每个钩子对应一组事件,Webpack 在不同的时刻触发这些钩子,插件可以注册自己的事件处理函数到对应的钩子上,以实现各种功能。

为了避免插件之间的耦合,Tapable 将事件处理函数按照钩子类型分为同步钩子(Sync Hook)、异步钩子(Async Hook)、单向异步钩子(Async Parallel Hook)和多向异步钩子(Async Series Hook)四种类型。这样,不同类型的钩子对应着不同的事件处理顺序和调用方式,插件在注册自己的事件处理函数时,可以选择不同的钩子类型来适应不同的应用场景。

除此之外,Tapable 还提供了一些辅助方法和工具函数,用于方便地创建和管理钩子、向钩子注册事件处理函数、调用钩子的事件处理函数等。这些工具函数的设计思路也遵循了解耦、简单易用的原则,为插件开发提供了很大的便利性。

Tapable 的使用

Webpack Tapable 的使用分为三个步骤:

  1. 定义一个新的 Tapable 实例:在 Webpack 插件中定义一个新的 Tapable 实例,并定义需要监听的事件。
const { SyncHook } = require('tapable');

class MyPlugin {
  constructor() {
    this.hooks = {
      beforeRun: new SyncHook(['compiler']),
      done: new SyncHook(['stats'])
    };
  }

  apply(compiler) {
    this.hooks.beforeRun.tap('MyPlugin', compiler => {
      console.log('Webpack is starting to run...');
    });

    this.hooks.done.tap('MyPlugin', stats => {
      console.log('Webpack has finished running.');
    });
  }
}
  1. 触发事件:在 Webpack 的编译过程中,调用 Tapable 实例的触发方法,触发事件。
compiler.hooks.beforeRun.call(compiler);
// Webpack is starting to run...

compiler.run((err, stats) => {
  if (err) {
    console.error(err);
    return;
  }

  console.log(stats);
  compiler.hooks.done.call(stats);
  // Webpack has finished running.
});
  1. 注册插件:在 Webpack 的配置文件中,将插件实例注册到 Webpack 中。
const MyPlugin = require('./my-plugin');

module.exports = {
  plugins: [new MyPlugin()]
};

以上是使用 Tapable 的基本流程,通过 Tapable 可以监听到编译过程中的各个事件,并对编译过程进行修改,从而实现各种插件。以下是一些常见的 Tapable 类型和用法:

  • SyncHook:同步 Hook,按照注册的顺序同步执行所有回调函数。
const { SyncHook } = require('tapable');

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

hook.tap('MyPlugin', (arg1, arg2) => {
  console.log(`Hook is triggered with arguments: ${arg1}, ${arg2}`);
});

hook.tap('MyPlugin', (arg1, arg2) => {
  console.log('Second callback is called');
});

hook.call('Hello', 'world');
// Hook is triggered with arguments: Hello, world
// Second callback is called
  • AsyncParallelHook:异步 Hook,按照注册的顺序异步执行所有回调函数,不关心回调函数的返回值。
const { AsyncParallelHook } = require('tapable');

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

hook.tap('MyPlugin', (arg1, arg2, callback) => {
  setTimeout(() => {
    console.log(`Hook is triggered with arguments: ${arg1}, ${arg2}`);
    callback();
  }, 1000);
});

hook.tap('MyPlugin', (arg1, arg2, callback) => {
  setTimeout(() => {
    console.log('Second callback is called');
    callback();
  }, 500)
})

Tapable 是如何实现的?代码简单实现一下?

Webpack Tapable 是基于发布-订阅模式的一个插件系统,它提供了一组钩子函数,让插件可以在相应的时机执行自己的逻辑。

下面是一个简单的自定义 Tapable 的实现:

class Tapable {
  constructor() {
    this.hooks = {};
  }

  // 注册事件监听函数
  tap(name, callback) {
    if (!this.hooks[name]) {
      this.hooks[name] = [];
    }
    this.hooks[name].push(callback);
  }

  // 触发事件
  call(name, ...args) {
    const callbacks = this.hooks[name];
    if (callbacks && callbacks.length) {
      callbacks.forEach((callback) => callback(...args));
    }
  }
}

在这个例子中,我们定义了一个 Tapable 类,它有一个 hooks 对象属性,用于存储各个事件对应的监听函数。然后我们定义了 tap 方法,用于注册事件监听函数,以及 call 方法,用于触发事件。

下面是一个使用自定义 Tapable 的例子:

const tapable = new Tapable();

tapable.tap('event1', (arg1, arg2) => {
  console.log('event1 is triggered with arguments:', arg1, arg2);
});

tapable.tap('event2', (arg1, arg2) => {
  console.log('event2 is triggered with arguments:', arg1, arg2);
});

tapable.call('event1', 'hello', 'world');
tapable.call('event2', 'foo', 'bar');

在这个例子中,我们定义了两个事件 event1event2,并为它们注册了监听函数。当我们调用 call 方法触发事件时,注册的监听函数就会依次执行。

这个自定义 Tapable 的实现虽然简单,但它体现了 Tapable 的设计思路和核心功能。在实际使用中,Webpack 的 Tapable 提供了更多的功能和钩子,可以满足不同场景的需求。