那些有关于Loader的知识

1,874 阅读7分钟

前言

使用过webpack的童鞋都应该知道loader这个概念,那么,不知道你有没有兴趣和我一起来了解他呢?

Emmm,那是什么?

对,你没看错,他来了,他来了,他正朝向我们走来,然后当面就给我们来了一个灵魂连环问。

Loader 是什么?有什么用?有什么特点?有哪些Loader ? 怎么用?怎么写一个自己的Loader ?

那今天,我们就一起来了解下吧。

版本说明

本文书写时,是针对Webpack 4.x,(5.x变动不大),望知晓。

Loader 是什么?

Loader 又称加载器, 它是类似其他构建工具中的 任务(task), 提供了处理前端构建步骤的强大方法。

Loader 有什么用?

loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。

img

本质上, webpack loader 将所有类型的文件转换为应用程序的 依赖图 (dependency graph) 然后变成可以直接引用的模块。

Loader 特性

  • loader 支持链式传递,能够对资源使用流水线(pipeline)。
    • 一组链式的 loader 将按照相反的顺序执行,loader 链中的第一个 loader 返回值给下一个 loader,在最后一个 loader,返回 webpack 所预期的 JavaScript。
    • 当链式调用多个 loader 的时候,请记住它们会以相反的顺序执行。取决于数组写法格式,从右向左或者从下向上执行。
  • loader 可以是同步的,也可以是异步的。
  • loader 运行在 Node.js 中,并且能够执行任何可能的操作。
  • loader 接收查询参数。用于对 loader 传递配置。
  • loader 也能够使用 options 对象进行配置。
  • 除了使用 package.json 常见的 main 属性,还可以将普通的 npm 模块导出为 loader,做法是在 package.json 里定义一个 loader 字段。
  • 插件(plugin)可以为 loader 带来更多特性。
    • Note: plugin 是一个扩展器,它丰富了webpack本身的能力。由于本文并不是讲解 plugin,所以此处不对plugin进行分析,更多请移步 rain120.github.io/study-notes…
  • loader 能够产生额外的任意文件。

Loader 配置

配置

webpack.config.js中 loader的配置如下

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader',
          },
          {
            loader: 'css-loader',
          },
        ],
      },
    ],
  },
};

内联配置 Loader

import '!style-loader!css-loader!less-loader?name=Rain120!./styles.less';

上面内联引入模块相当于如下配置 (内部执行转换过的rule配置):

module.exports = {
  // ...
  module: {
    // ...
    rules: [
      {
        test: /\.less$/,
        use: [
          {
            loader: 'style-loader',
            options: {},
          },
          {
            loader: 'css-loader',
            options: {},
          },
          {
            loader: 'less-loader'
          },
        ],
      },
    ],
  },
  // ...
};

再如:

import '-!my-loader!my-loader2!./styles.css';

上面这个语句在执行时会被转换成右边配置进行执行,注意:此处并不会改变预设配置,而是在执行时转换成右边配置。

img

通过前置所有规则及使用 !,可以对应覆盖到配置中的任意 loader, 更多参数请跳到下面👇🏻 Loader 匹配规则 查看。

Loader可以通过 options 传递查询参数,例如

{
    // ...
    loader: 'less-loader',
    // options: '?name=Rain120&age=18',
    options: {
        name: 'Rain120',
        age: 18
    }
}

Cli 配置 Loader

也可以通过 CLI 使用 loader

webpack --module-bind jade-loader --module-bind 'css=style-loader!css-loader'

这会对 .jade 文件使用 jade-loader,对 .css 文件使用 style-loadercss-loader

更多参考 webpack 使用 Loader

Loader 种类

关于 loader的种类, 可以通过 enforce 来配置,如下

module.exports = {
  // ...
	module: {
    // ...
    // 从下往上, css-loader -> style-loader
    rules: [
      {
        test: /\.css$/,
        use: {
          loader: 'style-loader'
        },
        enforce:'pre'
      },
      {
        test: /\.css$/,
        use: {
          loader: 'css-loader'
        }
      }
    ]
  },
  // ...
}

此时,在普通 loader 模式下 css-loader 将会在style-loader之后执行。即由之前的 css-loader -> style-loader 变成 style-loader -> css-loader

rule.enforce的参数: pre, post

  • pre Loader: 前置 loader
    • 配置: enforce: 'pre'
  • normal Loader: 普通 loader
    • 配置: 默认
  • inline Loader: 内联loader
    • 在模块中指定使用的 loader 是内联loader,例如 import '!style-loader!css-loader!less-loader?name=Rain120!./styles.less';
  • post Loader: 后置loader
    • 配置: enforce: 'post'

Loader 匹配规则

当然,webpack可以通过引入模块的路径规则,来判断是否使用内联模式或者剔除一些前置(pre) Loader, 后置(post) , 普通(normal) Loader。

Note:

这种内联模式, 并非 ES module 中的规范路径格式, 要尽量避免,因为

  1. 会在代码中耦合 webpack 的具体细节
  2. 可能会对 IDE 的路径解析产生干扰

规则如下:

-! : 剔除 配置中符合条件的 pre 和 normal Loader

! : 剔除 配置中符合条件的 normal Loader

!! : 剔除 配置中符合条件的 pre & normal & post Loader

// Disable normal loaders
import { a } from '!./file1.js';

// Disable preloaders and normal loaders
import { b } from '-!./file2.js';

// Disable all loaders
import { c } from '!!./file3.js';

webpack代码逻辑解析规则如下(5.0.0.beta.15 vs 4.43.0)

// ...
const firstChar = requestWithoutMatchResource.charCodeAt(0);
const secondChar = requestWithoutMatchResource.charCodeAt(1);
// 注意👇🏻👇🏻👇🏻: 旧版本是通过 Char Code 判断的是否是特殊标记📌
const noPreAutoLoaders = firstChar === 45 && secondChar === 33; // startsWith "-!"
const noAutoLoaders = noPreAutoLoaders || firstChar === 33; // startsWith "!"
const noPrePostAutoLoaders = firstChar === 33 && secondChar === 33; // startsWith "!!";
const rawElements = requestWithoutMatchResource
  .slice(noPreAutoLoaders || noPrePostAutoLoaders ? 2 : noAutoLoaders ? 1 : 0)
  .split(/!+/);
// ...

详见 5.0.0 beta.15 webpack NormalModuleFactory.js

// ...
// 注意👇🏻👇🏻👇🏻: 新版本通过判断开头是否是特殊标记📌
const noPreAutoLoaders = requestWithoutMatchResource.startsWith('-!');
const noAutoLoaders =
  noPreAutoLoaders || requestWithoutMatchResource.startsWith('!');
const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith('!!');
let elements = requestWithoutMatchResource
  .replace(/^-?!+/, '')
  .replace(/!!+/g, '!')
  .split('!');
let resource = elements.pop();
elements = elements.map(identToLoaderRequest);
// ...

详见 4.43.0 webpack NormalModuleFactory.js

Loader 执行

前置知识

什么是pitch

Webpack 允许在 loader 函数上挂载一个名为 pitch 的函数,运行时 pitch 会比 Loader 本身更早执行。它可以阻断 loader 链。

function pitch(
  // 当前 loader 之后的资源请求字符串
  // 以 ! 分割组成的字符串
  remainingRequest: string, 
  // 在执行当前 loader 之前经历过的 loader 列表
  // 已经迭代过(pitch)的 loader 以 ! 分割组成的字符串
  previousRequest: string,
  // 与 Loader 函数的 data 相同,用于传递需要在 Loader 传播的信息
  // 可以在执行 loaderA 时或者 loaderA.pitch 传递的参数
  data = {}
): void {
  // balabala ...
}

举个🌰

module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/i,
        use: [
          "style-loader", "css-loader", "less-loader"
        ],
      },
    ],
  },
};

当执行到 css-loader.pitch 时,

// css-loader 之后的 loader 列表及资源路径
remainingRequest = less-loader!./xxx.less
// css-loader 之前的 loader 列表
previousRequest = style-loader
// 默认值
data = {}

Loader 链式执行

Loader 的执行顺序遵循后进先出(Last In First Out)。

module.exports = {
  // ...
  module: {
    // ...
    rules: [
      {
        test: /\.css$/,
        // 执行顺序, css-loader -> style-loader
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

或者你是这样配置的 👇🏻

module.exports = {
  // ...
  module: {
    // ...
    // 执行顺序, css-loader -> style-loader
    rules: [
      {
        test: /\.css$/,
        use: {
          loader: 'style-loader'
        }
      },
      {
        test: /\.css$/,
        use: {
          loader: 'css-loader'
        }
      }
    ]
  },
  // ...
}

每个loader默认的执行阶段(normal execution)的执行顺序是从 pre --> normal --> inline --> post, 即,从后往前执行; 某些情况下,loader 只关心 request 后面的元数据(metadata),并且忽略前一个 loader 的结果。 在实际(从右到左)执行 loader 之前,会先从左到右调用 loader 上的 pitch 方法,pitch 阶段的执行顺序是 post --> inline --> normal --> pre。对于以下 use 配置:

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

pitch 和normal execution执行结果如下

|- 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

正常执行

img

在这个过程中如果任何 pitch 有返回值,则 loader 执行链被阻断。webpack 会跳过后面所有的的 pitch 和 loader,直接进入上一个loader 的 normal execution。

img

更多参考 pitching-loaderRule.enforce

Loader 的实现

注意: 这里并不是教你如何实现一个 Loader,我们只讨论实现原理和官方的编写原则,只要你遵守,肯定可以实现一个很 Nice 的 loader,trust yourself。

我们知道 loader 是导出为一个函数的 node 模块,该函数在 loader 转换资源的时候调用。

实现原理

给定的函数将调用 loader API,并通过 this 上下文访问。

// somepath/loader.js
export default function loader(source) {
  const options = this.getOptions();

  console.log('This loader options', JSON.stringify(options);

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

然后修改配置文件

// webpack.config.js
// ...
module: {
    rules: [
        {
        test: /\.txt$/,
        use: {
            loader: path.resolve(__dirname, '../somepath/loader.js'),
            options: {
                name: 'Rain120'
            },
        },
        },
    ],
},
// ...

That's all.

更多参考 Webpack Writing a Loader

编写原则

看着写一个 loader 很简单,但是,希望你在实现的时候遵循下面的规则,可以避免一些问题。

  • 单一原则: 每个 Loader 只做一件事;
  • 链式调用: Webpack 会按顺序链式调用每个 Loader;
  • 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;

更多参考 Webpack 用法准则

参考资料

那些有关于Loader的知识

Webpack Loader 官方文档

Loaders Api

【webpack 进阶】你真的掌握了 loader 么?- loader 十问

webpack 系列之四 loader 详解 1

往期文章列表

二维码系列 -- 二维码原理(二)

二维码系列 -- 常见的"码"(一)

Linux中的Link及Node中几种常见的软链接应用

Alpha混色模式合成算法