扩展 loader

正如您所看到的那样,loader 是 Webpack 构建中的一部分。如果要加载资源,则很可能需要配置一个匹配的 loader。尽管有很多可用的 loader,但可能没有一个能够满足你的需要。

接下来你将学会开发几个小型 loader。但在此之前,了解如何单独调试它们是很好的。

如果您想要一个独立的 loader 或插件项目作为一个好的开始,请考虑使用 webpack-defaults。它提供了一个基本的起点,包括 linting、测试和其他好的工具。

使用 loader-runner 调试 loader

loader-runner 允许您在 Webpack 之外运行 loader,让您可以了解有关 loader 开发的更多信息。首先安装它:

npm install loader-runner --save-dev

首先写一个例子进行测试,这个例子直接将输入叠加并返回:

loaders/demo-loader.js

module.exports = input => input + input;

设置要处理的文件:

demo.txt

foobar

代码中没有特定的与 Webpack 相关的东西。下一步是通过 loader-runner 运行 loader:

run-loader.js

const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");

runLoaders(
  {
    resource: "./demo.txt",
    loaders: [path.resolve(__dirname, "./loaders/demo-loader")],
    readResource: fs.readFile.bind(fs),
  },
  (err, result) => (err ? console.error(err) : console.log(result))
);

如果现在运行脚本(node run-loader.js),你应该看到以下输出:

{ 
  result: [ 'foobar\nfoobar\n' ],
  resourceBuffer: <Buffer 66 6f 6f 62 61 72 0a>,
  cacheable: true,
  fileDependencies: [ './demo.txt' ],
  contextDependencies: [] 
}

通过输出信息,我们可以看到处理结果,该资源被当做 buffer 处理,另外还可以看到其他元信息。这些数据足以开发更复杂的 loader 了。

如果要将输出信息保存到文件,请使用 Node 文档中讨论的 fs.writeFileSync("./output.txt", result.result) 或其异步版本。

对于安装到本地项目中的 loader,我们可以直接通过其名称进行引用而不是解析它们的完整路径。例如:loaders: ["raw-loader"]

实现异步 loader

尽管您可以使用同步接口实现许多 loader,但有时需要进行异步计算。另外,在将第三方软件包包装为 loader 时,还必须采用异步方式。

上面的示例可以通过使用 Webpack 特定的 API this.async() 转换成异步形式。Webpack 会设置 this,并且该函数遵循 Node 约定(错误优先,然是是结果)返回一个回调函数。

调整如下:

loaders/demo-loader.js

module.exports = function(input) {
  const callback = this.async();

  // callback 不存在 -> 返回同步结果
  // if (callback) { ... }

  callback(null, input + input);
};

鉴于 Webpack 通过 this 注入其API,所以此处不能使用箭头函数(() => ...)。

如果要将源映射传递给 Webpack,请将其作为回调的第三个参数。

再次运行演示脚本(node run-loader.js)应该得到与以前相同的结果。要在执行期间抛出错误,请尝试以下操作:

loaders/demo-loader.js

module.exports = function(input) {
  const callback = this.async();

  callback(new Error("Demo error"));
};

结果应该包含 Error: Demo error,以及与之相关的堆栈跟踪信息,显示错误发生的位置。

仅返回输出

loader 也可用于只返回输出。代码实现如下:

loaders/demo-loader.js

module.exports = function() {
  return "foobar";
};

但重点是什么呢?您可以通过 Webpack 入口传递一些东西给 loader。大多数情况下,入口都是指向一些已经存在的文件,而这种方式使您可以动态生成代码再传递给 loader。

如果要返回 Buffer,请设置 module.exports.raw = true。该标志将覆盖期望返回字符串的默认行为。

写文件

有一些 loader,如 file-loader,用来发出文件。Webpack 为此提供了一个单独的方法 this.emitFile。鉴于 loader-runner 中没有实现它,你必须模拟一个:

run-loader.js

...

runLoaders(
  {
    resource: "./demo.txt",
    loaders: [path.resolve(__dirname, "./loaders/demo-loader")],

    context: {
      emitFile: () => {},
    },

    readResource: fs.readFile.bind(fs),
  },
  (err, result) => (err ? console.error(err) : console.log(result))
);

要完全实现 file-loader,您必须做两件事:发出文件并返回它的路径。你可以按如下方式来实现:

loaders/demo-loader.js

const loaderUtils = require("loader-utils");

module.exports = function(content) {
  const url = loaderUtils.interpolateName(this, "[hash].[ext]", {
    content,
  });

  this.emitFile(url, content);

  const path = `__webpack_public_path__ + ${JSON.stringify(url)};`;

  return `export default ${path}`;
};

Webpack 提供了两种额外的 emit 方法:

  • this.emitWarning(<string>)
  • this.emitError(<string>)

我们也可以使用 console 中的相关方法来替代它们。另外,对于 this.emitFile,你必须模拟它才能让 loader-runner 工作。

接下来的问题是,如何将文件名传递给加载器。

将选项传递给 loader

为了更方便地演示如何获取传入的选项值,run-loader 需要进行一些小的调整:

run-loader.js

const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");

runLoaders(
  {
    resource: "./demo.txt",

    // loaders: [path.resolve(__dirname, "./loaders/demo-loader")],

    loaders: [
      {
        loader: path.resolve(__dirname, "./loaders/demo-loader"),
        options: {
          name: "demo.[ext]",
        },
      },
    ],

    context: {
      emitFile: () => {},
    },
    readResource: fs.readFile.bind(fs),
  },
  (err, result) => (err ? console.error(err) : console.log(result))
);

要捕获选项,您需要使用 loader-utils。它用来解析 loader 选项和查询参数。安装它:

npm install loader-utils --save-dev

把它引入到 loader,将其设置为捕获 name 并把 name 传递给 webpack 的插补器:

loaders/demo-loader.js

const loaderUtils = require("loader-utils");

module.exports = function(content) {

  const { name } = loaderUtils.getOptions(this);

  // const url = loaderUtils.interpolateName(this, "[hash].[ext]", {
  //   content,
  // });

  const url = loaderUtils.interpolateName(this, name, { content });

  ...
};

运行(node ./run-loader.js)之后,你应该看到一些东西:

{ 
  result: [ 'export default __webpack_public_path__+"demo.txt";' ],
  resourceBuffer: <Buffer 66 6f 6f 62 61 72 0a>,
  cacheable: true,
  fileDependencies: [ './demo.txt' ],
  contextDependencies: [] 
}

您可以看到结果与 loader 应返回的内容相匹配。您可以尝试将更多选项传递给 loader 或使用查询参数来查看不同组合会发生什么变化。

如果选项不是您所期望的,那么提前验证选项是一个好主意,而不是默默地直接失败。schema-utils 是为此目的而设计的。

将自定义 loader 引入 Webpack

要充分利用 loader,必须将它们与 Webpack 连接起来。要实现这一目标,您可以通过导入:

src/component.js

import "!../loaders/demo-loader?name=foo!./main.css";

鉴于定义冗长,loader 还可以使用别名:

webpack.config.js

const commonConfig = merge([
  {
  ...

    resolveLoader: {
      alias: {
        "demo-loader": path.resolve(
          __dirname,
          "loaders/demo-loader.js"
        ),
      },
    },

  },
  ...
]);

通过此更改,可以简化导入:

// import "!../loaders/demo-loader?name=foo!./main.css";

import "!demo-loader?name=foo!./main.css";

您还可以通过 rules 来处理 loader 的定义。一旦 loader 足够稳定,就可以建立一个基于 webpack-defaults 的项目,将 loader 代码推送到那里,并开始将 loader 作为包使用。

虽然使用 loader-runner 可以方便地开发和测试 loader,但是针对 Webpack 运行的集成测试还是必要的。环境之间的微妙差异使这一点至关重要。

loader 抛掷

loader,Webpack loader 处理过程

Webpack 分两个阶段处理 loader:抛掷和执行。如果您了解 Web 事件,则可以将 loader 的处理过程比作事件的捕获和冒泡。其核心思想是 Webpack 允许您在抛掷(捕获)阶段拦截执行。Webpack 首先从左到右穿过 loader,然后从右到左执行。

我们可以调整一个正在抛掷的 loader 的请求信息,甚至终止它。按照如下设置:

loaders/pitch-loader.js

const loaderUtils = require("loader-utils");

module.exports = function(input) {
  const { text } = loaderUtils.getOptions(this);

  return input + text;
};
module.exports.pitch = function(remainingReq, precedingReq, input) {
  console.log(`
    Remaining request: ${remainingReq}
    Preceding request: ${precedingReq}
    Input: ${JSON.stringify(input, null, 2)}
  `);

  return "pitched";
};

要把它引入到 runner,请将其添加到 loader 程序的定义中:

run-loader.js

runLoaders(
  {
    resource: "./demo.txt",
    loaders: [
      ...

      path.resolve(__dirname, "./loaders/pitch-loader"),

    ],
    ...
  },
  (err, result) => (err ? console.error(err) : console.log(result))
);

如果你现在运行(node ./run-loader.js),pitch-loader 应记录中间数据并拦截执行:

Remaining request: ./demo.txt
Preceding request: .../webpack-demo/loaders/demo-loader?{"name":"demo.[ext]"}
Input: {}

{ 
  result: [ 'export default __webpack_public_path__ + "demo.txt";' ],
  resourceBuffer: null,
  cacheable: true,
  fileDependencies: [],
  contextDependencies: [] 
}

loader 缓存

虽然 Webpack 默认缓存加载器,除非设置 this.cacheable(false),但编写缓存 loader 可能是一个很好的练习,因为它可以帮助您了解 loader 是如何协同工作的。以下示例显示了如何实现这一目标(由 Vladimir Grenaderov 提供):

const cache = new Map();

module.exports = function(content) {
  // Calls only once for given resourcePath
  const callbacks = cache.get(this.resourcePath);
  callbacks.forEach(callback => callback(null, content));

  cache.set(this.resourcePath, content);

  return content;
};
module.exports.pitch = function() {
  if (cache.has(this.resourcePath)) {
    const item = cache.get(this.resourcePath);

    if (item instanceof Array) {
      // Load to cache
      item.push(this.async());
    } else {
      // Hit cache
      return item;
    }
  } else {
    // Missed cache
    cache.set(this.resourcePath, []);
  }
};

可以使用 pitch-loader 将元数据附加到输入以供稍后使用。在该示例中,在抛掷阶段期间构建了高速缓存,并且在正常执行期间访问了高速缓存。

官方文档详细地介绍了 loader API。您可以看到 this 中所有可用的字段。

总结

编写 loader 很有趣,因为它们描述了从一种格式到另一种格式的转换。通常,您可以通过研究 API 文档或现有的 loader 来弄清楚如何实现特定的功能。

回顾一下:

  • loader-runner 是了解 loader 如何工作的宝贵工具,可以用它来调试 loader。
  • Webpack loader 接受输入并基于它生成输出。
  • loader 可以是同步的也可以是异步的。在后一种情况下,您应该使用 Webpack this.async() API来捕获 Webpack 暴露的回调函数。
  • 如果你想为 Webpack 入口动态生成代码,那么 loader 可以派上用场。在这种情况下,loader 可以不必接收输入,它可以只返回输出。
  • 使用 loader-utils 解析传递给 loader 的选项,并考虑使用 schema-utils 验证它们。
  • 在本地开发 loader 时,请考虑设置 resolveLoader.alias 来重命名引用。
  • loader 抛掷阶段补充了默认行为,允许您拦截和附加元数据。

您将在下一章学习编写插件。插件允许您拦截 webpack的执行过程,并且它们可以与加载器组合以开发更高级的功能。

用户头像
登录后发表评论