JS `Webpack` `Compiler` / `Compilation` 生命周期钩子:自定义打包流程

各位观众老爷,晚上好!我是你们的老朋友,今天咱们聊点硬核的——Webpack Compiler/Compilation 生命周期钩子,教你玩转自定义打包流程,让Webpack给你打工!

开场白:Webpack,你可真是个小机灵鬼!

Webpack这玩意,用起来方便是真方便,但如果你想深入了解它到底在背后搞些什么,那就得好好研究它的生命周期钩子了。这些钩子就像一个个关键的“时间节点”,允许你在Webpack打包过程中的特定时刻插入你的代码,改变它的行为,实现各种骚操作。

第一幕:Compiler vs Compilation,傻傻分不清楚?

在开始之前,我们先来捋清楚两个概念:Compiler 和 Compilation。

  • Compiler(编译器): Webpack 的核心引擎,负责启动整个编译流程。你可以把它想象成一个项目经理,负责分配任务、协调资源。Compiler 实例只会在启动 Webpack 时创建一次,贯穿整个构建过程。

  • Compilation(编译): 代表一次打包的过程。当 Webpack 检测到文件变更,需要重新打包时,就会创建一个新的 Compilation 实例。你可以把它想象成一个具体的施工队,负责将你的代码转换成最终的静态资源。

简单来说,Compiler 负责“启动”和“管理”,Compilation 负责“执行”和“产出”。一个 Compiler 可以创建多个 Compilation。

第二幕:生命周期钩子,Webpack 的秘密武器

Webpack 提供了大量的生命周期钩子,允许你在编译的不同阶段执行自定义逻辑。这些钩子分为两类:Compiler 钩子和 Compilation 钩子。

Compiler 钩子:全局掌控,运筹帷幄

Compiler 钩子提供了全局性的控制,允许你在 Webpack 启动、停止等关键时刻进行干预。常用的 Compiler 钩子包括:

钩子名称 触发时机 作用
beforeRun 在 Compiler 运行之前触发 允许你在 Webpack 运行之前执行一些准备工作,例如清理目录、读取配置文件等。
run 在 Compiler 运行时触发 允许你在 Webpack 运行时执行一些操作,例如修改 Compiler 的配置。
beforeCompile 在 Compilation 编译之前触发 允许你在 Compilation 编译之前执行一些准备工作,例如创建临时文件、初始化变量等。
compile 在 Compilation 编译时触发 允许你在 Compilation 编译时执行一些操作,例如修改 Compilation 的配置、添加自定义插件等。
afterCompile 在 Compilation 编译之后触发 允许你在 Compilation 编译之后执行一些清理工作,例如删除临时文件、释放资源等。
shouldEmit 在确定是否应该输出 assets 时触发 允许你决定是否应该将 Compilation 的结果输出到磁盘。如果返回 false,则 Webpack 不会生成任何文件。
emit 在将 assets 输出到磁盘之前触发 允许你在将 Compilation 的结果输出到磁盘之前执行一些操作,例如修改文件内容、添加文件头等。
afterEmit 在将 assets 输出到磁盘之后触发 允许你在将 Compilation 的结果输出到磁盘之后执行一些操作,例如上传文件到 CDN、发送通知等。
done 在 Compiler 完成所有任务后触发 允许你在 Webpack 完成所有任务后执行一些操作,例如发送构建报告、清理缓存等。
failed 在 Compiler 运行过程中发生错误时触发 允许你在 Webpack 运行过程中发生错误时执行一些操作,例如发送错误报告、回滚操作等。
invalid 在 Webpack 监测到文件变更,需要重新编译时触发 允许你在 Webpack 监测到文件变更时执行一些操作,例如清空缓存、重新加载配置文件等。
infrastructureLog 在 Webpack 输出基础设施日志时触发 允许你监听和处理 Webpack 的基础设施日志,例如诊断构建性能问题。

Compilation 钩子:细粒度控制,掌控细节

Compilation 钩子提供了更细粒度的控制,允许你在模块构建、资源生成等具体环节进行干预。常用的 Compilation 钩子包括:

钩子名称 触发时机 作用
buildModule 在开始构建模块之前触发 允许你在构建模块之前执行一些操作,例如修改模块的配置、添加自定义 loader 等。
rebuildModule 在重新构建模块之前触发 允许你在重新构建模块之前执行一些操作,例如清空模块的缓存、重新加载模块的依赖等。
finishModules 在所有模块构建完成后触发 允许你在所有模块构建完成后执行一些操作,例如分析模块的依赖关系、优化模块的加载顺序等。
optimizeDependencies 在优化模块依赖关系之前触发 允许你在优化模块依赖关系之前执行一些操作,例如修改模块的依赖关系、添加自定义的依赖优化算法等。
optimizeChunks 在优化 chunk 之前触发 允许你在优化 chunk 之前执行一些操作,例如修改 chunk 的配置、添加自定义的 chunk 优化算法等。
optimizeTree 在优化模块依赖树之前触发 允许你在优化模块依赖树之前执行一些操作,例如删除无用的模块、合并重复的模块等。
optimizeChunkAssets 在优化 chunk assets 之前触发 允许你在优化 chunk assets 之前执行一些操作,例如修改 chunk assets 的内容、压缩 chunk assets 等。
processAssets 在处理 assets 之前触发 允许你在处理 assets 之前执行一些操作,例如修改 assets 的内容、添加 assets 的元数据等。从Webpack 5开始推荐使用processAssets代替之前的optimizeAssets钩子,因为它提供了更灵活的控制,并且可以处理各种类型的 assets,例如 JavaScript、CSS、图片等。
additionalAssets 在添加额外的 assets 时触发 允许你在 Compilation 中添加额外的 assets,例如生成 HTML 文件、生成 manifest 文件等。
record 在记录 Compilation 的状态时触发 允许你在记录 Compilation 的状态时执行一些操作,例如将 Compilation 的状态保存到数据库、发送 Compilation 的状态到监控系统等。
succeedModule 在模块构建成功后触发 允许你在模块构建成功后执行一些操作,例如记录模块的构建时间、发送模块构建成功的通知等。
failedModule 在模块构建失败后触发 允许你在模块构建失败后执行一些操作,例如记录模块的构建错误信息、发送模块构建失败的通知等。
stillValidModule 在模块仍然有效时触发 (例如在 watch 模式下) 允许你在模块仍然有效时执行一些操作,例如更新模块的缓存、重新计算模块的 hash 值等。
moduleAsset 在模块生成 asset 时触发 允许你在模块生成 asset 时执行一些操作,例如修改 asset 的内容、添加 asset 的元数据等。
chunkAsset 在 chunk 生成 asset 时触发 允许你在 chunk 生成 asset 时执行一些操作,例如修改 asset 的内容、添加 asset 的元数据等。
needAdditionalSeal 在需要进行额外 sealing 时触发 (sealing 是指 Compilation 完成,不再接受更多模块) 允许你在需要进行额外 sealing 时执行一些操作,例如添加额外的 chunk、添加额外的 asset 等。
beforeModuleAssets 在生成模块的 assets 之前触发 允许你在生成模块的 assets 之前执行一些操作,例如修改模块的配置、添加自定义的 loader 等。
afterModuleAssets 在生成模块的 assets 之后触发 允许你在生成模块的 assets 之后执行一些操作,例如修改 assets 的内容、添加 assets 的元数据等。

第三幕:如何使用生命周期钩子?

使用生命周期钩子,主要有两种方式:

  1. 通过 Webpack 插件: 这是最常见的方式。你可以创建一个自定义的 Webpack 插件,然后在插件的 apply 方法中注册钩子。
  2. 直接访问 Compiler/Compilation 对象: 如果你想在 Webpack 配置文件中直接使用钩子,可以通过 Compiler 或 Compilation 对象来注册。

举个栗子:使用插件修改输出文件内容

假设我们想在每个输出文件的顶部添加一段版权声明。我们可以创建一个名为 CopyrightWebpackPlugin 的插件:

// copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
  constructor(options) {
    this.options = options || {};
  }

  apply(compiler) {
    compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, callback) => {
      // 遍历所有 chunks
      for (const name in compilation.assets) {
        if (compilation.assets.hasOwnProperty(name)) {
          const asset = compilation.assets[name];
          let content = asset.source();
          let prefix = this.options.banner || '// Copyright (c) 2023n'; // 默认版权声明

          asset._value = prefix + content;
        }
      }
      callback();
    });
  }
}

module.exports = CopyrightWebpackPlugin;

然后在 webpack.config.js 中使用这个插件:

// webpack.config.js
const path = require('path');
const CopyrightWebpackPlugin = require('./copyright-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  plugins: [
    new CopyrightWebpackPlugin({ banner: '// My Awesome Projectn' }), // 自定义版权声明
  ],
};

在这个例子中,我们使用了 emit 钩子,它会在 Webpack 将 assets 输出到磁盘之前触发。在钩子函数中,我们遍历所有 assets,并在每个文件的顶部添加了版权声明。

再来一个栗子:使用 Compiler 对象监听构建完成事件

如果你想在构建完成后执行一些操作,例如发送构建报告,可以使用 done 钩子:

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

module.exports = {
  // ... 其他配置
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
    }),
    {
      apply: (compiler) => {
        compiler.hooks.done.tap('DonePlugin', (stats) => {
          console.log('构建完成!');
          // 在这里发送构建报告
        });
      },
    },
  ],
};

在这个例子中,我们直接在 webpack.config.js 中使用 Compiler 对象注册了 done 钩子。当 Webpack 构建完成后,控制台会输出 "构建完成!"。

第四幕:异步钩子,别掉坑里了!

Webpack 的钩子分为同步钩子和异步钩子。对于异步钩子,你需要使用 tapAsynctapPromise 方法来注册,并且需要在钩子函数中调用 callback 或返回 Promise 来通知 Webpack 钩子函数已经执行完毕。

如果你忘记调用 callback 或返回 Promise,Webpack 会一直等待,导致构建卡死。

第五幕:高级技巧,玩转自定义打包流程

  • 动态修改 Webpack 配置: 你可以在 beforeRunrun 钩子中动态修改 Webpack 的配置,例如根据环境变量动态加载不同的 loader 或插件。
  • 自定义 Loader: 你可以创建自定义的 Loader,用于处理特定类型的文件。Loader 本身也可以使用 Compiler 和 Compilation 钩子。
  • 自定义 Plugin: 你可以创建自定义的 Plugin,用于扩展 Webpack 的功能。Plugin 可以监听各种 Compiler 和 Compilation 钩子,实现各种复杂的打包流程。
  • 利用processAssets进行资源优化: 从 Webpack 5 开始,processAssets钩子是资源优化最推荐的方式。你可以使用它来压缩图片,优化 CSS,甚至进行代码分割策略的调整。

示例:使用 processAssets 钩子压缩图片

假设你有一个图片压缩插件,可以使用 processAssets 钩子来应用它。

// image-optimization-plugin.js
class ImageOptimizationPlugin {
  constructor(options) {
    this.options = options || {};
  }

  apply(compiler) {
    compiler.hooks.compilation.tap('ImageOptimizationPlugin', (compilation) => {
      compilation.hooks.processAssets.tapAsync(
        {
          name: 'ImageOptimizationPlugin',
          stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE // 优化大小的阶段
        },
        (assets, callback) => {
          const imageAssets = Object.keys(assets).filter(assetName =>
            /.(png|jpg|jpeg|gif|svg)$/.test(assetName)
          );

          if (imageAssets.length === 0) {
            return callback();
          }

          Promise.all(imageAssets.map(assetName => {
            return new Promise((resolve, reject) => {
              // 模拟图片压缩,实际需要调用图片压缩库
              const originalContent = assets[assetName].source();
              const optimizedContent = `Optimized: ${originalContent}`;

              assets[assetName] = {
                source: () => optimizedContent,
                size: () => optimizedContent.length
              };
              resolve();
            });
          }))
            .then(() => callback())
            .catch(err => callback(err));
        }
      );
    });
  }
}

module.exports = ImageOptimizationPlugin;
// webpack.config.js
const ImageOptimizationPlugin = require('./image-optimization-plugin');

module.exports = {
  // ... 其他配置
  plugins: [
    new ImageOptimizationPlugin()
  ]
};

在这个例子中,我们首先找到所有图片资源,然后模拟压缩它们。 PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE 常量确保我们的插件在资源大小优化的阶段运行。

第六幕:调试技巧,排坑指南

调试 Webpack 插件可能会比较棘手。以下是一些常用的调试技巧:

  • 使用 console.log 在钩子函数中打印日志,可以帮助你了解 Webpack 的执行流程和插件的行为。
  • 使用 debugger 在钩子函数中添加 debugger 语句,可以在浏览器中进行断点调试。
  • 使用 Webpack 的 stats 对象: Webpack 的 stats 对象包含了构建过程的详细信息,可以帮助你分析构建性能和资源依赖关系。
  • 利用 Source Maps: 确保你的 Webpack 配置启用了 Source Maps,这样可以方便你调试 Loader 和 Plugin 的代码。

总结:Webpack,听我的!

Webpack 的 Compiler/Compilation 生命周期钩子是强大的工具,允许你自定义打包流程,实现各种复杂的构建需求。掌握这些钩子,你就可以让 Webpack 真正为你打工,构建出更高效、更灵活的应用程序。

记住,理解 Compiler 和 Compilation 的区别,选择合适的钩子,处理好异步钩子,你就能玩转 Webpack 的生命周期,成为 Webpack 的 Master!

今天的讲座就到这里,希望对大家有所帮助。下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注