webpack 中如何自定义 loader

Writing a webpack loader

Loader Interface

A loader is a JavaScript module that exports a function. The loader runner calls this function and passes the result of the previous loader or the resource file into it. The this context of the function is filled-in by webpack and the loader runner with some useful methods that allow the loader (among other things) to change its invocation style to async, or get query parameters.

The first loader is passed one argument: the content of the resource file. The compiler expects a result from the last loader. The result should be a String or a Buffer (which is converted to a string), representing the JavaScript source code of the module. An optional SourceMap result (as a JSON object) may also be passed.

A single result can be returned in sync mode. For multiple results the this.callback() must be called. In async mode this.async() must be called to indicate that the loader runner should wait for an asynchronous result. It returns this.callback(). Then the loader must return undefined and call that callback.

js
1/**
2 *
3 * @param {string|Buffer} content Content of the resource file
4 * @param {object} [map] SourceMap data consumable by https://github.com/mozilla/source-map
5 * @param {any} [meta] Meta data, could be anything
6 */
7function webpackLoader(content, map, meta) {
8  // code of your webpack loader
9}

this.emitFile

在 Webpack 的 loader 中,this.emitFile 是一个重要的方法,它用于向输出目录中添加一个文件。当你编写自定义的 loader 时,可能需要生成一些额外的文件,这时就可以使用 this.emitFile 方法。

this.emitFile 方法接受三个参数:

  1. 文件名 (name): 输出文件的名称。
  2. 内容 (content): 输出文件的内容,通常是一个 Buffer 或者字符串。
  3. 源地图 (sourceMap): 可选参数,如果文件有对应的 source map,可以在这里传入。

使用 this.emitFile 方法可以让你在 Webpack 构建过程中动态地添加文件到输出结果中。这在处理诸如图片、字体文件或者其他静态资源时特别有用。例如,你可能编写了一个将图片转换为 base64 字符串的 loader,但同时也想保留原始的图片文件作为输出,这时就可以使用 this.emitFile

简单示例代码如下:

javascript
1module.exports = function (source) {
2  // 对源代码进行一些处理
3  // ...
4
5  // 添加一个新的文件到输出中
6  this.emitFile("someFileName.ext", "文件内容");
7
8  // 返回处理后的源代码
9  return source;
10};

在使用 this.emitFile 时,需要注意的是,这会影响到最终生成的文件结构,因此要谨慎使用,确保它符合你的构建需求。

this.callback

webpack 的 loader 中的 this.callback 函数是一个用于异步返回处理结果的方法。它允许 loader 输出多个结果,并且可以传递错误信息或者警告信息。这对于那些需要执行异步操作或者需要返回多个值的场景特别有用。

this.callback 接受以下参数:

  1. error:一个可选的错误参数,如果 loader 过程中出现错误,可以通过这个参数传递错误信息。
  2. content:处理后的内容。
  3. sourceMap:可选的 source map。
  4. meta:可选的任何其他元数据。

下面是一个 this.callback 的示例用法:

javascript
1module.exports = function (source) {
2  const callback = this.async(); // 获取异步的 callback 函数
3
4  someAsyncOperation(source, (err, result, sourceMap, meta) => {
5    if (err) {
6      return callback(err);
7    }
8
9    // 异步操作完成后,通过 callback 返回结果
10    callback(null, result, sourceMap, meta);
11  });
12};

在这个示例中,someAsyncOperation 是一个假设的异步函数,用来处理 loader 接收到的 source。处理完成后,它会调用回调函数,传递错误信息(如果有的话),处理后的结果,以及可选的 source map 和其他元数据。然后,我们使用 this.callback 将这些信息传递回 webpack。如果处理过程中有错误发生,this.callback 的第一个参数将传递错误对象,webpack 将处理这个错误。

this.callback 和 return 的最主要区别

多个输出:

  • this.callback: 允许你返回多个结果,比如除了处理后的内容外,还可以返回一个 source map 或其他元数据。
  • return:只能返回一个输出,即处理后的内容。

Pitching Loader

在 webpack 中,一个 pitching loader 是一种特殊类型的 loader,它利用 webpack loader 的 "pitching" 阶段。这个阶段发生在常规的 loader 处理阶段之前,允许 loaders 执行一些预处理操作,甚至可以决定跳过后续的 loaders。

每个 loader 都可以提供一个 pitch 方法。当 webpack 处理模块时,它会首先按照从右到左的顺序调用所有 loader 的 pitch 方法。如果某个 loader 的 pitch 方法返回了一个结果,那么 webpack 将跳过剩余的 loaders,并开始执行加载链中的上一个 loader。

Pitching Loader 的结构

一个典型的 pitching loader 可以这样编写:

javascript
1module.exports = function (content) {
2  // 这是常规的 loader 处理函数
3  // 这里处理并返回模块内容
4};
5
6module.exports.pitch = function (remainingRequest, precedingRequest, data) {
7  // 这是 pitch 函数
8  // remainingRequest - 剩余请求字符串,表示 pitch 函数之后的 loaders 和资源路径
9  // precedingRequest - 在当前 loader 之前的 loaders 字符串
10  // data - 一个可以在 pitch 和常规 loader 函数之间共享的对象
11  const callback = this.async(); // 可以异步
12
13  setTimeout(() => {
14    data.metadata = "pitch";
15    callback();
16  }, 10);
17  // 你可以在这里执行一些操作,甚至返回一个结果来跳过剩余的 loaders
18};

用途和示例

Pitching loaders 非常有用,比如:

  • 条件性地应用 loaders:基于某些条件判断是否跳过后续的 loaders。
  • 避免不必要的处理:如果可以从缓存中获取结果,则跳过后续的 loaders。
  • 共享数据:在 pitch 阶段和常规 loader 处理阶段之间共享数据。

下面是一个简单的示例:

javascript
1module.exports.pitch = function(remainingRequest) {
2    if (/* 某些条件 */) {
3        // 返回 JavaScript 代码作为模块的结果
4        return 'module.exports = "这是从 Pitching Loader 返回的内容";';
5    }
6    // 如果没有返回值,webpack 将继续执行后续的 loaders
7};

在这个示例中,如果满足特定条件,pitch 方法将返回一个字符串,这个字符串将被视为模块的导出内容。这样,后续的 loaders 将被跳过,因为 webpack 认为模块已经被处理完成了。

使用 pitching loader 时需要小心,因为它们可以显著影响 webpack 的模块处理逻辑。确保你完全理解了它们的行为,特别是在涉及复杂构建流程的时候。

如何在 Loaders 之间共享 Data

pitch 阶段,你可以使用 data 对象在当前 loader 的 pitch 方法和常规 loader 函数之间共享数据。data 是一个空对象({}),你可以在其中添加任何属性。

javascript
1module.exports = function (content) {
2  // 在这里可以访问在 pitch 阶段设置的 data
3  if (this.data.someValue) {
4    // 做一些处理
5  }
6};
7
8module.exports.pitch = function (remainingRequest, precedingRequest, data) {
9  // 在 pitch 阶段设置 data
10  data.someValue = "一些数据";
11};

在这个例子中,我们在 pitch 方法中给 data 对象添加了 someValue 属性。然后,在常规 loader 函数中,我们可以通过 this.data 访问这个值。

这种数据共享机制使得 loader 可以在 pitch 阶段计算或决定一些东西,并在后续的加载过程中使用这些信息,无需再次计算。这对于性能优化和避免重复工作非常有用。

请注意,这种共享数据的方式仅适用于单个 loader 实例的 pitch 方法和常规方法之间。不同的 loader 实例之间不能共享这个 data 对象。

测试

https://webpack.js.org/contribute/writing-a-loader/#testing