Webpack5源码解读系列6 - 模块预处理器 - Loader
Loader介绍 & 使用
Loader介绍
Loaders are transformations that are applied to the source code of a module. They allow you to pre-process files as you import
or “load” them. Thus, loaders are kind of like “tasks” in other build tools and provide a powerful way to handle front-end build steps. Loaders can do these things:
- Transform files from a different language (like TypeScript) to JavaScript.
- Load inline images as data URLs.
- Allow you to do things like
import
CSS files directly from your JavaScript modules!
上面是官方文档介绍内容,主要介绍Loader
的执行时机以及用途:
- 执行时机:当模块被导入或者加载时,
Loader
会将源码进行转义; - 用途:
Loader
可以灵活用于代码转义(TS to JS)、图片处理、CSS文件处理等。
浏览器能够识别的语言只有JS、CSS和HTML,但在前端生态衍生出了很多新的语言以及语言糖,如TS
、Less
、JSX
、Vue Template
,编译器无法直接解析这些语言/语法,需要有前置工具处理这些语言/语法,Loader
则是提供模块预处理能力。
Loader基础用法
Loader
有配置式和内联式用法,配置式用于配置项目维度处理逻辑,而内联式用于配置单一文件,下面介绍时会出现一些名词介绍。
配置式
配置式用法从项目维度配置应用Loader
,具体配置路径为module.rules
。配置在执行时按照从右到左、从上到下规则执行配置。
module.exports = {
module: {
rules: [
{
// 匹配文件后缀应用 Loader
test: /.css$/,
use: [
// loader名称
{ loader: 'style-loader' },
{
loader: 'css-loader',
// loader自定义配置
options: {
modules: true,
},
},
],
},
{
test: /.js$/,
use: ['babel-loader'],
// 可选 pre 或 post,如果不填则是normal,按照pre -> normal -> post三个阶段顺序执行
enfore: 'pre'
}
],
},
};
内联式
内联式用法可针对个别模块配置Loader
,在import
语句内配置,用法为先配置Loader
后配置引用文件,Loader
之间使用!
做分割:
// 自右向左分别应用 css-loader style-loader
import Styles from 'style-loader!css-loader?modules!./styles.css';
内联使用Loader
可通过前缀去禁用来自配置文件配置的Loader
。
// 使用`!`开头禁用来自配置文件的所有 normal 阶段的loader
import Styles from '!style-loader!css-loader?modules!./styles.css';
// 使用`!!`开头代表禁用所有的的配置loader,包括 pre、normal、post阶段
import Styles from '!!style-loader!css-loader?modules!./styles.css';
// 使用`-!`开头代表禁用所有 pre、normal阶段的loader
import Styles from '-!style-loader!css-loader?modules!./styles.css';
运行Loader原理
Webpack
并没有直接编写处理Loader
模块,而是将Loader
执行模块抽离出来,单独作为第三方包loader runner
。
Loader本质
Loader
是一个同步或者异步JS函数,loader runner
在执行Loader
时会传入上一个Loader
执行结果以及资源内容,同时在函数内部(非箭头函数)还可以使用this
语句访问执行Context
对象。同时,由于Loader
是在Node
环境下执行,所以可以使用Node
环境的任何能力。
const loaderAPI = () => {};
// Loader导出函数作为执行对象
export default loaderAPI;
Loader
区分同步和异步,异步用法与我们平时使用基于Promise
异步能力不同,是由loader runner
内部实现一套区分同步异步能力。
- 同步
Loader
:同步Loader
可通过return
语句返回单个内容,也使用this.callback
传递多个返回:
module.exports = function (content, map, meta) {
// 返回单个结果
return someSyncOperation(content);
};
module.exports = function (content, map, meta) {
// 调用this.callback返回多个结果
this.callback(null, someSyncOperation(content), map, meta);
return; // always return undefined when calling callback()
};
- 异步
Loader
:需要显式调用this.async()
方法才可定义为异步Loader
。
module.exports = function (content, map, meta) {
// 通过调用this.async获取返回值
var callback = this.async();
someAsyncOperation(content, function (err, result, sourceMaps, meta) {
if (err) return callback(err);
// 多个返回值
callback(null, result, sourceMaps, meta);
});
};
loader runner
内部通过runSyncOrAsync
方法完成处理Loader
同步、异步逻辑,函数内部流程如下:
Loader执行顺序
规则优先级别
Loader
可以通过enforce
配置字段调整规则处理顺序,对应选项有pre
、post
,如果不填,默认为normal
,如:
module.exports = {
module: {
rules: [
{
test: /.xxx$/,
use: ['normal-loader-1', 'normal-loader-2'],
},
{
test: /.xxx$/,
use: ['pre-loader-1', 'pre-loader-2'],
enforce: 'pre',
},
{
test: /.xxx$/,
use: ['post-loader-1', 'post-loader-2'],
enforce: 'post',
},
],
},
};
如果上面三个规则同时命中时,会按照pre
-> normal
-> post
顺序执行,如果处理的文件还包含内联Loader
配置时,那么会按照pre
-> normal
-> inline
-> post
顺序执行。
Loader处理阶段
Loader
在运行时区分Pitch
阶段和Normal
阶段,一般情况下执行顺序是指Normal
阶段的执行顺序,按照“从右到左”顺序执行,而Pitch
阶段则相反,是“从左到右”。
Pitch
阶段:按照post
->inline
->normal
->pre
处理顺序在读取源代码之前执行Normal
阶段:按照规则优先级别为pre
->normal
->inline
->post
执行顺序修改模块源码,最终输出。
Loader
可以定义普通方法和pitch
方法,如果Loader
函数的pitch
属性如果是一个方法的话,那么loader runner
会为其注册pitch
阶段任务。
为什么要有pitch方法呢?
有某些场景loader需要在修改目标的metadata, 此时需要在读取资源之前做处理,而普通loader方法是在读取到文件之后再做处理,无法满足需求。
loader runner
在执行时,会先进入Pitch
阶段,按“从左到右”顺序执行Loader.pitch
,执行完毕后读取文件内容,再进入Normal
阶段,按照“从右到左”顺序执行Loader
。
Loader熔断机制
如果一个Loader
的pitch
方法返回一个undefined
值时,那么会执行下一个方法,但是如果返回的是一个非空值时,那么会触发Loader
的熔断机制,不再继续执行后续的Loader
,而是掉头往前面执行:
实例讲解
Loader
存在Normal
和Pitch
之分,接下来分别讲解babel-loader
和style-loader
,感受一下不同类型Loader
的应用。
babel-loader
如果我们的应用需要兼容旧版本浏览器,那么我们一定会使用到babel-loader
,babel-loader
是对babel
进行一层封装,将应用代码转义为向后兼容代码。简而言之babel-loader
是babel
与webpack
的适配层。
接下来我们浅浅地解析一下代码:
首先映入眼帘的是babel-loader
的模块导出语句,从代码中我们能过获得两个信息:
-
babel-loader
只有一个normal loader
,所以该Loader
是在读取文件后运行。 -
babel-loader
是一个异步Loader
。
接下来进入loader
函数,函数内部做了两件事情:
- 参数检验/合并
- 调用
babel/core
进行代码转义、输出
可以看出Loader
实际上并不复杂,就是针对模块内容的字符串处理函数,如果有需要我们也可以编写属于自己的Loader
。
style-loader
介绍
style-loader
提供自动将css
代码注入到html
代码中,无法独自使用,一般和css-loader
或者url-loader
一起使用。以css-loader
为例,它们的分工如下:
css-loader
负责将css
代码转译为js
代码;style-loader
负责将转译后的css
代码自动注入到html
中。
有以下例子和配置
// index.css
.container {
width: 100px;
height: 100px;
}
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /.css$/,
enforce: "pre",
use: ["style-loader", "css-loader"]
}
]
},
}
上面代码经过css-loader
、style-loader
处理之后,在运行时会自动注入到html
中(在运行时动态注入):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>webpack-loader</title>
<style>
.container {
width: 100px;
height: 100px;
}
</style>
</head>
<body></body>
</html>
原理
style-loader
源码大体上可以分为两块内容:
- 代码生成:承接
Loader
处理逻辑部分,本文仅介绍这部分能力原理。 - 运行时代码:运行时提供动态注入
css
代码能力,从这里可以看出webpack
有很强的灵活性。
我们进入源码阅读,首先从入口文件开始读起:
可以判断出style-loader
只有pitch loader
,且 pitch
函数内部会返回 字符串。根据上面讲解的Loader
执行顺序,我们很容易就能够发现webpack
配置项存在问题:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /.css$/,
enforce: "pre",
use: ["style-loader", "css-loader"]
}
]
},
}
根据「熔断机制」,如果一个pitch loader
返回一个非空内容,那么就不会执行后面的Loader
,此时css-loader
也不会被执行。
别急,我们继续看pitch
执行完毕时返回具体内容,我们以一个简单例子看一下整个过程:
// index.js
import './index.css';
export default function A() {}
// index.css
.abc {
width: 100%;
}
左边是生成代码部分,右边是返回style-loader
处理完index.css
的例子:
从右图中观察到style-loader
并没有直接处理index.css
文件,而是导入了运行时注入style
标签能力模块,在结尾部分使用内联 Loader
用法重新导入index.css
文件,此时会使用到css-loader
解析index.css
文件。
在Webpack
视野中,index.css
和../../../node_modules/css-loader/dist/cjs.js!./index.css
是不同文件,所以会重新编译文件,并使用css-loader
解析文件,最终效果如下:
小结
Loader
在Webpack
中充当重要角色,是Webpack
能够将所有资源都视为模块的基础,提供将任何资源模块都解析为JS
模块能力。
Loader
本质上是一个Node
环境下第三方包,能够使用任何Node
能力。Loader
执行时分为Pitch
和Normal
和阶段,Pitch
阶段会提取Loader
导出函数的pitch
方法并执行(Loader.pitch
),它们的执行顺序为:
Pitch
阶段:按照注册顺序“从左到右”(或“从上到下”)执行Loader.pitch
方法。Normal
阶段:按照注册顺序“从右到左”(或“从下到上”)执行Loader
方法。
特殊地,如果一个Loader.pitch
返回的内容为非空字符串,那么会触发“熔断机制”,即不会执行后面任何Loader
,并返回到上一个Loader
并继续执行:
最后分别挑选了
babel-loader
和style-loader
实例讲解处理过程。
转载自:https://juejin.cn/post/7231821116897198140