loader 是什么
// css-loader/index.js
export default async function loader(content, map, meta) {
...
}
随便找个 loader 看看就能发现,loader 就是一个导出为的函数的模块。
可以接收 3 个参数
- content:资源文件或者上一个 loader 返回的结果
- map:可选,SourceMap 数据
- meta:可选,任意内容
返回值是 String 或者 Buffer(可以转换为 String)。
loader 的作用
loader 用于对模块的源代码进行转换。loader 可以使你在
import或 "load(加载)" 模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的得力方式。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中importCSS 文件!
从文档的介绍可以得知,loader 的作用是对文件进行预处理,因此 loader 会在真正编译之前被执行。
为什么需要 loader 呢?因为 webpack 编译器会将所有资源都视为 js 模块编译,所以 loader 负责将非 js 文件预处理为标准的 js 模块,才能交给编译器去编译。
loader 的执行流程
loader 的执行流程存在 normal 和 pitch 两个阶段。
一个 loader 可以分为两个部分,我们将 normal 阶段执行的部分称为 normal loader,pitch 阶段执行的部分称为 pitch loader。
- normal loader:模块导出的函数
- pitch loader:导出函数上的 pitch 属性,值为函数
// normal loader
function loader(content) { ... }
// pitch loader
loader.pitch = function pitchLoader() { ... }
export default loader
整个执行流程与浏览器的事件模型类似。首先从左到右执行 pitch lolader;读取资源文件;然后是再从右到左执行 normal loader。
来看看源码,执行流程是通过递归的方式实现的。
// pitch 阶段
function iteratePitchingLoaders(options, loaderContext, callback) {
// 所有 loader.pitch 都执行了则加载资源文件
if(loaderContext.loaderIndex >= loaderContext.loaders.length)
return processResource(options, loaderContext, callback);
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
...
// 加载 loader
loadLoader(currentLoaderObject, function(err) {
...
var fn = currentLoaderObject.pitch;
currentLoaderObject.pitchExecuted = true;
...
// 执行 loader.pitch,完成后递归执行下一个
runSyncOrAsync(
fn,
loaderContext, [...],
function(err) {
...
iteratePitchingLoaders(options, loaderContext, callback);
}
)
}
}
// 加载资源文件
function processResource(options, loaderContext, callback) {
// 读取资源文件
...
// 进入 normal 阶段
iterateNormalLoaders(options, loaderContext, [null],
}
// normal 阶段
function iterateNormalLoaders(options, loaderContext, args, callback) {
...
runSyncOrAsync(fn, loaderContext, args, function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
iterateNormalLoaders(options, loaderContext, args, callback);
});
}
loader 的执行顺序
除了在配置的时候定义 loader 的顺序,还有别的方式能够改变吗?当然是有的。
影响 loader 执行顺序的因素有 3 个:
- pitch loader 的返回值
- loader 的优先级
- inline loader 的前缀
接下来一个一个的分析,它们是如何影响的。
pitch loader 的返回值
正如前面提到的,pitch loader 是导出函数的 pitch 属性,值为函数,支持 3 个参数:
/**
* @remainingRequest 剩余需要处理的 loader 的绝对路径
* @precedingRequest pitch 阶段已经迭代过的 loader 的绝对路径
* @data 默认是空对象
*/
loader.pitch = function(remainingRequest, precedingRequest, data) {
...
};
其中 remainingRequest 和 precedingRequest 是以 ! 分割成的字符串,返回的 loader 与是否存在 pitch 无关。
normal loader 和 pitch loader 通过第三个参数 data 进行通行。
熔断效果
前文提到的执行流程中,pitch 阶段从左往右依次执行,这时候所有 pitch loader 的返回值都是 undefined。
如果其中一个 pitch loader 的返回值不是 undefined 时,就会发生熔断效果。
loader2.pitch 的返回值是非 undefined,就会跳过后面的流程,直接来到前一个 loader(loader1) 的 normal 阶段,返回值将作为参数传给 loader1,因此也不会去获取资源文件。
代码实现如下:
function iteratePitchingLoaders(options, loaderContext, callback) {
...
loadLoader(currentLoaderObject, function(err) {
...
var fn = currentLoaderObject.pitch;
runSyncOrAsync(
fn,
loaderContext, [...],
function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
var hasArg = args.some(function(value) {
return value !== undefined;
})
// 返回值为非 undefined
if(hasArg) {
// 指向前一个 loader
loaderContext.loaderIndex--;
// 进入 normal 阶段
iterateNormalLoaders(options, loaderContext, args, callback);
} else {
// 返回值为 undefined,继续执行下一个 pitch loader
iteratePitchingLoaders(options, loaderContext, callback);
}
}
)
}
}
pitch 的应用场景
为什么需要 pitch 呢?什么时候该使用它呢?
有些情况下,loader 只关心 request 后面的 元数据(metadata) ,并且忽略前一个 loader 的结果
文档给出的场景有点模糊。
常用的 loader 中,style-loader 就是一个典型的例子。查看它的源码就能发现,style-loader 的所有逻辑都在 pitch loader 中,normal loader 是个空函数。
const loaderAPI = () => {};
loaderAPI.pitch = function loader(request) { ... }
export default loaderAPI;
它做的事情很简单,就是将样式内容通过新建 style 标签插入到文档 head 中。
这些处理逻辑可以放到 normal loader 中吗?当然可以,那为什么不放在 normal loader 里呢?
将 style-loader 设计为 normal loader
style-loader 通常和 css-loader 搭配使用,将 style-loader 设计为 normal laoder,那么样式文件会先经过 css-loader 处理后,传递给 style-loader。
我们使用自己的 my-style-loader 代替 style-loader,看看 css-loader 处理后传递过来的是什么。
function loader(content) {
console.log('content', content)
return content
}
module.exports = loader
从截图可以看出,css-loader 返回的是一个 js 模块的字符串。
而 style-loader 负责的工作是将样式内容插入到文档中。要实现这个目的
- 需要先执行从 css-loader 传入的 js 模块,得到它导出的样式内容
- 创建 style 标签,包裹样式内容,插入到文档中
对于第一点,我们可以自己实现执行 js 模块的逻辑,但是 webpack 就具有这样的能力,为什么不利用它呢?
那么我们就要换一种设计方式。
将 style-loader 设计为 pitch loader
来模拟一下 style-loader 的实现方式。
在 pitch 中,返回值为处理逻辑的 js 模块:
- 通过 inline-laoder 的方式,导入 css-loader 处理后的样式内容
- 创建 style 标签,包裹样式内容,插入到文档中
loader.pitch = function(remainingRequest) {
return `
import style from '!!${remainingRequest}'
const styleEl = document.createElement('style')
styleEl.innerHTML = style
document.head.appendChild(styleEl)
`
}
因为 pitch 的返回值不是 undefined,会发生熔断,webpack 编译器得到的是 style-laoder pitch 阶段返回的 js 模块,开始对它进行编译。
在编译的过程中,如果发现有 import/require 这样的模块引入的语句,会递归的编译文件。
import style from '!!${remainingRequest}'
当执行到这句代码时,webpack 先编译执行 css-loader 返回的 js 模块,得到它导出的样式内容,再继续执行后面的语句。
当 loader 需要的是特定的资源文件,而不是 js 模版时,将逻辑设计在 pitch 阶段是个 很好的选择
loader 的优先级
在配置中可以通过 Rule.enforce 设置 loader 的优先级,优先级对 pitch 阶段和 normal 阶段都会有影响。
- pitch 阶段:post -> inline -> normal -> pre
- normal 阶段:pre -> normal -> inline -> post
inline loader 的前缀
通过导入的方式引用的 loader 为 inline loader,在导入时通过添加前缀的方式,可以禁用某些类型的 loader。
- 使用
!前缀,将禁用所有已配置的 normal loader(普通 loader) - 使用
!!前缀,将禁用所有已配置的 loader(preLoader, loader, postLoader) - 使用
-!前缀,将禁用所有已配置的 preLoader 和 loader,但是不禁用 postLoaders
结尾
虽然社区提供了很多的 loader 能够满足日常开发需求,需要自己写 loader 的情况很少,但是深入了解 loader,对于平时的项目配置还是有帮助的,能够明白每一个配置项的作用和目的。