Loader Interface

loader 本质上是导出为函数的 JavaScript 模块。loader runner 会调用此函数,然后将上一个 loader 产生的结果或者资源文件传入进去。函数中的 this 作为上下文会被 webpack 填充,并且 loader runner 中包含一些实用的方法,比如可以使 loader 调用方式变为异步,或者获取 query 参数。

起始 loader 只有一个入参:资源文件的内容。compiler 预期得到最后一个 loader 产生的处理结果。这个处理结果应该为 String 或者 Buffer(能够被转换为 string)类型,代表了模块的 JavaScript 源码。另外,还可以传递一个可选的 SourceMap 结果(格式为 JSON 对象)。

如果是单个处理结果,可以在 同步模式 中直接返回。如果有多个处理结果,则必须调用 this.callback()。在 异步模式 中,必须调用 this.async() 来告知 loader runner 等待异步结果,它会返回 this.callback() 回调函数。随后 loader 必须返回 undefined 并且调用该回调函数。

/**
 *
 * @param {string|Buffer} content 源文件的内容
 * @param {object} [map] 可以被 https://github.com/mozilla/source-map 使用的 SourceMap 数据
 * @param {any} [meta] meta 数据,可以是任何内容
 */
function webpackLoader(content, map, meta) {
  // 你的 webpack loader 代码
}

示例

以下部分提供了不同类型的 loader 的一些基本示例。注意,mapmeta 参数是可选的,查看下面的 this.callback

同步 Loaders

无论是 return 还是 this.callback 都可以同步地返回转换后的 content 值:

sync-loader.js

module.exports = function (content, map, meta) {
  return someSyncOperation(content);
};

this.callback 方法则更灵活,因为它允许传递多个参数,而不仅仅是 content

sync-loader-with-multiple-results.js

module.exports = function (content, map, meta) {
  this.callback(null, someSyncOperation(content), map, meta);
  return; // 当调用 callback() 函数时,总是返回 undefined
};

异步 Loaders

对于异步 loader,使用 this.async 来获取 callback 函数:

async-loader.js

module.exports = function (content, map, meta) {
  var callback = this.async();
  someAsyncOperation(content, function (err, result) {
    if (err) return callback(err);
    callback(null, result, map, meta);
  });
};

async-loader-with-multiple-results.js

module.exports = function (content, map, meta) {
  var callback = this.async();
  someAsyncOperation(content, function (err, result, sourceMaps, meta) {
    if (err) return callback(err);
    callback(null, result, sourceMaps, meta);
  });
};

"Raw" Loader

默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 rawtrue,loader 可以接收原始的 Buffer。每一个 loader 都可以用 String 或者 Buffer 的形式传递它的处理结果。complier 将会把它们在 loader 之间相互转换。

raw-loader.js

module.exports = function (content) {
  assert(content instanceof Buffer);
  return someSyncOperation(content);
  // 返回值也可以是一个 `Buffer`
  // 即使不是 "raw",loader 也没问题
};
module.exports.raw = true;

Pitching Loader

loader 总是 从右到左被调用。有些情况下,loader 只关心 request 后面的 元数据(metadata),并且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 之前,会先 从左到右 调用 loader 上的 pitch 方法。

对于以下 use 配置:

module.exports = {
  //...
  module: {
    rules: [
      {
        //...
        use: ['a-loader', 'b-loader', 'c-loader'],
      },
    ],
  },
};

将会发生这些步骤:

|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
      |- requested module is picked up as a dependency
    |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution

那么,为什么 loader 可以利用 "pitching" 阶段呢?

首先,传递给 pitch 方法的 data,在执行阶段也会暴露在 this.data 之下,并且可以用于在循环时,捕获并共享前面的信息。

module.exports = function (content) {
  return someSyncOperation(content, this.data.value);
};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  data.value = 42;
};

其次,如果某个 loader 在 pitch 方法中给出一个结果,那么这个过程会回过身来,并跳过剩下的 loader。在我们上面的例子中,如果 b-loaderpitch 方法返回了一些东西:

module.exports = function (content) {
  return someSyncOperation(content);
};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  if (someCondition()) {
    return (
      'module.exports = require(' +
      JSON.stringify('-!' + remainingRequest) +
      ');'
    );
  }
};

上面的步骤将被缩短为:

|- a-loader `pitch`
  |- b-loader `pitch` returns a module
|- a-loader normal execution

The Loader Context

loader context 表示在 loader 内使用 this 可以访问的一些方法或属性。

loader 上下文示例

下面提供一个例子,将使用 require 进行调用:

/abc/file.js 中:

require('./loader1?xyz!loader2!./resource?rrr');

this.addContextDependency

addContextDependency(directory: string)

添加目录作为 loader 结果的依赖。

this.addDependency

addDependency(file: string)
dependency(file: string) // shortcut

加入一个文件作为产生 loader 结果的依赖,使它们的任何变化可以被监听到。例如,sass-loader, less-loader 就使用了这个技巧,当它发现无论何时导入的 css 文件发生变化时就会重新编译。

this.async

告诉 loader-runner 这个 loader 将会异步地回调。返回 this.callback

this.cacheable

设置是否可缓存标志的函数:

cacheable(flag = true: boolean)

默认情况下,loader 的处理结果会被标记为可缓存。调用这个方法然后传入 false,可以关闭 loader 处理结果的缓存能力。

一个可缓存的 loader 在输入和相关依赖没有变化时,必须返回相同的结果。这意味着 loader 除了 this.addDependency 里指定的以外,不应该有其它任何外部依赖。

this.callback

可以同步或者异步调用的并返回多个结果的函数。预期的参数是:

this.callback(
  err: Error | null,
  content: string | Buffer,
  sourceMap?: SourceMap,
  meta?: any
);
  1. 第一个参数必须是 Error 或者 null
  2. 第二个参数是一个 string 或者 Buffer
  3. 可选的:第三个参数必须是一个可以被 this module 解析的 source map。
  4. 可选的:第四个参数,会被 webpack 忽略,可以是任何东西(例如一些元数据)。

如果这个函数被调用的话,你应该返回 undefined 从而避免含糊的 loader 结果。

this.clearDependencies

clearDependencies();

移除 loader 结果的所有依赖,甚至自己和其它 loader 的初始依赖。考虑使用 pitch

this.context

模块所在的目录 可以用作解析其他模块成员的上下文。

在我们的 例子 中:因为 resource.js 在这个目录中,这个属性的值为 /abc

this.data

在 pitch 阶段和 normal 阶段之间共享的 data 对象。

this.emitError

emitError(error: Error)

emit 一个错误,也可以在输出中显示。

ERROR in ./src/lib.js (./src/loader.js!./src/lib.js)
Module Error (from ./src/loader.js):
Here is an Error!
 @ ./src/index.js 1:0-25

this.emitFile

emitFile(name: string, content: Buffer|string, sourceMap: {...})

产生一个文件。这是 webpack 特有的。

this.emitWarning

emitWarning(warning: Error)

发出一个警告,在输出中显示如下:

WARNING in ./src/lib.js (./src/loader.js!./src/lib.js)
Module Warning (from ./src/loader.js):
Here is a Warning!
 @ ./src/index.js 1:0-25

this.fs

用于访问 compilation 的 inputFileSystem 属性。

this.getOptions(schema)

提取给定的 loader 选项,接受一个可选的 JSON schema 作为参数

this.getResolve

getResolve(options: ResolveOptions): resolve

resolve(context: string, request: string, callback: function(err, result: string))
resolve(context: string, request: string): Promise<string>

创建一个类似于 this.resolve 的解析函数。

在 webpack resolve 选项 下的任意配置项都是可能的。他们会被合并进 resolve 配置项中。请注意,"..." 可以在数组中使用,用于拓展 resolve 配置项的值。例如:{ extensions: [".sass", "..."] }

options.dependencyType 是一个额外的配置。它允许我们指定依赖类型,用于从 resolve 配置项中解析 byDependency

解析操作的所有依赖项都会自动作为依赖项添加到当前模块中。

this.hot

loaders 的 HMR(热模块替换)相关信息。

module.exports = function (source) {
  console.log(this.hot); // true if HMR is enabled via --hot flag or webpack configuration
  return source;
};

this.importModule

5.32.0+

this.importModule(request, options, [callback]): Promise

一种可以选择的轻量级解决方案,用于子编译器在构建时编译和执行请求。

  • request: 加载模块的请求字符串
  • options:
    • layer:指定该模块放置/编译的层
    • publicPath:用于构建模块的公共路径
  • callback:一个可选的 Node.js 风格的回调,返回模块的 exports 或 ESM 的命名空间对象。如果没有提供回调,importModule将返回一个 Promise。

webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /stylesheet\.js$/i,
        use: ['./a-pitching-loader.js'],
        type: 'asset/source', // 我们将 type 设置为 'asset/source',其会返回一个字符串。
      },
    ],
  },
};

a-pitching-loader.js

exports.pitch = async function (remaining) {
  const result = await this.importModule(
    this.resourcePath + '.webpack[javascript/auto]' + '!=!' + remaining
  );
  return result.default || result;
};

src/stylesheet.js

import { green, red } from './colors.js';
export default `body { background: ${red}; color: ${green}; }`;

src/colors.js

export const red = '#f00';
export const green = '#0f0';

src/index.js

import stylesheet from './stylesheet.js';
// stylesheet 在构建时会成为一个字符串:`body { background: #f00; color: #0f0; }`。

在上面的例子中你可能会注意到一些东西:

  1. 我们有一个 pitching loader
  2. 我们在 pitching loader 中使用 !=! 语法来为请求设置 matchResource,例如,我们将使用 this.resourcePath + '.webpack[javascript/auto]' 而不是原始资源匹配 module.rules
  3. .webpack[javascript/auto].webpack[type] 模式的伪拓展,当没有指定其他模块类型时,我们使用它指定一个默认 模块类型,它通常和 !=! 语法一起使用。

注意,上面的示例是一个简化的示例,你可以查看 webpack 仓库的完整示例

this.loaderIndex

当前 loader 在 loader 数组中的索引。

示例中:loader1 中得到:0,loader2 中得到:1

this.loadModule

loadModule(request: string, callback: function(err, source, sourceMap, module))

解析给定的 request 到模块,应用所有配置的 loader,并且在回调函数中传入生成的 source、sourceMap 和模块实例(通常是 NormalModule 的一个实例)。如果你需要获取其他模块的源代码来生成结果的话,你可以使用这个函数。

this.loadModule 在 loader 上下文中默认使用 CommonJS 来解析规则。用一个合适的 dependencyType 使用 this.getResolve。例如,在使用不同的语义之前使用 'esm''commonjs' 或者一个自定义的。

this.loaders

所有 loader 组成的数组。它在 pitch 阶段的时候是可以写入的。

loaders = [{request: string, path: string, query: string, module: function}]

在此示例中:

[
  {
    request: '/abc/loader1.js?xyz',
    path: '/abc/loader1.js',
    query: '?xyz',
    module: [Function],
  },
  {
    request: '/abc/node_modules/loader2/index.js',
    path: '/abc/node_modules/loader2/index.js',
    query: '',
    module: [Function],
  },
];

this.mode

当 webpack 运行时读取 mode 的值

可能的值为:'production', 'development', 'none'

this.query

  1. 如果这个 loader 配置了 options 对象的话,this 就指向这个对象。
  2. 如果 loader 中没有 options,而是以 query 字符串作为参数调用时,this.query 就是一个以 ? 开头的字符串。

this.request

被解析出来的 request 字符串。

在我们的示例中:'/abc/loader1.js?xyz!/abc/node_modules/loader2/index.js!/abc/resource.js?rrr'

this.resolve

resolve(context: string, request: string, callback: function(err, result: string))

像 require 表达式一样解析一个 request。

  • context 必须是一个目录的绝对路径。此目录用作解析的起始位置。
  • request 是要被解析的 request。通常情况下,像 ./relative 的相对请求或者像 module/path 的模块请求会被使用,但是像 /some/path 也有可能被当做 request。
  • callback 是一个给出解析路径的 Node.js 风格的回调函数。

解析操作的所有依赖项都会自动作为依赖项添加到当前模块中。

this.resource

request 中的资源部分,包括 query 参数。

示例中:'/abc/resource.js?rrr'

this.resourcePath

资源文件的路径。

在【示例](#example-for-the-loader-context)中:'/abc/resource.js'

this.resourceQuery

资源的 query 参数。

示例中:'?rrr'

this.rootContext

从 webpack 4 开始,原先的 this.options.context 被改为 this.rootContext

this.sourceMap

是否应该生成一个 source map。因为生成 source map 可能会非常耗时,你应该确认 source map 确实需要。

this.target

compilation 的目标。从配置选项中传递。

示例:'web', 'node'

this.utils

5.27.0+

可以访问 contextifyabsolutify 功能。

  • contextify: 返回一个新的请求字符串,尽可能避免使用绝对路径。
  • absolutify: 尽可能使用相对路径返回一个新的请求字符串。

my-sync-loader.js

module.exports = function (content) {
  this.utils.contextify(
    this.context,
    this.utils.absolutify(this.context, './index.js')
  );
  this.utils.absolutify(this.context, this.resourcePath);
  // …
  return content;
};

this.version

loader API 的版本号 目前是 2。这对于向后兼容性有一些用处。通过这个版本号,你可以自定义逻辑或者降级处理。

this.webpack

如果是由 webpack 编译的,这个布尔值会被设置为 true。

Webpack 特有属性

loader 接口提供所有模块的相关信息。然而,在极少数情况下,你可能需要访问 compiler api 本身。

因此,你应该把它们作为最后的手段。使用它们将降低 loader 的可移植性。

this._compilation

用于访问 webpack 的当前 Compilation 对象。

this._compiler

用于访问 webpack 的当前 Compiler 对象。

过时的上下文属性

由于我们计划将这些属性从上下文中移除,因此不鼓励使用这些属性。它们仍然列在这里,以备参考。

this.debug

一个布尔值,当处于 debug 模式时为 true。

this.inputValue

从上一个 loader 那里传递过来的值。如果你会以模块的方式处理输入参数,建议预先读入这个变量(为了性能因素)。

this.minimize

决定处理结果是否应该被压缩。

this.value

向下一个 loader 传值。如果你知道了作为模块执行后的结果,请在这里赋值(以元素数组的形式)。

this._module

一种 hack 写法。用于访问当前加载的 Module 对象。

错误报告

您可以通过以下方式从 loader 内部报告错误:

  • 使用 this.emitError. 将在不中断模块编译的情况下报告错误。
  • 使用 throw(或其他未捕获的意外异常)。loader 运行时引发错误将导致当前模块编译失败。
  • 使用 callback(异步模式)。向回调传递错误也会导致模块编译失败。

示例:

./src/index.js

require('./loader!./lib');

从 loader 当中抛出错误:

./src/loader.js

module.exports = function (source) {
  throw new Error('This is a Fatal Error!');
};

或者在异步模式下,传入一个错误给 callback:

./src/loader.js

module.exports = function (source) {
  const callback = this.async();
  //...
  callback(new Error('This is a Fatal Error!'), source);
};

这个模块将获取像下面的 bundle:

/***/ "./src/loader.js!./src/lib.js":
/*!************************************!*\
  !*** ./src/loader.js!./src/lib.js ***!
  \************************************/
/*! no static exports found */
/***/ (function(module, exports) {

throw new Error("Module build failed (from ./src/loader.js):\nError: This is a Fatal Error!\n    at Object.module.exports (/workspace/src/loader.js:3:9)");

/***/ })

然后构建输出结果将显示错误,与 this.emitError 相似:

ERROR in ./src/lib.js (./src/loader.js!./src/lib.js)
Module build failed (from ./src/loader.js):
Error: This is a Fatal Error!
    at Object.module.exports (/workspace/src/loader.js:2:9)
 @ ./src/index.js 1:0-25

如下所示,不仅有错误消息,还提供了有关所涉及的 loader 和模块的详细信息:

  • 模块路径:ERROR in ./src/lib.js
  • request 字符串:(./src/loader.js!./src/lib.js)
  • loader 路径:(from ./src/loader.js)
  • 调用路径:@ ./src/index.js 1:0-25

Inline matchResource

在 webpack v4 中引入了一种新的内联请求语法。前缀为 <match-resource>!=! 将为此请求设置 matchResource

matchResource 被设置时,它将会被用作匹配 module.rules 而不是源文件。如果需要对资源应用进一步的 loader,或者需要更改模块类型,这可能会很有用。 它也显示在统计数据中,用于匹配 Rule.issuertest in splitChunks

示例:

file.js

/* STYLE: body { background: red; } */
console.log('yep');

loader 可以将文件转换为以下文件,并使用 matchResource 应用用户指定的 CSS 处理规则:

file.js (transformed by loader)

import './file.js.css!=!extract-style-loader/getStyles!./file.js';
console.log('yep');

这将会向 extract-style-loader/getStyles!./file.js 中添加一个依赖,并将结果视为 file.js.css。因为 module.rules 有一条匹配 /\.css$/ 的规则,并且将会应用到依赖中。

这个 loader 就像是这样:

extract-style-loader/index.js

const getStylesLoader = require.resolve('./getStyle');

module.exports = function (source) {
  if (STYLES_REGEXP.test(source)) {
    source = source.replace(STYLES_REGEXP, '');
    return `import ${JSON.stringify(
      this.utils.contextify(this.context || this.rootContext, `${this.resource}.css!=!${getStylesLoader}!${this.remainingRequest}`)
    )};${source}`;
  }
  return source;
};

extract-style-loader/getStyles.js

module.exports = function (source) {
  const match = STYLES_REGEXP.match(source);
  return match[0];
};

Logging

自 webpack 4.37 发布以来,Logging API 就可用了。当 stats configuration 或者 infrastructure logging 中启用 logging 时,loader 可以记录消息,这些消息将以相应的日志格式(stats,infrastructure)打印出来。

  • Loaders 最好使用 this.getLogger() 进行日志记录,这是指向 compilation.getLogger() 具有 loader 路径和已处理的文件。这种日志记录被存储到 Stats 中并相应地格式化。它可以被 webpack 用户过滤和导出。
  • Loaders 可以使用 this.getLogger('name') 获取具有子名称的独立记录器。仍会添加 loader 路径和已处理的文件。
  • Loaders 可以使用特殊的回退逻辑来检测日志支持 this.getLogger() ? this.getLogger() : console。在使用不支持 getLogger 方法的旧 webpack 版本时提供回退方法。

2 位译者

12 位贡献者

TheLarkInnjhnnstbroadleybyzyksokraEugeneHlushkojantimonsuperburritowizardofhogwartssnitin315chenxsanjamesgeorge007