现在我们要干两件事情:
- 对上一节的备注插件的备注1进行修改,让它把我们的备注2写到index.html里面去
- 提供一个对外的hook,让其它插件可以把他们的备注3传过来,然后修改上一节的备注插件,把备注3写到index.html里面去
准备工作
本文的工程环境承接上文
这个简单的工程目录如下:
- build
- node_modules
- package.json
- plugins
- 0.createLicense.js
- 1.createFileList.js
- 2.createLicense.js
- 3.addRemark.js
- 4.addRemark.js
- loaders
- src
- index.js
- index.html
- LICENSE
实现功能1
我们来分析一下,各个插件的函数的执行顺序。
- 首先,肯定是各个构造函数,按照在plugin数组的位置依次执行
- 在这之后,各个apply函数,按照在plugin数组的位置依次执行
- 然后,才是apply函数里面的hook订阅传入的回调函数,以及回调函数里订阅传入的回调函数…不断套娃
- 同一插件可以按套娃深度来做一个执行顺序判断
- 同一hook的回调按照订阅顺序做个判断
- 不同hook按照webpack的执行逻辑做个先后判断
我们再来看一下,添加备注的插件是怎么利用这个配置的:
- 它在构造函数里面把传入的参数经过一定处理放到
this.remarks
, - 在apply函数挂上钩子之前并没有做任何修改,
- 然后在钩子回调函数里面直接使用
this.remarks
简便起见,我们不妨在插件的apply函数里直接就把备注插件的remarks给修改掉。
我们可以在compiler.options.plugins
获得所有插件实例。
const pluginName = 'WebpackConfigResetPlugin';
class WebpackConfigResetPlugin {
constructor(options = {}) {
this.remarks = options.remarks || '<!-- 这是一条由WebpackConfigResetPlugin生成的默认备注 -->'
}
apply(compiler) {
const { hooks, options, webpack } = compiler;
const outputPath = options.output.path
options.plugins.forEach(plugin => {
if('AddRemarksPlugin' === plugin.constructor.name){
plugin.remarks = this.remarks
}
});
}
}
module.exports = WebpackConfigResetPlugin;
分析功能2
功能2和上一节中html-webpack-plugin做的事情十分类似,让我们来看看它是怎么做的吧。
看看ReadMe和源码:
- 它在lib/hooks.js生成了若干个hook,
- 然后在index.js#L347发布了beforeEmit这一promise,
- 这使得我们自己实现的的beforeEmit的订阅回调函数得以执行,也就是html内容添加了备注。
这里面有个关于tapable的知识点,可以了解一下。我们也可以参考官方文档的各种hooks。
hook类型很多,涉及到同步异步,多订阅等等。我们先以一个同步钩子为例,有个概念。
- 我先生成一个钩子
const { SyncHook } = require('tapable'); const hook = new SyncHook(['age']);
- 订阅。传入回调,此时不会立刻执行
hook.tap('self intro', (age) => { console.log(`I'm ${age} years old`); });
- 发布。 此时订阅传入的回调函数会开始执行
hook.call('18');
写一个Hook
好了,分析到这里,我们参照lib/hooks.js写一个最简单的同步钩子。
- 因为字多难以理解,且容易混淆,我们将在下文使用简称:
- 事件1: 添加备注插件进行添加备注这一动作
- 事件2: 本插件对添加备注插件进行配置修改
- 事件3: 其它插件对本插件进行配置修改
- 事件4: 本插件通知其它插件对本插件进行配置修改
- 事件5: 其它插件订阅本插件对配置修改的hook
-
本插件只关注事件2和事件4
- 先确定hook 的发布时机(即事件4)。
注意到我们要仿写的hook是和compilation绑定的,我们的hook只能挂在compilation上。
那么事件4区间大概在hooks.thisCompilation ~ hooks.afterEmit之间,因为其它hook不传compilation参数
再看看我们要做的事情,分析上一节的添加备注插件可知:- 事件1发生在HtmlWebpackPlugin的beforeEmit的回调上
- 那么事件2应当在HtmlWebpackPlugin.getHooks(compilation).beforeEmit之前
(最好不要挂beforeEmit,因为这样会要考虑顺序问题) - 事件3在事件2之后
- 事件4在事件3之后(这件事情是必定发生的, 而且因为是最简单的同步钩子,这里不必过多考虑)
- 那么事件4区间在事件2之前,在.beforeEmit之前
理论上,我们可以任意选取符合上面两个要求的时机来实现事件4和事件2。
在这里,我选择了在hooks.thisCompilation的回调函数里面发布我们的hook事件。
这给后面的事件2留下了充足的弹性空间。
这也意味着,事件4将发生在.thisCompilation之后,在.compilation之前。
因此我们把暴露的钩子命名为.beforeCompilation (或者叫.afterThisCompilation也行😳) - Hook实现
以下是plugins/hook.jsconst SyncHook = require('tapable').SyncHook; const pluginHooksMap = new WeakMap(); function getHooks(compilation) { let hooks = pluginHooksMap.get(compilation); if (hooks === undefined) { hooks = createHooks(); pluginHooksMap.set(compilation, hooks); } return hooks; } function createHooks() { return { beforeCompilation: new SyncHook(['pluginObj']), }; } module.exports = getHooks;
插件实现
已经约定好了事件4发生在hooks.thisCompilation,
而事件2应该在事件4之后,
所以我们把原来在apply方法实现的事件2挪到hooks.compilation里面
const pluginName = 'WebpackConfigResetPlugin';
const getHooks = require('./hooks')
class WebpackConfigResetPlugin {
constructor(options = {}) {
this.remarks = options.remarks || '<!-- 这是一条由WebpackConfigResetPlugin生成的默认备注 -->'
}
apply(compiler) {
const { hooks, options, webpack } = compiler;
const outputPath = options.output.path
// options.plugins.forEach(plugin => {
// if('AddRemarksPlugin' === plugin.constructor.name){
// plugin.remarks = this.remarks
// }
// });
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
getHooks(compilation).beforeCompilation.call(this)
});
compiler.hooks.compilation.tap(pluginName, (compilation) => {
options.plugins.forEach(plugin => {
if ('AddRemarksPlugin' === plugin.constructor.name) {
plugin.remarks = this.remarks
}
});
});
}
}
WebpackConfigResetPlugin.getHooks = getHooks;
module.exports = WebpackConfigResetPlugin;
调用本插件Hook的插件实现
插件对外暴露的功能都写好了,总得写个例子看看情况吧。
这个插件关注的是事件3和事件5。
事件3很好办,就一个回调函数的事儿。
- (plugin) => plugin.remarks = ‘一个备注’
事件5有点难办,因为要获取compilation对象,最早的hook是hooks.thisCompilation,但是事件4也是发生在这里。
事件4和事件5都挂在hooks.thisCompilation上,怎么保证事件5先来呢?
这就是一拍脑袋就下决定的后遗症了,当然,也可以说是经验不足,理解也不到位。
不过,也不是没有解决办法。我们确保插件实例在webpack配置plugin数组里的位置比WebpackConfigResetPlugin的实例靠前就是了。
以下是实现:
const pluginName = 'HookPlugin';
const WebpackConfigResetPlugin = require('./5.changeWebpackConfig')
class HookPlugin {
constructor(options = {}) {
this.remarks = options.remarks || '<!-- 这是一条由HookPlugin hook WebpackConfigResetPlugin 生成的默认备注 -->'
}
apply(compiler) {
const { hooks, options, webpack } = compiler;
const outputPath = options.output.path
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
WebpackConfigResetPlugin.getHooks(compilation)
.beforeCompilation.tap(
pluginName,
(plugin) => plugin.remarks = this.remarks
)
})
}
}
module.exports = HookPlugin;
这里专门提一下在webpack里面的配置:
plugins: [
...
new HookPlugin(),
new WebpackConfigResetPlugin(),
],
源代码
https://github.com/nicennnnnnnlee/webpack-plugin-loader-examples