# 一.plugin的实现

当我们需要一个构建功能时 webpack 本身暂未支持的,我们便可以通过寻找合适的 webpack 插件来帮助实现需要的功能,或者开发一个 webpack 插件来满足项目的构建需求。

# 1.一个简单的 plugin

plugin 的实现可以是一个类,使用时传入相关配置来创建一个实例,然后放到配置的plugins字段中,而 plugin 实例中最重要的方法是apply,该方法在 webpack compiler 安装插件时会被调用一次,apply接受 webpack compiler 对象实例的引用,你可以在 compiler 对象实例上注册各种事件钩子函数,来影响 webpack 的所有构建流程,以便完成更多其他的构建任务。

下边的这个例子,是一个可以创建 webpack 构建温江列表 markdown 的 plugin,实现上相对简单,但呈现了一个 webpack plugin 的基本形态。

class FileIsPlugin {
  constructor(options) {}
  apply(compiler) {
    compiler.hooks.emit.tap("FileIsPlugin", (compilation) => {
      // 在compiler的emit hook中注册一个方法,当webpack执行到这个阶段时会调用这个方法
      var filelist = "In this build:\n\n" // 给生成的markdown文件创建一个简单标题
      for (var filename in compilation.assets) {
        filelist += "- " + filename + "\n" // 遍历所有编译后的资源,每一个文件添加一行说明
      }
      compilation.assets["filelist.md"] = {
        // 将列表作为一个新的文件资源插入到webpack构建结果中
        source: function() {
          return filelist
        },
        size: function() {
          return tilelist.length
        },
      }
    })
  }
}
module.exports = FileListPlugin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 2.开发和调试

创建一个 js 代码文件,在 webpack 配置文件中引用这个文件的代码,照样运行 webpack 构建查看结果即可。

const FileIsPlugin = requrie("./plugins/FileIsPlugin.js") // 假如我们上述那个例子的代码是 ./plugins/FileIsPlugin这个文件
module.exports = {
  plugins: [
    new FileIsPlugin(), // 实例化这个插件,有的时候需要传入对应的配置
  ],
}
1
2
3
4
5
6

# 3.事件钩子

webpack 中的事件钩子,基本覆盖了 webpack 构建流程中的每一个步骤,你可以在这些步骤中注册自己的处理函数,来添加额外的功能。

this.hooks = {
  shouldEmit: new SyncBaiHook(["compilation"]), //这里的声明的事件钩子函数接收的参数是
  compilation,
  done: new AsyncSeriesHook(["stats"]), //这里接受的参数是stats,以此类推
  additionalPass: new AsyncSeriesHOok([]),
  beforeRun: new AsyncSeriesHook(["compilation"]),
  run: new AsyncSeriesHook(["compilation"]),
  emit: new AsyncSeriesHook(["compilation"]),
  afterEmit: new AsyncSeriesHook(["compilation"]),
  thisCompilations: new SyncHook(["compilation", "params"]),
}
1
2
3
4
5
6
7
8
9
10
11

从这你可以看到各个事件钩子函数接收的参数是什么,你还会发现事件钩子会有不同的类型,例如SyncBaiHook,AsyncSeriesHook,SyncHook

# 4.事件钩子类型

上述提到的 webpack compiler 中使用了多种类型的事件钩子,根据其名称可以区分出是同步还是异步,对于通不过的事件钩子来说,注册事件的方法只有tap可用,例如上述shouldEmit应该这样注册事件函数的:

apply(compiler){
  compiler.hooks.shouldEmit.tap('PluginName',(compilation)=>{
    ...
  })
}
1
2
3
4
5

但如果是异步的事件钩子,那么可以使用tabPromise或者tabAsync来注册事件函数,tabPromise要求方法返回Promise以便处理异步,而tapAsync则是需要用callback来返回结构,例如:

compiler.hooks.done.tapPromise("PluginName", (state) => {
  return new Promise((resolve, reject) => {
    fs.writeFile("path/to/file", stats.toJson(), (err) =>
      err ? reject(err) : resolve()
    )
  })
})
// 或者
compiler.hooks.done.tapAsync("PluginName", (stats, callback) => {
  fs.writeFile("path/to/file", stats.toJson(), (err) => callback(err))
})
// 如果插件处理中没有异步操作要求的话,也可以用同步的方式
compiler.hooks.done.tap("PluginName", (stats, callback) => {
  callback(fa.writeFileSync("path/to/file", stats.toJson()))
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

然而 tabable 这个工具库提供的钩子类型远不止这几种,多样化的钩子类型,主要是为了能够覆盖多种使用场景:

  • 连续地执行注册的事件函数
  • 并行的执行注册的事件函数
  • 一个接一个地执行注册的事件函数,从前边的事件函数获取输入,即瀑布流的方式
  • 异步地执行注册的事件函数
  • 在允许时停止执行注册的事件函数,一旦一个方法返回了一个非undefined的值,就跳出执行流

除了同步和异步的区别,我们再参考上述这一些使用场景,以及官方文档的 Plugin API,进一步将事件钩子类型做一个区分。 名称带有parallel的,注册的事件函数会并行调用,如:

  • AsyncParallelHook
  • AsyncParallelBailHook 名称带有bail的,注册的事件函数会被顺序调用,直至一个处理方法有返回值(ParalleBail 的事件函数则会并行调用,第一个返回值会被使用)
  • SyncBailHook
  • AsyncParalleBailHook 通过上面的名称可以看出,有一些类型是可以结合到一起的,如AsyncParalleBailHook,这样它就具备了更加多样化的特性。