目录

Webpack - 自定义 loader

loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用。给定的函数将调用loader API,并通过 this 上下文访问。

背景

webpack 自身只能解析 JS 和 JSON 文件,无法理解其它类型的的文件模块,当我们需要处理 CSS、图片等非 JS 资源时,需要使用 loader 将之转换为 webpack 核心能够理解的形式。loader 本质是一个函数,接收待处理的资源模块,输出符合 webpack 处理规范的的内容。

loader 使用

以简单的 css 模块处理为例,我们使用 style-loadercss-loader 这两个 loader,类似如下的 webpack 配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
module.exports = {
  //...
  module: {
    rules: [
      test: /\.css$/,
      // css-loader: 解析 CSS 文件,以字符串形式打包到 js 文件
      // style-loader: 把 js 里的样式代码插入到 html
      use: ['style-loader', 'css-loader'],
    ]
  }
  //...
};

loader 执行顺序是从后往前,先执行 css-loader,再将其执行结果交给 style-loader 处理。由此可以得到自定义 loader 时需要遵守的几个基本原则:

  • 单一职责:每个 loader 只做一个任务,不仅易维护,也可以在更多场景链式调用。
  • 链式传递:资源文件传入第一个 loader,后续 loader 接受上一个 loader 返回的处理结果。这意味着 loader 不一定要输出 js,只要下一个 loader 可以处理这个输出,这个 loader 就可以返回任意类型的模块。
  • 逆向调用:体现在上面的例子就是 use 配置项从右往左执行,这与 js 的函数式编程而非管道流相关。

最简单的自定义 loader

编写一个 loader,实现功能:替换所有 .txt 文件中以 [name] 标记的关键词。gitub 仓库
replace-keyword-loader.js:

1
2
3
4
5
6
7
8
9
// 利用 loader-utils 工具包获取传入的参数
const { getOptions } = require('loader-utils');

module.exports = function(source) {
  const options = getOptions(this);
  source = source.replace(/\[name\]/g, options.keyword);

  return `export default ${JSON.stringify(source)}`;
}

webpack.config.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const path = require('path');

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.txt$/,
        use: [
          {
            loader: path.resolve('./src/replace-keyword-loader.js'),
            options: {
              keyword: 'JiangBao'
            }
          }
        ]
      }
    ]
  },
};

raw 模式

之前的基础示例中,资源文件会默认转化为 UTF-8 字符串,然后传给 loader,但是在一些图片、音频、视频资源下,使用字符串就不合适了,这时候可以接收原文件的 Buffer,设置导出 raw 字段为 true 即可开启 buffer 类型的参数。
一个简单的例子:实现最简化的 url-loader 功能,将图片转为 base64 编码格式。github 仓库
raw-loader.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const { getOptions } = require('loader-utils');

module.exports = function(source) {
  const options = getOptions(this);
  const mimetype = options.mimetype || '';

  return `export default ${JSON.stringify(`data:${mimetype};base64,${source.toString('base64')}`)}`;
}

// 开启 buffer 类型文件格式
module.exports.raw = true;

webpack.config.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const path = require('path');

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.png$/,
        use: [
          {
            loader: path.resolve('./src/raw-loader.js'),
            options: {
              mimetype: 'image/png'
            }
          }
        ]
      }
    ]
  },
};

pitch

官网对 pitching loader 的介绍:loader总是从右到左地被调用,有些情况下,loader 只关心 request 后面的元数据(metadata),并且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 之前,会先从左到右调用 loader 上的 pitch 方法。如果某个 loader 在 pitch 方法中返回一个结果,那么这个过程就会跳过剩下的 loader。这个关系引用两张图可以看的很清晰

实例:实现一个简单的style-loadergithub 仓库
css-loader 会将css文件打包成字符串到js模块,我们自定义 style-loader 则需要通过操作 dom,将这份css代码转成 style 标签插入到 htmlhead 中。避免循环引用问题,需要在 pitch 方法上执行。
my-style-loader.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const { stringifyRequest } = require('loader-utils');

function loader(source) {};

loader.pitch = function(remainingRequest) {
  return `
    let style = document.createElement('style');
    const content = require(${stringifyRequest(this, '!!' + remainingRequest)});
    style.innerHTML = content.default;
    document.head.appendChild(style);
  `;
}

module.exports = loader;

webpack.config.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const path = require('path');

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          path.resolve('./src/my-style-loader.js'),
          'css-loader'
        ]
      }
    ]
  },
};

参考

编写一个 loader
揭秘 webpack loader