likes
comments
collection
share

还不会写Webpack Plugin,那就快来看这篇文章

作者站长头像
站长
· 阅读数 4

本质上在 Webpack 编译阶段会为各个编译对象初始化不同的 Hook ,开发者可以在自己编写的 Plugin 中监听到这些 Hook ,在打包的某个特定时间段触发对应 Hook 注入特定的逻辑从而实现自己的行为。

可以换句话定义,就是 Plugin 内部完全是基于 tapable 来实现,由于 tapable 事件流机制过于硬核,这里就不细说,只要理解为一个事件监听的类即可

我们来确认 plugin 的实现要求

  • 一个 JavaScript 命名函数或者一个类
  • 在插件函数的 prototype 或者类上定义一个 apply 方法(方法参数是 compiler 对象)
  • 指定一个绑定到 webpack 自身的事件钩子 (opens new window)
  • 处理 webpack 内部实例的特定数据
  • 功能完成后调用 webpack 提供的 callback 回调函数

基本构成

class ExamplePlugin {
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options){
    //
  }
  
  // Webpack 会调用 ExamplePlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler) {
    // 在emit阶段插入钩子函数,用于特定时机处理额外的逻辑;
    compiler.hooks.emit.tap('ExamplePlugin', (compilation) => {
      // 在功能流程完成后可以调用 webpack 提供的回调函数;
    });

    // 如果事件是异步的,会带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知webpack,才会进入下一个处理流程。
    compiler.plugin('emit',function(compilation, callback) {
      // 支持处理逻辑
      // 处理完毕后执行 callback 以通知 Webpack 
      // 如果不执行 callback,运行流程将会一直卡在这不往下执行 
      callback();
    });
  }
}

module.exports = ExamplePlugin;
  • 在配置文件中,Webpack 在读取配置的过程中会先执行 new ExamplePlugin(options) 获取实例对象

  • 在初始化阶段,Webpack 会逐个调用每个 plugin 的 apply 方法给插件实例传入 compiler 对象

  • 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(eventName, callback) 监听到 Webpack 广播出来的事件,并且可以通过 compiler 对象去操作 Webpack

读取输出资源、代码块、模块及其依赖

emit 事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容

class ExamplePlugin {
  apply(compiler) {
    // 异步事件,需要调用 callback 回调函数通知 webpack,才会进入下一个处理流程。
    compiler.plugin('emit', function (compilation, callback) {
      // compilation.chunks 存放所有代码块,是一个数组
      compilation.chunks.forEach(function (chunk) {
        // chunk 代表一个代码块
        // 代码块由多个模块组成,通过 chunk.forEachModule 能读取组成代码块的每个模块
        chunk.forEachModule(function (module) {
          // module 代表一个模块
          // module.fileDependencies 存放当前模块的所有依赖的文件路径,是一个数组
          module.fileDependencies.forEach(function (filepath) {
          });
        });

        // Webpack 会根据 Chunk 去生成输出的文件资源,每个 Chunk 都对应一个及其以上的输出文件
        // 例如在 Chunk 中包含了 CSS 模块并且使用了 ExtractTextPlugin 时,
        // 该 Chunk 就会生成 .js 和 .css 两个文件
        chunk.files.forEach(function (filename) {
          // compilation.assets 存放当前所有即将输出的资源
          // 调用一个输出资源的 source() 方法能获取到输出资源的内容
          const source = compilation.assets[filename].source();
        });
      });

      // 如果忘记了调用 callback,Webpack 将一直卡在这里而不会往后执行。
      callback();
    })
  }
}

监听文件变化

Webpack 会从配置的入口模块出发,依次找出所有的依赖模块,当入口模块或者其依赖的模块发生变化时, 就会触发一次新的 Compilation

这里提一嘴:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译,只要文件有改动,Compilation就会被重新创建

在开发插件时经常需要知道是哪个文件发生变化导致了新的 Compilation,为此可以使用如下代码

// 当依赖的文件发生变化时会触发 watch-run 事件
compiler.hooks.watchRun.tap('ExamplePlugin', (watching, callback) => {
  // 获取发生变化的文件列表
  const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
  
  // changedFiles 格式为键值对,键为发生变化的文件路径。
  if (changedFiles[filePath] !== undefined) {
    // filePath 对应的文件发生了变化
  }
  
  callback();
});

修改输出资源

当我们需要修改、增加或者删除输出的资源,我们就需要监听 emit 事件。emit 事件执行时,所有模块的转换和代码块对应的文件已经生成好, 需要输出的资源即将输出,因此 emit 事件是修改 Webpack 输出资源的最后时机

所有需要输出的资源会存放在 compilation.assets 中,compilation.assets 是一个键值对,键为需要输出的文件名称,值为文件对应的内容

// 设置名称为 fileName 的输出资源
compilation.assets[fileName] = {
  // 返回文件内容
  source: () => {
    // fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer
    return fileContent;
    },
  // 返回文件大小
    size: () => {
    return Buffer.byteLength(fileContent, 'utf8');
  }
};

callback();

如何判断使用了哪些插件

// 判断当前配置使用使用了 ExtractTextPlugin,
// compiler 参数即为 Webpack 在 apply(compiler) 中传入的参数
function hasExamplePlugin(compiler) {
  // 当前配置所有使用的插件列表
  const plugins = compiler.options.plugins;
  
  // 去 plugins 中寻找有没有 ExtractTextPlugin 的实例
  return plugins.find(plugin => plugin.__proto__.constructor === ExamplePlugin) != null;
}

终端不中断编译进行打印警告

如果你在 apply 函数内插入 throw new Error("Message"),终端会提示报错,并将 webpack 中断处理,为了不影响 webpack 的执行,要在编译期间向用户发出警告或错误消息,则应使用 compilation.warnings 和 compilation.errors

compilation.warnings.push("warning");
compilation.errors.push("error");

实战

我们来实现一个 FileListTxtPlugin 插件,实现将 webpack 打印的文件相关信息枚举出来,并生成一份清单输出到打包目录下。

根据上文讲解,我们可以通过 emit hook 来进行

  1. 初始化插件文件
class FileListTxtPlugin {
  // apply函数 帮助插件注册,接收complier类
  constructor(options) {
    // webpack 中配置的 options 对象
    this.options = options;
  }

  apply(complier) {
    // 需要读取和写入文件,这里就用异步事件
    complier.hooks.emit.tapAsync(() => {
      // ...
    })
  }
}

export default FileListTxtPlugin
  1. 插入逻辑
class FileListTxtPlugin {
  // apply函数 帮助插件注册,接收complier类
  constructor(options) {
    // webpack 中配置的 options 对象
    this.options = options;
  }

  apply(complier) {
    // 异步的钩子
    complier.hooks.emit.tapAsync(
      'FileListTxtPlugin',
      (compilation, callback) => {
        // 获取所有文件路径
        const fileDependencies = [...compilation.fileDependencies];
        
        // 打包后 dist 目录下的文件资源都放在 assets 对象中
        const assets = compilation.assets;
        
        // 定义返回文件的内容
        let fileContent = `文件数量:${Object.keys(assets).length}\n文件列表:`;

        // 遍历 dist 目录下的所有资源
        Object.keys(assets).forEach((item) => {
          // 文件的源内容
          const source = assets[item].source();
          // 文件的大小
          let size = assets[item].size();

          // 如果大于 1024 byte,则用 kb
          size =
            size >= 1024 ? `${(size / 1024).toFixed(2)}/kb` : `${size}/bytes`;

          // 获取对应的文件路径
          const sourcepath =
            fileDependencies.find((path) => {
              if (path.includes(item)) return path;
            }) || '';

          // 文件内容追加
          fileContent = `${fileContent}\n  filename: ${item}    size: ${size}   ${sourcepath}`;
        });

        // 添加自定义输出文件,这里是添加文件输出,这里可以修改替换对应的文件
        compilation.assets['fileList.txt'] = {
          source: function () {
            // 定义文件的内容
            return fileContent;
          },
          size: function () {
            // 定义文件的体积
            return Buffer.byteLength(fileContent, 'utf8');
          },
        };
        
        // 注意,异步钩子中 callback 函数必须要调用
        callback();
      }
    );
  }
}

module.exports = FileListTxtPlugin;
  1. 注册插件
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const FileListTxtPlugin = require('./plugins/fileListTxtPlugin');

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  // mode: 'production',
  output: {
    clean: true, // 在生成文件之前清空 output 目录
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
  },
  module: {
    rules: [
      // ...
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      filename: 'index.html',
    }),
    new FileListTxtPlugin(),
  ],
};