Loader 是 Webpack 中非常重要的组成部分,前面章节中已经介绍了很多 loader 的用法。本章对 loader 的细节做一些详细说明。

借助于 loader-runner 来单独运行一个 loader

我们可以通过 loader-runneropen in new window 实现不借助于 Webpack 直接运行一个 loader。首先安装 loader-runner.

npm add loader-runner --develop
1

我们先写一个简单的 loader,这个 loader 将输入的文本重复一遍再输出。

loaders/demo-loader.js

module.exports = (input) => input + input;
1

我们在项目根目录下准备一个 demo.txt 文件,里面写入一些文本。

现在,我们来运行 loader-runner。

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))
);
1
2
3
4
5
6
7
8
9
10
11
12

此时,执行 node ./run-loader.js,可以看到输出内容为:

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

我们可以看到输出的文本内容和一些元数据信息。

如果我们的 loader 是安装在项目本地,我们可以直接通过 loader 的名字来查找 loader,loaders: ["babel-loader"]

实现一个异步 loader

上面的例子中,我们同步实现了一些功能,但是在某些情况下,我们需要进行一些异步处理,这时候,我们可以借助于 Webpack 提供的异步 API this.async()。函数返回一个回调函数,与 NodeJS 的回调函数签名一致。

loaders/demo-loader.js

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

  callback(null, input + input);
};
1
2
3
4
5

注意,我们使用的 this,因此无法使用箭头函数。

此时,执行 node ./run-loader.js,我们看到同样的输出内容。

如果我们需要抛出错误,则可以这么实现。

loaders/demo-loader.js

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

  callback(new Error('Demo error'));
};
1
2
3
4
5

此时,运行结果将会是一个错误信息。

只返回输出结果

我们可以在 loader 中只返回代码,比如

loaders/demo-loader.js

module.exports = () => 'foobar';
1

这种做法在需要动态生成代码的时候将会非常有用。

写文件

像 file-loader 之类的 loader 会输出文件。Webpack 提供了 this.emitFile API。因为 loader-runner 没有实现这个 API,因此我们需要 mock 掉。

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))
);
1
2
3
4
5
6
7
8
9
10
11

作为一个处理资源的 loader,核心任务就是输出资源内容和文件路径。

我们可以借助于 loader-utilsopen in new window 来对文件名做一些定制处理。

npm add loader-utils --develop
1

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}`;
};
1
2
3
4
5
6
7
8
9
10
11
12
13

Webpack 还提供了 this.emitWarning(<string>)this.emitError(<string>) 方法,我们同样需要在 loader-runder 中 mock。

loader-utils 包含了许多实用方法。比如 loaderUtils.parseQuery(this.resourceQuery) 可以处理传递给 loader 的查询参数。

下一步就是如何将文件名传递给下一个 loader。

给 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))
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

在 demo loader 中可以使用这个 name 参数。

loaders/demo-loader.js

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

module.exports = function(content) {

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

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

  ...
};
1
2
3
4
5
6
7
8
9
10
11
12
13

执行 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: [],
  missingDependencies: []
}
1
2
3
4
5
6
7
8

我们可以在使用参数之前对参数进行一些校验,这是可以使用 schema-utilsopen in new window

在 Webpack 中使用自定义 loader

src/component.js

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

上面的使用方式非常繁琐,我们可以在 webpack 的配置文件中给 loader 定义一个别名,

webpack.config.js

const commonConfig = merge([
  {
    resolveLoader: {
      alias: {
        "demo-loader": path.resolve(
          __dirname,
          "loaders/demo-loader.js"
        ),
      },
    },
  },
  ...
]);
1
2
3
4
5
6
7
8
9
10
11
12
13

这时候,我们可以这样引用这个 loader

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

我们也可以通过 Webpack 的rule 来引用 loader。

loader 的执行过程

23ffe365f20592287b5b0ef2ef282684.png webpack 中 loader 的执行过程。

像浏览器中的事件一样,Webpack 中 loader 的执行过程也分两个阶段。我们可以在 pitch 的过程中做一些定制处理逻辑。Webpack 会从左到右解析 loader,然后从右到左执行。

我们可以定义一个 pitch-loader 来定制 loader 的解析过程,我们设置可以终止 loader 的解析。

loaders/pitch-loader.js

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

module.exports = function (input) {
  return input + loaderUtils.getOptions(this).text;
};
module.exports.pitch = function (remaining, preceding, input) {
  console.log(`Remaining: ${remaining}, preceding: ${preceding}
Input: ${JSON.stringify(input, null, 2)}
  `);

  return 'pitched';
};
1
2
3
4
5
6
7
8
9
10
11
12

每个 loader 都可以提供一个 pitch 函数给 Webpack 调用,在 pitch 函数里做一些定制处理。

修改 run-loader,如下

run-loader.js

runLoaders(
  {
    resource: "./demo.txt",
    loaders: [
      ...
      path.resolve(__dirname, "./loaders/pitch-loader"),
    ],
    ...
  },
  (err, result) => (err ? console.error(err) : console.log(result))
);
1
2
3
4
5
6
7
8
9
10
11

执行 node ./run-loader.js 可以看到输出的一些定制处理信息。

在 loader 中使用缓存

默认情况下 Webpack 都会开启 loader 的缓存,除非我们通过 this.cacheable(false) 来禁用。我们可以通过下面的代码来了解 loader 的缓存是如何工作的。

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) {
      item.push(this.async()); // 加载到缓存中
    } else {
      return item; // 命中缓存
    }
  } else {
    cache.set(this.resourcePath, []); // 未命中缓存
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

pitch loader 可以被用来添加一些额外的元数据信息。本例中,在 pitch 阶段,构建缓存,然后在执行阶段使用缓存。

总结

loader-runner 可以帮助我们理解 loader 是如何工作的。

在 Webpack 中,loader 既可以是同步的,也可以是异步的。Webpack 提供了 this.async() 来实现异步 loader。

我们可以通过 loader 实现动态生成代码。

可以使用 loader-utils 来处理参数,同时使用 schema-utils 来对参数进行校验。

在本地开发的时候,我们可以借助于 resolveLoader.alias 的形式避免不必要的路径引用。

loader 的 pitch 阶段可以方便我们添加一些元数据和做 loader 的解决做一些定制化操作。

关注微信公众号,获取最新推送~

加微信,深入交流~