认识
一、认识
Webpack
中的 loader
本质上是一个函数, 接收文件内容源代码作为入参, 同时返回处理后的结果。
Webpack
的 loader-runner
核心逻辑为: 在 Webpack
进入构建阶段后,首先接受待处理的资源文件路径, 会通过 IO
接口读取文件内容,之后调用 LoaderRunner
并将文件内容以 source
参数形式传递到 Loader
数组,source
数据在 Loader
数组内首先经过pitch
阶段读取资源文件内容再经过normal
阶段处理资源文件内容,最终以标准 JavaScript
代码提交给 Webpack
主流程,以此实现内容翻译功能。
二、语法
2.1 配置语法
module.exports = {
module:{
rules: [
{
test: //,
enforce: 'pre',
use: [{loader: 'xx-loader'}]
}
]
}
}
-
test
: 是一个正则表达式,我们会对应的资源文件根据test
的规则去匹配。如果匹配到,那么该文件就会交给对应的loader
去处理。 -
use
:use
表示匹配到test
中匹配对应的文件应该使用哪个loader
的规则去处理,use
可以为一个字符串,也可以为一个数组。额外注意,如果use
为一个数组时表示有多个loader
依次处理匹配的资源,按照从右往左(从下往上
) 的顺序去处理。 -
enforce
:loader
中存在一个enforce
参数标志着loader
的顺序。enforce
有两个值分别为pre
、post
:-
当我们的
rules
中的规则没有配置enforce
参数时,默认为normal loader
(默认loader
)。 -
当我们的
rules
中的规则配置enforce:'pre'
参数时,我们称之它为pre loader
(前置loader
)。 -
当我们的
rules
中的规则配置enforce:'post'
参数时,我们称之它为post loader
(后置loader
)。
-
2.2 编写语法
function loader(sourceCode, sourceMap?, data?){
console.log(this);
}
loader.pitch = function(remainingRequest, previousRequest, data){
}
module.exports = loader;
-
sourceCode
: 资源输入, 对于第一个执行的Loader
为资源文件的内容;后续执行的Loader
则为前一个Loader
的执行结果,可能是字符串,也可能是代码的AST
结构 -
sourceMap
: 可选参数,代码的sourcemap
结构 -
data
: 可选参数,其它需要在Loader
链中传递的信息,比如posthtml/posthtml-loader
就会通过这个参数传递额外的AST
对象 -
this
: 表示上下文对象, 上下文对象将在运行Loader
时以this
方式注入到Loader
函数。有如下属性:-
this.fs
:Compilation
对象的inputFileSystem
属性,我们可以通过这个对象获取更多资源文件的内容 -
this.resource
: 当前文件路径 -
this.resourceQuery
: 文件请求参数,例如import "./a?foo=bar"
的resourceQuery
值为?foo=bar
-
this.async
: 用于声明这是一个异步Loader
,开发者需要通过async
接口返回的callback
函数传递处理结果 -
this.callback
: -
this._compiler
: 用于访问webpack
的当前Compiler
对象 -
this._compilation
: 用于访问webpack
的当前Compilation
对象 -
this.getOptions(schema)
: 用于获取当前Loader
的配置对象。提取给定的loader
选项,接受一个可选的JSON schema
作为参数 -
this.cacheable(true / false)
:Webpack
默认会缓存Loader
的执行结果直到资源或资源依赖发生变化,开发者需要对此有个基本的理解,必要时可以通过this.cachable
显式声明不作缓存 -
this.emitWarning
: 添加警告 -
this.emitError
: 添加错误信息,注意这不会中断Webpack
运行 -
this.emitFile
: 用于直接写出一个产物文件,例如file-loader
依赖该接口写出Chunk
之外的产物; -
addDependency
: 将dep
文件添加为编译依赖,当dep
文件内容发生变化时,会触发当前文件的重新构建
-
-
pitch
:-
remainingRequest
: 表示剩余需要处理的loader
的绝对路径以!
分割组成的字符串。 -
previousRequest
: 表示pitch
阶段已经迭代过的loader
按照!
分割组成的字符串。 -
data
: 默认是一个空对象{}
, 在normalLoader
与pitch Loader
进行交互正是利用了第三个data
参数。
-
三、编写
3.1 通过 this.cacheable() 取消 Loader 缓存
需要注意,Loader
中执行的各种资源内容转译操作通常都是 CPU
密集型 —— 这放在 JavaScript
单线程架构下可能导致性能问题;又或者异步 Loader
会挂起后续的加载器队列直到异步 Loader
触发回调,稍微不注意就可能导致整个加载器链条的执行时间过长。
为此,Webpack
默认会缓存 Loader
的执行结果直到模块或模块所依赖的其它资源发生变化,我们也可以通过 this.cacheable
接口显式关闭缓存:
module.exports = function(source) {
this.cacheable(false);
// ...
return output;
};
3.2 Loader 通过 this.callback() 返回多个结果
简单的 Loader
可直接 return
语句返回处理结果,复杂场景还可以通过 callback
接口返回更多信息,供下游 Loader
或者 Webpack
本身使用
export default function loader(content, map) {
this.callback(null, content, map);
}
通过 this.callback(null, content, map)
语句,同时返回转译后的内容与 sourcemap
内容。callback
的完整签名如下:
this.callback(
// 异常信息,Loader 正常运行时传递 null 值即可
err: Error | null,
// 转译结果
content: string | Buffer,
// 源码的 sourcemap 信息
sourceMap?: SourceMap,
// 任意需要在 Loader 间传递的值
// 经常用来传递 ast 对象,避免重复解析
data?: any
);
3.3 Loader 通过 this.async() 返回异步结果
涉及到异步或 CPU
密集操作时,Loader
中还可以以异步形式返回处理结果
async function loader(source){
// 1. 调用 this.async() 获取异步回调函数, 此时 Webpack 会将该 Loader 标记为异步加载器, 会挂起当前执行队列直到 callback 被触发;
const callback = this.async();
try{
// 2. 编译模块
const result = await handler(source);
}catch(error){
}
// 3. 编译结束, 调用异步回调 callback 返回处理结果
callback(null,css,map);
}
3.4 Loader 通过 return promise 返回异步结果
涉及到异步或 CPU
密集操作时,Loader
中还可以以异步形式返回处理结果
function loader(source){
return Promise((resolve)=>{
setTimeout(()=>{
resolve(3000);
});
});
}
3.5 通过导出 raw 处理 Loader 二进制资源
默认情况下,资源文件会被转化为 UTF-8
字符串,然后传给 loader
。通过设置 raw
为 true
,loader
可以接收原始的 Buffer
。每一个 loader
都可以用 String
或者 Buffer
的形式传递它的处理结果。complier
将会把它们在 loader
之间相互转换。
有时候我们期望以二进制方式读入资源文件,例如在 file-loader
、image-loader
等场景中,此时只需要添加 export const raw = true
语句即可,如:
module.exports = function (content) {
assert(content instanceof Buffer);
return someSyncOperation(content);
// 返回值也可以是一个 `Buffer`
// 即使不是 "raw",loader 也没问题
};
module.exports.raw = true;
3.6 Loader 通过 this.emitFile() 直接写出文件
Loader Context
的 emitFile
接口可用于直接写出新的产物文件
function loader(source){
this.emitFile(outputPath,content,null,assetInfo);
return ……
}
借助 emitFile
接口,我们能够在 Webpack
构建主流程之外提交更多产物,这有时候是必要的,除上面提到的 file-loader
外,response-loader
、mermaid-loader
等也依赖于 emitFile
实现构建功能。
3.7 Loader 通过 this.addDependency() 添加额外依赖
Loader Context
的 addDependency
接口用于添加额外的文件依赖,当这些依赖发生变化时,也会触发重新构建
function loader(source){
this.addDependency(……);
}
3.8 Loader 函数上挂载 pitch 函数, 先于 Loader 执行
Webpack
启动后会以一种所谓链式调用的方式按 use
数组顺序从后到前调用 Loader
。链式调用有两个问题:
-
Loader
链条一旦启动之后,需要所有Loader
都执行完毕才会结束,没有中断的机会 —— 除非显式抛出异常 -
某些场景下并不需要关心资源的具体内容,但
Loader
需要在source
内容被读取出来之后才会执行
为了解决这两个问题,Webpack
在 Loader
基础上叠加了 pitch
的概念。Webpack
允许在 Loader
函数上挂载名为 pitch
的函数,运行时 pitch
会比 Loader
本身更早执行。
function loader1(sourceCode){
console.log("loader1");
return sourceCode + `\n // loader1 处理`;
}
loader1.pitch = function(remainingRequest, previousRequest, data){
}
export default loader1;
-
remainingRequest
: 当前loader
之后的资源请求字符串 -
previousRequest
: 在执行当前loader
之前经历过的loader
列表 -
data
: 与Loader
函数的data
相同,用于传递需要在Loader
传播的信息
实现上,Loader
链条执行过程分三个阶段:pitch
、解析资源、执行。pitch
阶段按配置顺序从左到右逐个执行 loader.pitch
函数(如果有的话),开发者可以在 pitch
返回任意值中断后续的链路的执行。纯函数.pitch
如果返回一个非 undefined
的值会发生熔断, loader
执行链条会被阻断, 立马掉头执行, 直接掉头执行上一个已经执行的loader
的normal
阶段并且将pitch
的返回值传递给下一个normal loader
pitcher loader
应用场景: 如果在loader
开发中你的需要依赖loader
其他loader
,但此时上一个loader
的normal
函数返回的并不是处理后的资源文件内容而是一段js
脚本, 可以将逻辑放在 pitch
中, 通过 import someThing from !!${remainingRequest}
的方式, 将脚本交给webpack
去编译执行。
四、配置
4.1 modules.rules loaderName
module.exports = {
module: {
rules: [
{
test:/\.js$/,
loader: "xx-loader"
}
]
}
}
module.exports = {
module: {
rules: [
{
test:/\.js$/,
use: ['xx-loader','xx-loader']
}
]
}
}
module.exports = {
module: {
rules: [
{
test:/\.js$/,
use: [{ loader: 'xx-loader' }]
}
]
}
}
4.2 module.rules loaderPath
const path = require('path')
// webpack.config.js
module.exports = {
...
module: {
rules: [
{
test:/\.js$/,
// .js后缀其实可以省略,后续我们会为大家说明这里如何配置loader的模块查找规则
loader: path.resolve(__dirname,'../loaders/babel-loader.js')
}
]
}
}
这里我们在loader
参数中传入一个绝对路径的形式,直接去该路径查找对应的loader
所在的js
文件。
4.3 module.rules resolveLoader.alias
const path = require('path')
// webpack.config.js
module.exports = {
...
resolveLoader: {
alias: {
'babel-loader': path.resolve(__dirname,'../loaders/babel-loader.js')
}
},
module: {
rules: [
{
test:/\.js$/,
loader: 'babel-loader'
}
]
}
}
此时,当webpack
在解析到loader
中使用babel-loader
时,查找到alias
中定义了babel-loader
的文件路径。就会按照这个路径查找到对应的loader
文件从而使用该文件进行处理。
4.4 module.rules resolveLoader.modules
const path = require('path')
// webpack.config.js
module.exports = {
...
resolveLoader: {
modules: [ path.resolve(__dirname,'../loaders/') ]
},
module: {
rules: [
{
test:/\.js$/,
loader: 'babel-loader'
}
]
}
}
4.5 import inline loader
引用资源时,通过!
分割使用loader
的方式称为行内loader
。
import Styles from 'style-loader!css-loader?modules!./styles.css';
4.6 import inline of disable normal loader
单个 !
开头, 排除所有 normal-loader
import Styles from '!style-loader!css-loader?modules!./styles.css';
4.7 import inline of disable all loader
两个 !!
开头, 排除所有 pre-loader
、normal-loader
、post-loader
import Styles from '!!style-loader!css-loader?modules!./styles.css';
4.8 import inline of disable preLoader and normalLoader
一个 -!
开头, 排除所有 pre-loader
、normal-loader
import Styles from '-!style-loader!css-loader?modules!./styles.css';
五、思考与沉淀
5.1 loader 与 plugin 的区别
loader
本质上是一个函数, 在 Webpack
进入构建阶段后,首先接受待处理的资源文件路径, 会通过 IO
接口读取文件内容,之后调用 LoaderRunner
并将文件内容以 source
参数形式传递到 Loader
数组,source
数据在 Loader
数组内首先经过pitch
阶段读取资源文件内容再经过normal
阶段处理资源文件内容,最终以标准 JavaScript
代码提交给 Webpack
主流程,以此实现内容翻译功能。
Webpack
中的 Plugin
是一个类, 每一个插件都必须提供一个 apply
方法。apply
方法会接收一个 compiler
对象。 编写 Plugin
就是通过 tapable
发布订阅的能力操作 compiler
对象从而影响打包结果。其过程为: 每一个 Plugin
的 apply
方法通过 tapable
在 Webpack
编译准备阶段订阅对应的事件, 当编译执行到一定阶段时发布对应事件, 执行对应事件函数, 从而达到在编译阶段的不同生命周期内去触发对应的 Plugin
。
因此, plugin
和loader
的区别在于loader
只在编译module
时执行,而plugin
可能在webpack
工作流程的各个阶段执行。