Webpack 5 Compilation processAssets Hook 使用

4,181 阅读3分钟

Webpack 5 之前的 plugin 中,我们常用 compiler.hooks.emit 对资源进行一些处理(比如删除注释、压缩文件等),在 Webpack 5 中使用该 hook 将会得到以下警告:

(node:3881) [DEP_WEBPACK_COMPILATION_ASSETS] DeprecationWarning: Compilation.assets will be frozen in future, all modifications are deprecated.
BREAKING CHANGE: No more changes should happen to Compilation.assets after sealing the Compilation.
        Do changes to assets earlier, e. g. in Compilation.hooks.processAssets.
        Make sure to select an appropriate stage from Compilation.PROCESS_ASSETS_STAGE_*.
(Use `node --trace-deprecation ...` to show where the warning was created)

根据提示可知,我们需要使用 Compilation 中的 processAssets hook 来对资源进行再处理,根据官方的解释,该 hook 中有以下参数:

  • name:插件名称。
  • stagehook 的执行顺序。
  • additionalAssets:添加资源时的回调。

一直对 stageadditionalAssets 参数的使用存在疑惑(官方解释也是摸棱两可),今天抽空研究了下,把相关结论记录在此,以供大家参考。

stage

Webpack 提供了 PROCESS_ASSETS_STAGE_ADDITIONALPROCESS_ASSETS_STAGE_PRE_PROCESS 等 15 种 stage点击查看详情),该参数主要用于控制 processAssets hook 的执行顺序,比如下面的例子:

//plugins.js
const webpack = require('webpack');

class OnePlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('OnePlugin', (compilation) => {
      compilation.hooks.processAssets.tapAsync({
        name: 'OnePlugin',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_DERIVED,
      }, (_, callback) => {
        console.log('OnePlugin PROCESS_ASSETS_STAGE_DERIVED');
        callback();
      });

      compilation.hooks.processAssets.tapAsync({
        name: 'OnePlugin',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_PRE_PROCESS,
      }, (_, callback) => {
        console.log('OnePlugin PROCESS_ASSETS_STAGE_PRE_PROCESS');
        callback();
      });
    });
  }
}

class TwoPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('TwoPlugin', (compilation) => {
      compilation.hooks.processAssets.tapAsync({
        name: 'TwoPlugin',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
      }, (_, callback) => {
        console.log('TwoPlugin PROCESS_ASSETS_STAGE_ADDITIONAL');
        callback();
      });
    });
  }
}

module.exports = {
  OnePlugin,
  TwoPlugin,
};

//webpack.config.js
const path = require('path');
const { OnePlugin, TwoPlugin } = require('./plugins');

module.exports = {
  entry: './src/index.js',
  mode: process.env.NODE_ENV ?? 'development',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new OnePlugin(),
    new TwoPlugin(),
  ]
};

执行 npx webpack 命令:

TwoPlugin PROCESS_ASSETS_STAGE_ADDITIONAL
OnePlugin PROCESS_ASSETS_STAGE_PRE_PROCESS
OnePlugin PROCESS_ASSETS_STAGE_DERIVED

通过上面的输出,可知 Webpack 在触发 processAssets hook 时,会根据指定的 stage 对应的执行顺序优先级执行相应回调,而忽略注册顺序,如果两个回调指定的 stage 相同,最先注册的最先执行,比如下面的例子:

//plugins.js
const webpack = require('webpack');

class OnePlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('OnePlugin', (compilation) => {
      compilation.hooks.processAssets.tapAsync({
        name: 'OnePlugin',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
      }, (_, callback) => {
        console.log('OnePlugin PROCESS_ASSETS_STAGE_ADDITIONAL');
        callback();
      });
    });
  }
}

class TwoPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('TwoPlugin', (compilation) => {
      compilation.hooks.processAssets.tapAsync({
        name: 'TwoPlugin',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
      }, (_, callback) => {
        console.log('TwoPlugin PROCESS_ASSETS_STAGE_ADDITIONAL');
        callback();
      });
    });
  }
}

module.exports = {
  OnePlugin,
  TwoPlugin,
};

//webpack.config.js
const path = require('path');
const { OnePlugin, TwoPlugin } = require('./plugins');

module.exports = {
  entry: './src/index.js',
  mode: process.env.NODE_ENV ?? 'development',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new OnePlugin(),
    new TwoPlugin(),
  ]
};

执行 npx webpack 命令:

OnePlugin PROCESS_ASSETS_STAGE_ADDITIONAL
TwoPlugin PROCESS_ASSETS_STAGE_ADDITIONAL

通过输出可知,在指定了相同的 stage 时,最先注册的先执行,后注册的后执行。

additionalAssets

先看下面的例子:

//plugins.js
const webpack = require('webpack');

class OnePlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('OnePlugin', (compilation) => {
      compilation.hooks.processAssets.tapAsync({
        name: 'OnePlugin',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_PRE_PROCESS,
        additionalAssets: true,
      }, (_, callback) => {
        console.log('OnePlugin PROCESS_ASSETS_STAGE_PRE_PROCESS');
        callback();
      });

      compilation.hooks.processAssets.tapAsync({
        name: 'OnePlugin',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_DERIVED,
        additionalAssets: true,
      }, (_, callback) => {
        console.log('OnePlugin PROCESS_ASSETS_STAGE_DERIVED');
        callback();
      });


      compilation.hooks.processAssets.tapAsync({
        name: 'OnePlugin',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
        additionalAssets: (_, callback) => {
          console.log('OnePlugin PROCESS_ASSETS_STAGE_ADDITIONAL additionalAssets callback')
          callback();
        },
      }, (_, callback) => {
        console.log('OnePlugin PROCESS_ASSETS_STAGE_ADDITIONAL');
        callback();
      });

      compilation.hooks.processAssets.tapAsync({
        name: 'OnePlugin',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS ,
        additionalAssets: true,
      }, (_, callback) => {
        console.log('OnePlugin PROCESS_ASSETS_STAGE_ADDITIONS');
        callback();
      });
    });
  }
}

class TwoPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('TwoPlugin', (compilation) => {
      compilation.hooks.processAssets.tapAsync({
        name: 'TwoPlugin',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_DERIVED,
      }, (_, callback) => {
        console.log('TwoPlugin PROCESS_ASSETS_STAGE_DERIVED');
        compilation.emitAsset('hello.txt',  new webpack.sources.RawSource('Hello World'));
        callback();
      });
    });
  }
}

module.exports = {
  OnePlugin,
  TwoPlugin,
};

//webpack.config.js
const path = require('path');
const { OnePlugin, TwoPlugin } = require('./plugins');

module.exports = {
  entry: './src/index.js',
  mode: process.env.NODE_ENV ?? 'development',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new OnePlugin(),
    new TwoPlugin(),
  ]
};

执行 npx webpack 命令:

OnePlugin PROCESS_ASSETS_STAGE_ADDITIONAL
OnePlugin PROCESS_ASSETS_STAGE_PRE_PROCESS
OnePlugin PROCESS_ASSETS_STAGE_DERIVED
TwoPlugin PROCESS_ASSETS_STAGE_DERIVED
OnePlugin PROCESS_ASSETS_STAGE_PRE_PROCESS
OnePlugin PROCESS_ASSETS_STAGE_DERIVED
OnePlugin PROCESS_ASSETS_STAGE_ADDITIONAL additionalAssets callback
OnePlugin PROCESS_ASSETS_STAGE_ADDITIONS

根据前文对 stage 的介绍,该命令的输出应该为:

OnePlugin PROCESS_ASSETS_STAGE_ADDITIONAL
OnePlugin PROCESS_ASSETS_STAGE_PRE_PROCESS
OnePlugin PROCESS_ASSETS_STAGE_DERIVED
TwoPlugin PROCESS_ASSETS_STAGE_DERIVED
OnePlugin PROCESS_ASSETS_STAGE_ADDITIONS

但根据输出可知,在 TwoPlugin PROCESS_ASSETS_STAGE_DERIVEDOnePlugin PROCESS_ASSETS_STAGE_ADDITIONS 之间多出了以下几个输出:

OnePlugin PROCESS_ASSETS_STAGE_PRE_PROCESS
OnePlugin PROCESS_ASSETS_STAGE_DERIVED
OnePlugin PROCESS_ASSETS_STAGE_ADDITIONAL additionalAssets callback

这是因为在 TwoPluginprocessAssets hook 中我们通过 compilation.emitAsset 新增加了一个 hello.txt 资源,并且 OnePlugin 中已执行的几个 processAssets hook 均设置了 additionalAssets 参数,因此此刻 Webpack 先回过头触发相关的回调,然后再按照 stage 规则继续执行其它的 processAssets hook

根据上面的分析可知,参数 additionalAssets 的主要作用是为了监听后续要执行的 processAssets hook 所新增的资源,以便对其进行处理。