前言
使用过webpack的童鞋都应该知道loader这个概念,那么,不知道你有没有兴趣和我一起来了解他呢?
Emmm,那是什么?
对,你没看错,他来了,他来了,他正朝向我们走来,然后当面就给我们来了一个灵魂连环问。
Loader 是什么?有什么用?有什么特点?有哪些Loader ? 怎么用?怎么写一个自己的Loader ?
那今天,我们就一起来了解下吧。
版本说明
本文书写时,是针对Webpack 4.x,(5.x变动不大),望知晓。
Loader 是什么?
Loader 又称加载器, 它是类似其他构建工具中的 任务(task), 提供了处理前端构建步骤的强大方法。
Loader 有什么用?
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
本质上, 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';
上面这个语句在执行时会被转换成右边配置进行执行,注意:此处并不会改变预设配置,而是在执行时转换成右边配置。
通过前置所有规则及使用 !,可以对应覆盖到配置中的任意 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-loader 和 css-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 中的规范路径格式, 要尽量避免,因为
- 会在代码中耦合 webpack 的具体细节
- 可能会对 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
正常执行
在这个过程中如果任何 pitch 有返回值,则 loader 执行链被阻断。webpack 会跳过后面所有的的 pitch 和 loader,直接进入上一个loader 的 normal execution。
更多参考 pitching-loader 和 Rule.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 用法准则
参考资料
【webpack 进阶】你真的掌握了 loader 么?- loader 十问