阅读(3114) (0)

Webpack:Loader Interface

2023-05-09 11:23:28 更新

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 的一些基本示例。注意,map 和 meta 参数是可选的,查看下面的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。通过设置 raw 为 true,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-loader 的 pitch 方法返回了一些东西:

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-loaderless-loader 就使用了这个技巧,当它发现无论何时导入的 css 文件发生变化时就会重新编译。

this.addMissingDependency

addMissingDependency(file: string)

添加一个不存在的文件作为 loader 结果的依赖项,以使它们可监听。类似于 addDependency,但是会在正确附加观察者之前处理在编译期间文件的创建。

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 忽略,可以是任何东西(例如一些元数据)。

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+

可以访问 contextify 与 absolutify 功能。

  • 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.issuer 和 test 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('./getStyles');

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 = source.match(STYLES_REGEXP);
  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 版本时提供回退方法。