NiceLeeのBlog 用爱发电 bilibili~

Webpack Plugin制作6-修改其它插件的配置

2021-10-13
nIceLee

阅读:


现在我们要干两件事情:

  • 对上一节的备注插件的备注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.js
      const 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

系列文章


内容
隐藏