webpack_plugins

webpack插件

一个简单的 plugin

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class FileListPlugin {
constructor(options) {}

apply(compiler) {
// 在 compiler 的 emit hook 中注册一个方法,当 webpack 执行到该阶段时会调用这个方法
compiler.hooks.emit.tap('FileListPlugin', (compilation) => {
// 给生成的 markdown 文件创建一个简单标题
var filelist = 'In this build:\n\n'

// 遍历所有编译后的资源,每一个文件添加一行说明
for (var filename in compilation.assets) {
filelist += ('- '+ filename +'\n')
}

// 将列表作为一个新的文件资源插入到 webpack 构建结果中
compilation.assets['filelist.md'] = {
source: function() {
return filelist
},
size: function() {
return filelist.length
},
}
})
}
}

module.exports = FileListPlugin

webpack 4.0 版本之前使用的是旧版本的 tapable,和新版本插件API有不少区别,基本注册事件一致,只是注册改变了。

开发和调试 plugin

你要在本地开发和调试 webpack plugin 是很容易的一件事情,你只需要创建一个 js 代码文件,如同上述的例子一样,该文件对外暴露一个类,然后在 webpack 配置文件中引用这个文件的代码,照样运行 webpack 构建查看结果即可。大概的配置方式如下:

1
2
3
4
5
6
7
8
9
// 假设我们上述那个例子的代码是 ./plugins/FileListPlugin 这个文件
const FileListPlugin = require('./plugins/FileListPlugin.js')

module.exports = {
// ... 其他配置
plugins: [
new FileListPlugin(), // 实例化这个插件,有的时候需要传入对应的配置
],
}

webpack 中的事件钩子

重点关注的是webpack插件钩子,下面重点说下

先来一波文档充充饥 compiler事件钩子compilation事件钩子

我们可以看到在事件钩子列表中看到,webpack 中会有相当多的事件钩子,基本覆盖了 webpack 构建流程中的每一个步骤,你可以在这些步骤都注册自己的处理函数,来添加额外的功能,这就是 webpack 提供的 plugin 扩展

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

上面是webpack事件钩子申明的方式,可以看到有很多类型的事件钩子,从这里你可以看到各个事件钩子函数接收的参数是什么,你还会发现事件钩子会有不同的类型,例如 SyncBailHook,AsyncSeriesHook,SyncHook,接下来我们再介绍一下事件钩子的类型以及我们可以如何更好地利用各种事件钩子的类型来开发我们需要的 plugin。

了解事件钩子类型

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
compiler.hooks.done.tapPromise('PluginName', (stats) => {
// 返回 promise
return new Promise((resolve, reject) => {
// 这个例子是写一个记录 stats 的文件
fs.writeFile('path/to/file', stats.toJson(), (err) => err ? reject(err) : resolve())
})
})

// 或者
compiler.hooks.done.tapAsync('PluginName', (stats, callback) => {
// 使用 callback 来返回结果
fs.writeFile('path/to/file', stats.toJson(), (err) => callback(err))
})

// 如果插件处理中没有异步操作要求的话,也可以用同步的方式
compiler.hooks.done.tap('PluginName', (stats, callback) => {
callback(fs.writeFileSync('path/to/file', stats.toJson())
})

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

  1. 连续地执行注册的事件函数

  2. 并行地执行注册的事件函数

  3. 一个接一个地执行注册的事件函数,从前边的事件函数获取输入,即瀑布流的方式
    异步地执行注册的事件函数

  4. 在允许时停止执行注册的事件函数,一旦一个方法返回了一个非 undefined 的值,
    就跳出执行流

除了同步和异步的区别,我们再参考上述这一些使用场景,以及官方文档的 Plugin API,进一步将事件钩子类型做一个区分。

名称带有 parallel 的,注册的事件函数会并行调用,如:

AsyncParallelHookAsyncParallelBailHook

名称带有 bail 的,注册的事件函数会被顺序调用,直至一个处理方法有返回值(ParallelBail 的事件函数则会并行调用,第一个返回值会被使用):

SyncBailHookAsyncParallelBailHookAsyncSeriesBailHook

名称带有 waterfall 的,每个注册的事件函数,会将上一个方法的返回结果作为输入参数,如:

SyncWaterfallHookAsyncSeriesWaterfallHook

通过上面的名称可以看出,有一些类型是可以结合到一起的,如 AsyncParallelBailHook,这样它就具备了更加多样化的特性。

了解了 webpack 中使用的各个事件钩子的类型,才能在开发 plugin 更好地去把握注册事件的输入和输出,同步和异步,来更好地完成我们想要的构建需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// plugin 的实现可以是一个类, 使用时传入相关配置来创建一个实例, 然后放到配置的 plugins 字段中, 而 plugin 实例中最重要的方法是 apply, 
// 该方法在 webpack compiler 安装插件时会被调用一次,apply 接收 webpack compiler 对象实例的引用,
// 你可以在 compiler 对象实例上注册各种事件钩子函数,来影响 webpack 的所有构建流程, 以便完成更多其他的构建任务。

const chalk = require('chalk')
const SyncHook = require('tapable').SyncHook;

const pluginName = 'FileListPlugin'
const Log = (str, obj) => {console.log(chalk.blue('\n'+str));console.log(obj, +'\n')}

class FileListPlugin {
constructor(options) {
this.options = options
}

apply(compiler) {
if (compiler.hooks.myCustomHook) throw new Error('Already in use');
compiler.hooks.myCustomHook = new SyncHook(['a', 'b', 'c']);

// 在 compiler 的 emit hook 中注册一个方法,当 webpack 执行到该阶段时会调用这个方法
compiler.hooks.emit.tap(pluginName, (compilation) => {
console.log('emit')
compiler.hooks.myCustomHook.call(1, 2, 3);
let chunkslist = 'In this build chunks:\n\n'
let chunks = compilation.chunks
// console.log('chunks', chunks)
for (let chunkid of chunks) {
// Log('chunkid', chunkid)
chunkslist += ('- ' + chunkid + '\n')
}
// 将 chunks 信息写下来
compilation.assets['chunkslist.md'] = {
source: function () {
return chunkslist
},
size: function () {
return chunkslist.length
},
}

let filelist = 'In this build:\n\n'
// 遍历所有编译后的资源,每一个文件添加一行说明
for (let filename in compilation.assets) {
filelist += ('- ' + filename + '\n')
}

// 将列表作为一个新的文件资源插入到 webpack 构建结果中
compilation.assets['filelist.md'] = {
source: function () {
return filelist
},
size: function () {
return filelist.length
},
}
})

compiler.hooks.compile.tap(pluginName, (compilation) => {
Log('compile', compilation.chunks)
})
compiler.hooks.compilation.tap(pluginName, (compilation) => {
// Log('compilation', compilation.chunks)
compilation.hooks.optimize.tap(pluginName, (coms) => {
Log('compilation.hooks', coms)
})
})
compiler.hooks.done.tap(pluginName, (compilation) => {
Log('done', compilation.chunks)
})
compiler.hooks.failed.tap(pluginName, (compilation) => {
Log('failed', compilation.chunks)
})
compiler.hooks.afterEmit.tap(pluginName, (compilation) => {
Log('afterEmit', compilation.chunks.length)
})
compiler.hooks.run.tapAsync(pluginName, (compilation,callback) => {
Log('run', compilation.chunks)
callback()
})
compiler.hooks['watchRun'].tap(pluginName, (compilation) => {
Log('watchRun', compilation.chunks)
})
}
}

module.exports = FileListPlugin