现在我们要干两件事情:
- 对上一节的备注插件的备注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