✏️ loader知识分享
什么是loader
webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。loader 本质上是导出为函数的 JavaScript 模块。—— webpack官方中文文档
如下图所示,在webpack使用过程中,经常会出现以下两种形式,前者更多是我们在webpack配置文件中,去根据文件匹配信息,去配置loader相关信息;后者更多是在loader / plugin中去修改/替换/生成的行内loader信息。这就涉及到loader的相关分类。
// webpack.config.js
{
module: {
rules: [
{
test: /.txt$/,
use: [
{
loader: getLoader("a-loader.js"),
},
],
enforce: "pre",
},
{
test: /.txt$/,
use: [
{
loader: getLoader("b-loader.js"),
}
],
enforce: "post",
},
],
},
}
// app.js
import "/Users/jiangyuereee/Desktop/loader/d-loader.js!./txt.txt"
loader的分类
在webpack里,loader可以被分为四类,分别是:后置post
,普通normal
,行内inline
,前置pre
。
enforce
对于post
,normal
,pre
,主要取决于在配置里Rule.enforce
的取值:pre
|| post
,若无设置,则为normal
。
注意:相对于的是Rule,并非某个loader。那么作用于的就是对应Rule的所有loader。
inline
行内loader比较特殊,是在import / require
的时候,将loader写入代码中。而对于inline
而言,有三种前缀语法:
!
:忽略normal
loader
-
-!
:忽略pre
loader和normal
loader -
!!
:忽略所有loader(pre
/noraml
/post
)
行内loader通过!
将资源中的loader进行分割,同时支持在loader后面,通过?
传递参数,参数信息参考loader.options内容。
而以上说的三种前缀语法,则是写在内联loader字符串的前缀上,来表示忽略哪些配置loader
example
以a-loader
为pre loader
,b-loader
为normal loader
,c-loader
为post loader
为例。
补充:本文的loader均为
module.exports = function (content) {
console.log("x-loader");
return content;
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
console.log("x-loader-pitch");
};
- 无前缀信息
import "/Users/jiangyuereee/Desktop/loader/d-loader.js!./txt.txt"
c-loader-pitch
d-loader-pitch
b-loader-pitch
a-loader-pitch
a-loader
b-loader
d-loader
c-loader
- !前缀信息
import "!/Users/jiangyuereee/Desktop/loader/d-loader.js!./txt.txt";
c-loader-pitch
d-loader-pitch
a-loader-pitch
a-loader
d-loader
c-loader
- -!前缀信息
import "-!/Users/jiangyuereee/Desktop/loader/d-loader.js!./txt.txt";
c-loader-pitch
d-loader-pitch
d-loader
c-loader
- !!前缀信息
import "!!/Users/jiangyuereee/Desktop/loader/d-loader.js!./txt.txt";
d-loader-pitch
d-loader
loader的优先级
四种loader调用先后顺序为:pre
> normal
> inline
> post
。
在相同种类loader的情况下,调用的优先级为,自下而上,自右向左。(pitch情况下,则反过来)。
例如:
{
module: {
rules: [
{
test: /.txt$/,
use: [
{
loader: getLoader("a-loader.js"),
},
],
enforce: "post",
},
{
test: /.txt$/,
use: [
{
loader: getLoader("b-loader.js"),
},
{
loader: getLoader("c-loader.js"),
},
],
enforce: "post",
},
],
},
}
a-loader-pitch
b-loader-pitch
c-loader-pitch
c-loader
b-loader
a-loader
Loader调用链
每个loader都有自己的normal函数和pitch函数,而调用过程则是先根据从低到高的优先级顺序,调用loader各自的pitch函数,再由高到低调用各自的normal函数,其过程,更像是一个洋葱模型。
Loader - pitch
loader 总是 从右到左被调用。有些情况下,loader 只关心 request 后面的 元数据(metadata),并且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 之前,会先 从左到右 调用 loader 上的 pitch 方法。—— webpack官方中文文档
对于一个loader,除了通过module.exports
导出处理函数外,还可以通过module.exports.pitch
导出pitch
方法。正常来说,在loader
从右向左调用之前,会进行一次从左到右的pitch
方法调用,而在pitch
调用过程中,如果任何一个有返回值,那么将阻断后续的loader
调用链,进而将自身的返回结果传递给上一个loader
作为content
。
补充:pitch
方法同样有同步 / 异步之分,同样可以选择return
或者this.callback
去传递更多的信息,后续会讲到。
loader的调用过程:
而一旦其中的一个pitch
返回了结果,那么将跳过后续的loader
,将返回结果传递给前一个loader
。
像常见的style-loader / vue-loader等等,就会利用pitch阶段进行拦截处理工作,从而实现loader特色化工作。
同步 / 异步loader
如果是单个处理结果,可以在 同步模式 中直接返回。如果有多个处理结果,则必须调用
this.callback()
。在 异步模式 中,必须调用this.async()
来告知 loader runner 等待异步结果,它会返回this.callback()
回调函数。随后 loader 必须返回undefined
并且调用该回调函数。—— webpack官方中文文档
在webpack中,loader可能会由于依赖于读取外部配置文件、进行网络请求等等原因,从而比较耗时。而此时如果进行同步操作,就会导致webpack阻塞住,所以loader会有同步 / 异步之分。
在loader中,可以通过两种方式返回数据:
return
:return
只能返回content
信息;
callback
:
this.callback(
err: Error | null, // 错误信息
content: string | Buffer, // content信息
sourceMap?: SourceMap, // sourceMap
meta?: any // 会被 webpack 忽略,可以是任何东西(例如:AST、一些元数据啥的)。
);
同步loader
对于同步loader而言,使用return
或者this.callback
均可以达到想要的效果。只是说,相对于return
,this.callback
可以返回更多的信息。
module.exports = function(content, map, meta) {
// return handleData(content);
this.callback(null, handleData(content), handleSourceMap(map), meta);
return; // 当调用 callback() 函数时,总是返回 undefined
};
异步loader
对于异步loader而言,需要通过this.async()
,来获取到callback
函数。
module.exports = function(content, map, meta) {
var callback = this.async();
asycnHandleData(content, function(err, result) {
if (err) return callback(err);
callback(null, result, map, meta);
});
};
loader的参数
一般来说,起始loader只有一个入参:资源文件的内容。默认情况下,资源文件的内容会以 UTF-8 字符串传递给loader。而在有需要的情况下,loader可以通过设置module.exports.raw = true
;来表示,需要接受一个Buffer
。同时,每个loader都可以传递String || Buffer
,complier会将其在loader
之间根据raw
的值来进行转换。(例如:图片文件等)
而根据callback
的参数也可以知道,除了起始loader之外,loader可以接受三个参数:content
、sourceMap
、meta
。
loader.pitch的参数
pitch一共有三个参数:
remainingRequest
:当前loader右侧的所有loader加上资源路径,根据!
分割,连接而成的内联loader。
-
precedingRequest
:当前loader左侧的所有loader,根据!
分割,连接而成的内联loader。 -
data
:在 pitch 阶段和 normal 阶段之间共享的 data 对象。即:pitch阶段的参数data和normal阶段通过this.data获取的data为同一对象。
补充:左右侧,相对于loader调用链,normal阶段。
example
例如:有a
、b
、c
三个loader,( use: [a, b, c] )在处理某个文件的时候,对应的a、b、c各自的pitch参数分别为:
- a:
remainingRequest
:b-loader.js!c-loader.js!file.txt;precedingRequest
: '';
-
b:
remainingRequest
:c-loader.js!file.txt;precedingRequest
: a-loader.js; -
c:
remainingRequest
:file.txt;precedingRequest
: a-loader.js!b-loader.js;
loader的输出
compiler 预期得到最后一个 loader 产生的处理结果。这个处理结果应该为
String
或者Buffer
(能够被转换为 string)类型,代表了模块的 JavaScript 源码。另外,还可以传递一个可选的 SourceMap 结果(格式为 JSON 对象)。—— webpack官方中文文档
当loader链路到了最后一个loader
时,compiler
期望得到的处理结果是可转换为String
的Buffer
或者String
类型,表示当前模板处理后的js
源码。同时根据callback
函数可知,还可以传递一个sourceMap
。
除去正常loader处理返回的形式输出源码外,还可以根据this.emitFile
来进行额外输出文件。
emitFile(
name: string,
content: Buffer|string,
sourceMap?: {...}
);
loader缓存
开发环境默认情况下,loader 的处理结果会被标记为可缓存。因为很多loader转换的过程是非常耗时的,webpack默认会将所有loader标记为可缓存的,在依赖文件没有改变的情况下是不会重新进行loader处理的过程。
可以通过this.cacheable=false
来标记loader为不可缓存,在大部分情况下,还是不推荐将loader标记为不可缓存,可以使用this.addDependency
添加文件依赖。
example
{
entry: "./app.js",
mode: "development",
module: {
rules: [
{
test: /.txt$/,
use: [
{
loader: getLoader("a-loader.js"),
},
],
},
]
}
}
在.txt文件没有发生变化的时候,watch模式下,重新编译是不会重新调用a-loader去重新处理.txt文件。如果期望在某个.js文件发生变化的时候,重新调用a-loader进行处理,就可以使用this.addDependency添加文件依赖。
module.exports = function (content) {
console.log("a-loader");
this.addDependency("/Users/jiangyuereee/Desktop/loader/c.js");
return content;
};
注意:整个loader调用链都会被重新激活。
loaderAPI
除了以上提到的一些API之外,在loader内还可以使用一些其他API:The Loader Context
loader开发工具包
可以通过该包进行loader的开发和调试。
集成了loader开发中,常用的一些方法,方便开发。
便于验证loader options的合法性(包括但不限于loader使用)。
参考资料
-- 欢迎关注「 字节前端 ByteFE 」 简历投递联系邮箱「tech@bytedance.com」
转载自:https://juejin.cn/post/6950092728919130126