logo陈三

webpack 插件原理

by 陈三 on 
主题:  webpack

大部分时候,webpack 对我来说就是个黑盒。

且让我们抛开所有让人畏惧的细节,设想一下,webpack 在构建时会发生什么:

  1. 读取入口文件
  2. 一步一步处理
  3. 输出各种 assets 文件

显然,期间的每一步,应该都是设定好的,而非天马行空。

那么,我们用 webpack 插件扩展的,究竟是什么?

从勾子说起。

勾子

你可能已经见过勾子(hook),比如 React hooks ,又比如 Wordpress 的 hooks

维基上这样定义:

In computer programming, the term hooking covers a range of techniques used to alter or augment the behaviour of an operating system, of applications, or of other software components by intercepting function calls or messages or events passed between software components. Code that handles such intercepted function calls, events or messages is called a hook.

勾子用于拦截组件间传递的函数、事件、或消息。

在 webpack 构建过程中,文件从上一环节传递到下一环节,这期间,就有拦截的机会。

Tapable

Tapable 是 webpack 内置各种勾子的实际提供者。

比如,我们调用 SyncHook 来创建一个同步的 hook:

import { SyncHook } from 'tapable';
const hook = new SyncHook();

创建完 hook 后,我们可以 tap(窃听)该 hook:

hook.tap('MyPlugin', () => {
  // 窃听成功
});

这样在该 hook 执行时,我们的窃听函数就会执行 - 这正是 webpack 插件的原理,通过 tapable,webpack 主动开放各种 hook。我们只需要 tap 回调到 hook 上,就能保证它们在某个时间点被执行:

hook.call(); // 某个时间点执行该 hook,所有窃听的回调都会被执行

除了 SyncHook 外,tapable 还提供了其它类型的勾子,具体可查询文档

当然,在 webpack 下我们并不直接调用 tapable,因此这里仅就它的原理稍作了解。

webpack 插件写法

在 webpack 下,它推荐的插件写法是这样:

class MyWebpackPlugin {
  apply(compiler) {
    // 定义一个 class,其中有一个 apply 方法,
    // apply 方法接收 webpack 的 compiler 对象
  }
}

在插件完成后,我们就可以在配置文件中调用插件:

{
  plugins: [
    new MyWebpackPlugin()
  ]
}

webpack 在运行时,会调用我们的插件定义的 apply 方法:

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

于是,我们的勾子们就顺利插入到 webpack 生命周期中。

webpack 勾子

在知道 webpack 插件的运转原理及 webpack 插件写法之后,我们只要梳理清楚 webpack 所有勾子及勾子触发的时序即可对症下药,在需要的勾子上挂上“窃听器”。

webpack 的勾子分三类:

  1. Compiler hooks
  2. Compilation hooks
  3. JavascriptParser hooks

总共加起来,大约有百来个。一一掌握这百来个勾子并不现实;针对需求,然后查找文档、定位可用的勾子可能更切实际。