WEB 十一月 19, 2021

webpack5-note

文章字数 112k 阅读约需 1:42

为什么需要 loader

webpack是一个用于现代 JavaScript 应用程序的静态模块打包工具。内部通过构建依赖图管理模块之间的依赖关系,生成一个或多个 bundle 静态资源。
image.png

但是 webpack 只能处理 JavaScript 、Json 模块。应用程序除了JavaScript 、Json 模块以外还有图片、音频、字体等媒体资源、less、sass 等样式文件等非 js 代码的模块。所以需要一种能力,将非 js 资源模块解析成能够被 webpack 管理的模块。这也就是 loader 的作用。

举个例子,比如对于 less 样式文件来说,在 webpack 配置文件中如果处理 index.less 文件会经过 less-loader、css-loader、style-loader 处理,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
}
]
}
};
复制代码

webpack 解析到 index.less 模块的时候,首先会使用类似于 fs.readFile 去读取文件并且获取到文件内的源代码文本 source;拿到的 source 是需要去经过 js parser 转成 ast 的,但是在这之前会去 webpack 配置的 loader 中看看是否有处理该文件的 loader,发现有 [‘style-loader’, ‘css-loader’, ‘less-loader’] 三个 loader 按照顺序去处理的,所以 webpack 会将 source 源码以及 loader 处理器 交给 loader-runner 这个 loader 处理库,处理库会对源文件按照一定的规则经过层层 loader 进行加工处理,最终得到 webpack 可以识别的模块;然后转成 ast 进行进一步的处理,比如分析 ast ,收集模块的依赖,直到将依赖链路分析完毕为止。

到此为止应该知道 index.less 源文件会经过 三个 loader 按照一定的规则处理后得到 js 模块。那三个 loader 都是干了什么事情使得可以从样式文件转成 js 文件呢?

首先会将 source 作为入参经过 less-loader 处理,less-loader 能够将 less 代码经过 less 解析生成器 转化成 css 代码。当然转化后的 css 代码也是不能直接使用的,因为在 css 中会存在 import 依赖其他的 css 文件。

将 less-loader 解析后的 css 代码传入到 css-loader 中,在 css-loader 中会使用 css parser 解析也就是 postcss 解析 css,比如会将 import 解析成 js 中 require 的形式来引用其他的样式资源,同时还会将 css 代码转化成字符串, 通过 module.exports 抛出,此时已经将 css 文件转成了 js 模块,webpack 能够处理了。但是还不能使用,因为并没有作为 style 标签中被引用。所以需要经过 style-loader 处理。

将 css-loader 解析后的 js 代码 传入到 style-loader 中,经过 loader-utils 中路径转化函数对 require 路径处理,添加创建 style 标签, 以及将 require 引用的代码赋值给 innerHtml 中,这样,得到一段 js 代码,代码中包含了经过 style-loader 添加的 创建 style 标签内容,标签的内容是经过 css-loader 处理的将 css 解析成 js 代码, 同时 less-loader 将 less 文件解析成了 css。然后就将 less 模块解析成了 js 模块,webpack 就会后续的统一管理了。

这就是 webpack 处理 less 文件成 js 文件的过程, 但是这才是一小部分,如果能够真的可以使用还需要很多的路要走,不过不是这篇文章的重点了。到此应该大概的了解了 webpack 中 loader 是什么作用以及为什么需要 loader 了。简单的来说,loader 就是处理module(模块、文件)的,能够将 module 处理成 webpack 能够解析的样子,同时还可以对解析的文件做一些再加工

如何配置 loader

如下是 webpack 中对于 loader 的基本配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
module.exports = {
resolveLoader: {
// 从根目录下那个文件中寻找 loader
modules: ['node_modules', path.join(__dirname, 'loaders')],
},
module: {
rules: [{
enforce: 'normal',
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: {
presets: [
"@babel/preset-env"
]
}
}]
},
{
enforce: 'pre',
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};
复制代码

具体可以参考 webpack.docschina.org/configurati… 对于 rule 的文档详细介绍。其中比较重要的字段是 enforce。将 loader分为了: post(后置)、normal(普通)、pre(前置)类型。

除了可以在配置文件中设置 loader 之外,由于 loader 是对任意一个文件或者模块的处理。所以也可以在引用每一个模块的地方引用 loader,比如说:

1
2
import style from 'style-loader!css-loader?modules!less-loader!./index.less'
复制代码

在文件地址 ./index.less 前可以添加 loader 多个 loader 使用 !分割, 同时再每个 loader 中后面可以添加 ?作为 loader 的 options。这种添加 loader 的方式是 inline(内联)类型的 loader。同时还可以加上特殊标记前缀,来表示某个特定的 model 要使用什么类型的 loader,分别为如下:

符号 变量 含义
-! noPreAutoLoaders 不要 pre(前置) 和 normal(普通) loader
! noAutoLoaders 不要普通 loader
!! noPrePostAutoLoaders 不要前后置和普通 loader,只要内联 loader

比如说对于如下:

1
2
import style from '-!style-loader!css-loader?modules!less-loader!./index.less'
复制代码

对于 ./index.less 这个模块来说,不能使用配置文件中配置的前置普通的 loader,只能使用后置的以及内联的 loader 处理本模块。

所以说对于处理模块的 loader 来说,一共有四种类型: post(后置)、normal(普通)、inline(内联)、 pre(前置)。一共有三种标记可以标记某个特定模块具体使用什么类型的 loader, 接下来通过源码的角度来看看具体是怎么实现的。

loader 怎么工作

假设有如下文件和 rules:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const request = 'inline-loader1!inline-loader2!./src/index.js';
const rules = [
{
enforce: 'pre',
test: /\.js$/,
use: ['pre-loader1', 'pre-loader2'],
},
{
enforce: 'normal',
test: /\.js$/,
use: ['normal-loader1', 'normal-loader2'],
},
{
enforce: 'post',
test: /\.js$/,
use: ['post-loader1', 'post-loader2'],
}
];
复制代码

这里 request 也就是模块为./src/index.js, 同时该模块被 async-loader1 以及 async-loader2 这两个内联的 loader 处理。 同时还有一个 webpack 配置文件中的 rules,其中有 前置 loader pre-loader1、pre-loader2,普通的 loader normal-loader1、normal-loader2,当然对于 enforce 没有被赋值的情况下就是默认的 normal。还有 post 后置 loader post-loader1、post-loader2。

首先我们需要获取出这四种 loader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const preLoaders = [];
const normalLoaders = [];
const postLoaders = [];
const inlineLoaders = request.replace(/^-?!+/, "").replace(/!!+/g, '!').split('!');

for(let i = 0; i < rules.length; i++) {
let rule = rules[i];
if(rule.test.test(resource)) {
if(rule.enforce === 'pre') {
preLoaders.push(...rule.use);
} else if(rule.enforce === 'post') {
postLoaders.push(...rule.use);
} else { // normal
normalLoaders.push(...rule.use);
}
}
}
复制代码

为了获取 内联的 loader,需要将引用的地址用 !分割获取, 但是在这之前,需要将 -?! 特殊标记前缀置位空,同时对于连续的 !也需要置为空避免出现空的 loader。这样就能获取到 [ ‘async-loader1’, ‘async-loader2’, ‘./src/index.js’ ], 已经能够获取到内联的 loader 了,同时通过循环遍历 rules 能够获取到其他的 loader。到此我们已经拿到四种 loader 了。值得注意的是, 在引用地址中和 rules 中 loader 的顺序就是定义的顺序内有发生改变的。

接下来我们需要获取 loader 执行的顺序列表 loaders 了。默认情况下也就是没有特殊标记的情况下,loaders 会是如下的顺序生成:

1
2
3
4
5
6
7
loaders = [
...postLoaders,
...inlineLoaders,
...normalLoaders,
...preLoaders,
];
复制代码

默认情况下,分别按照 post inline normal pre 的顺序以及每一种 loader 定义的顺序排列生成 loaders。

对于带有特殊标记的引用来说也会影响到 loaders 中的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if(request.startsWith('!')) { // 不要 normal
loaders = [
...postLoaders,
...inlineLoaders,
...preLoaders,
];
} else if(request.startsWith('-!')) { // 不要 normal、pre
loaders = [
...postLoaders,
...inlineLoaders
];
} else if(request.startsWith('!!')) { // 不要 post、normal、pre
loaders = [
...inlineLoaders,
];
} else { // post、inline、normal、pre
loaders = [
...postLoaders,
...inlineLoaders,
...normalLoaders,
...preLoaders,
];
}
复制代码

对于 引用地址 request 仅仅是以 ! 开头 是不需要 normal 类型的 loader 的, 但是其他的类型的 loader 顺序依然保持。同理,-! 不需要 normal、pre loader, !! 不需要 post、normal、pre loader。

到此,对于引用文件 request 的 loader处理列表 loaders 已经拿到了,接下来需要经过 loader-runner 对 loader 列表中的 loader 按照一定的规则对 资源文件进行加工处理。

1
2
3
4
5
6
7
runLoaders({
resource: path.join(__dirname, resource),
loaders
}, (err, data) => {
console.log(data);
});
复制代码

loader 获取 loaders 列表的完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
const { runLoaders } = require('loader-runner');
const fs = require('fs');
const path = require('path');

const loadDir = path.resolve(__dirname,'loaders', 'runner');
const request = 'inline-loader1!inline-loader2!./src/index.js';

let preLoaders = [];
let normalLoaders = [];
let postLoaders = [];
let inlineLoaders = request.replace(/^-?!+/, "").replace(/!!+/g, '!').split('!');

const resource = inlineLoaders.pop();

const resolveLoader = loader => path.resolve(loadDir, loader);

const rules = [
{
enforce: 'pre',
test: /\.js$/,
use: ['pre-loader1', 'pre-loader2'],
},
{
enforce: 'normal',
test: /\.js$/,
use: ['normal-loader1', 'normal-loader2'],
},
{
enforce: 'post',
test: /\.js$/,
use: ['post-loader1', 'post-loader2'],
}
];



for(let i = 0; i < rules.length; i++) {
let rule = rules[i];
if(rule.test.test(resource)) {
if(rule.enforce === 'pre') {
preLoaders.push(...rule.use);
} else if(rule.enforce === 'post') {
postLoaders.push(...rule.use);
} else {
normalLoaders.push(...rule.use);
}
}
}

preLoaders = preLoaders.map(resolveLoader);
normalLoaders = normalLoaders.map(resolveLoader);
inlineLoaders = inlineLoaders.map(resolveLoader);
postLoaders = postLoaders.map(resolveLoader);
let loaders = [];

if(request.startsWith('!')) { // 不要 normal
loaders = [
...postLoaders,
...inlineLoaders,
...preLoaders,
];
} else if(request.startsWith('-!')) { // 不要 normal、pre
loaders = [
...postLoaders,
...inlineLoaders
];
} else if(request.startsWith('!!')) { // 不要 post、normal、pre
loaders = [
...inlineLoaders,
];
} else { // post、inline、normal、pre
loaders = [
...postLoaders,
...inlineLoaders,
...normalLoaders,
...preLoaders,
];
}

runLoaders({
resource: path.join(__dirname, resource),
loaders,
readResource:fs.readFile.bind(fs)
}, (err, data) => {
console.log(data);
});


复制代码

总结一下,webpack 拿到模块文件的引用地址 request 后,其中有一步需要经过 loader 处理,首先或取出四种 loader 并且的分别按照 post inline normal pre 组装成 loader 执行列表 loaders, 同时其中可以通过特殊标记对 loader 类型进行过滤。但是 loader 顺序依然保持。得到 laoders 后会交给 loader-runner 对按照一定的规则对源文件做进一步的加工处理。 接下来详细介绍下比较重要的 loader-runner,先从基本的原理概念讲解,然后一起实现一个 loader-runner。

loader-runner 基本规则

有没有考虑过一个问题,为什么在配置文件中配置的 loader 都是从右向左的处理源文件而不是从左到右呢?这是由于 loader-runner 在处理每一个 loader 的时候,会先从左到右的执行 loader pitch 方法,然后再执行本身的 loader 方法称为 normal。如下图所示:
image.png
上一节已经介绍了 loaders 里面有一定顺序的 loader,然后会交给 laoder-runner 执行,有如下 post-loader1 代码所示,loader 可以添加一个 pitch 方法:

1
2
3
4
5
6
7
8
9
function loader(source){
console.log('post-loader1 normal');
return source+"【post-loader1】";
}
loader.pitch = function(){
console.log('post-pitch1 pitch');
}
module.exports = loader;
复制代码

loader 方法可以称为 normal 方法,该方法主要接受 源文件内容作为参数,然后返回加工后的源文件,例子中的 loader 主要就是为 source 字符串后追加 【post-loader1】字符串,然后交给一下个 loader 的 normal 作为入参执行。同时 normal 方法中也可以添加一个 pitch 方法, 该方法主要是为执行 loader 之前做一些预处理或者拦截的工作。

开始解释下这个图,整个处理过程类似于 DOM 的事件冒泡机制,开始调用 loader-runner,会按照 laoders 中的顺序依次执行,先执行 loader 中的 pitch 方法,如果方法没有返回值则继续执行下一个 pitch 直到执行完毕后开始执行最后一个 loader 的 normal。然后从右向左的执行 loader 的 normal方法,你并且前一个 loader 的返回值作为后一个 normal 的入参。但是如果在中途有 loader 的 pitch 返回值 如图红色虚线, 那么则直接将返回值作为前一个 loader normal 的入参然后继续执行,这样子就不会去解析源代码了,比如缓存中会使用到这个场景。

对于 loader 来说无论是 normal 还是 pitch 都可以写同步代码和异步代码的,对于同步代码可以直接返回一个值就可以作为下一个 loader 的入参。但是异步的会有一点点差别具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function loader(source) {
const callback = this.async();
setTimeout(() => {
callback(null, source + "【async-loader1】");
}, 3000);
}
loader.pitch = function () {
const callback = this.async();
console.log('async-loader1-pitch');
callback(null);
}
module.exports = loader;
复制代码

首先需要调用 this.async 函数来声明这是个异步的方法,返回一个回调的句柄,用来异步执行完毕后执行后续的流程。callback 提供 err、和下一个 normal 的入参。同时 pitch 也是一样也可以 daioyong this.async 讲一个同步的方法改成异步。

到此,loader 执行流程和同步异步 loader 介绍完了,接下来我们用源码角度去进一步的了解 loader-runner,以及了解基于职责链模式的设计。

让我们实现 loader-runner

loader-runner 上一节也介绍了整体的流程和DOM 事件冒泡机制、作用域链、原型链、react 事件机制、koa 洋葱模型等都是差不多的,他们核心都是基于 职责链模式 这一设计模式的。具体关于职责链模式可以参考 www.yuque.com/docs/share/… 对于职责链模式是使多个对象可以统一的处理,避免了请求方因为可能类型多种多样导致和接受处理方的耦合,为了保证请求可以经过多个接受按照规则处理,需要有一套机制将接收方行程一条链,然后请求方就会按照这个链路进行执行。所以职责链来说重要的是:一点为了保证请求的函数职责单一,需要具备通用性也就是函数签名和返回值应该保持一致,并且具备通知链条开始执行下一个的能力;二点为了保证请求的函数式开放封闭的,需要一个链条将这个过程串联起来。

开始执行的 loader 代码中会提供 options 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
runLoaders({
resource: path.join(__dirname, resource),
loaders,
readResource:fs.readFile.bind(fs)
}, (err, data) => {
console.log(data);
});

runLoaders 第一个参数 options 值为:
{
resource: '/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/src/index.js',
loaders: [
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/post-loader1',
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/post-loader2',
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/inline-loader1',
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/inline-loader2',
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/normal-loader1',
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/normal-loader2',
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/pre-loader1',
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/pre-loader2'
]
}
复制代码

提供给 runLoader 方法的第一个参数 options 有资源的绝对地址路径 resource,以及 loader 绝对地址路径的 loaders 列表。

对于 runLoaders 方法而言,一部分是创建一个执行上下文环境,然后调用 iteratePitchingLoaders 方法开始进入 laoder 的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
exports.runLoaders = function (options, callback) {
createLoaderContext(options);
let processOptions = {
resourceBuffer: null, //最后我们会把loader执行的Buffer结果放在这里
readResource: options.readResource || readFile,
}
iteratePitchingLoaders(processOptions, loaderContext, function (err, result) {
if (err) {
return callback(err, {});
}
callback(null, {
result,
resourceBuffer: processOptions.resourceBuffer
});
});
};
复制代码

接下来介绍一下 loader 执行上下文,以及 loader 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
function parsePathQueryFragment(resource) { //resource =./src/index.js?name=wms#1
let result = /^([^?#]*)(\?[^#]*)?(#.*)?$/.exec(resource);
return {
path: result[1], //路径名 ./src/index.js
query: result[2], // ?name=wms
fragment: result[3] // #1
}
};

//loader绝对路径
// 比如:/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/post-loader1?{"presets":["/Users/anning/Desktop/webpack-demo/node_modules/@babel/preset-env"]}"
function createLoaderObject(loader) {
let obj = {
path: '', //当前loader的绝对路径
query: '', //当前loader的查询参数
fragment: '', //当前loader的片段
normal: null, //当前loader的normal函数,也就是loader本函数
pitch: null, //当前loader的pitch函数
raw: null, //是否是Buffer
data: {}, //自定义对象 每个loader都会有一个data自定义对象
pitchExecuted: false, //当前 loader的pitch函数已经执行过了,不需要再执行了
normalExecuted: false //当前loader的normal函数已经执行过了,不需要再执行
}
Object.defineProperty(obj, 'request', {
get() {
return obj.path + obj.query + obj.fragment;
},
set(value) {
let splittedRequest = parsePathQueryFragment(value);
obj.path = splittedRequest.path;
obj.query = splittedRequest.query;
obj.fragment = splittedRequest.fragment;
}
});
obj.request = loader;
return obj;
};

function loadLoader(loaderObject) {
let normal = require(loaderObject.path);
loaderObject.normal = normal;
loaderObject.pitch = normal.pitch;
loaderObject.raw = normal.raw;
};

function createLoaderContext(options) {
// 要加载的资源的绝对路径
const splittedResource = parsePathQueryFragment(options.resource || '');

// 准备loader对象数组
loaders = (options.loaders || []).map(createLoaderObject);

// loader执行时候的上下文对象 这个对象将会成为loader执行的时候的this指针
const loaderContext = {};

loaderContext.context = path.dirname(splittedResource.path); // 要加载的资源的所在目录
loaderContext.loaderIndex = 0; //当前处理的loader索引
loaderContext.loaders = loaders; // loader集合
loaderContext.resourcePath = splittedResource.path;//资源绝对路径
loaderContext.resourceQuery = splittedResource.query;// 资源路径中解析出的query
loaderContext.resourceFragment = splittedResource.fragment;// 资源路径中解析出的片段
loaderContext.async = null; //是一个方法,可以使loader的执行从同步改成异步
loaderContext.callback = null; //调用下一个loader

// 加载资源的完整路径
Object.defineProperty(loaderContext, 'resource', {
get() {
return loaderContext.resourcePath + loaderContext.resourceQuery + loaderContext.resourceFragment;
}
});
//request =loader1!loader2!loader3!./src/index.js
Object.defineProperty(loaderContext, 'request', {
get() {
return loaderContext.loaders.map(l => l.request).concat(loaderContext.resource).join('!')
}
});
//剩下的loader从当前的下一个loader开始取,加上resource
Object.defineProperty(loaderContext, 'remainingRequest', {
get() {
return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(l => l.request).concat(loaderContext.resource).join('!')
}
});
//当前loader从当前的loader开始取,加上resource
Object.defineProperty(loaderContext, 'currentRequest', {
get() {
return loaderContext.loaders.slice(loaderContext.loaderIndex).map(l => l.request).concat(loaderContext.resource).join('!')
}
});
//之前loader
Object.defineProperty(loaderContext, 'previousRequest', {
get() {
return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(l => l.request)
}
});
//当前loader的query, 如果配置中配置了options则使用,否则使用query中的
Object.defineProperty(loaderContext, 'query', {
get() {
let loader = loaderContext.loaders[loaderContext.loaderIndex];
return loader.options || loader.query;
}
});
//当前loader的data,可以在pitch normal函数中获取到
Object.defineProperty(loaderContext, 'data', {
get() {
let loader = loaderContext.loaders[loaderContext.loaderIndex];
return loader.data;
}
});
};
复制代码

createLoaderContext 是根据 传入的options生成执行 normal 或者 pitch 的上下文,也就是 this。在这里面除了一些通用的参数有比较重要的是 async callback。 在之前介绍的异步 loader 中 this.async 中的 this 就是 loaderContext, 同时还有 loaderIndex 作为全局的对象,可以通过loaderIndex控制执行到哪一个 loader,以及流程应该下一步还是上一步。也就是每一个 loader 之间保证职责可以单一, 并且通过 this.callback 来控制是否继续执行。

createLoaderObject 是根据每一个 loader 绝对地址创建 loader 对象,这里面有包含 normal、pitch、等函数。还有 data 用于在 同一个 loader 中 normal、pitch 共享数据,还有pitchExecuted、normalExecuted 执行完毕的标志位。loadLoader 函数是为了加载 loader,module.exports 为 normal 然后获取 pitch、raw。

接下来具体看看 具体是怎么执行的。执行分为三大部分,一部分是iteratePitchingLoaders方法,主要作用是控制在 pitch 方法之间的流转,再者就是iterateNormalLoaders控制 normal 函数在 normal 中的流转,以及还有执行 normal、pitch 函数的 runSyncOrAsync。当 pitch 到末尾的时候需要 processResource 方法去获取源文件。

如下是 iteratePitchingLoaders 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function iteratePitchingLoaders(options, loaderContext, callback) {
// 所有的 pitch 处理完毕,开始获取源代码
if (loaderContext.loaderIndex >= loaderContext.loaders.length) {
return processResource(options, loaderContext, callback);
}
//获取当前的loader
const currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// 已经处理过pitch了,需要处理下一个loader的pitch
if (currentLoaderObject.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(options, loaderContext, callback)
}
// 加载laoder
loadLoader(currentLoaderObject);
let pitchFunction = currentLoaderObject.pitch;
currentLoaderObject.pitchExecuted = true;
if (!pitchFunction) {
return iteratePitchingLoaders(options, loaderContext, callback);
}

runSyncOrAsync(
pitchFunction, //要执行的pitch函数
loaderContext, //上下文对象
//这是要传递给pitchFunction的参数数组
[loaderContext.remainingRequest, loaderContext.previousRequest, loaderContext.data = {}],
//处理完成的回调
function (err, ...args) {
if (args.length > 0) { //如果args有值,说明这个pitch有返回值
loaderContext.loaderIndex--; //索引减1,开始回退了
iterateNormalLoaders(options, loaderContext, args, callback);
} else { //如果没有返回值,则执行下一个loader的pitch函数
iteratePitchingLoaders(options, loaderContext, callback)
}
}
);
};
复制代码

具体来说,使用 loadLoader 加载 loader 处理器,获取到 pitch函数,设置 laoder 的 pitch 标志位设置为 true,表示函数开始进入处理阶段了。在处理阶段,如果获取到的 pitch 函数不存在,则直接重新调用 iteratePitchingLoaders 进入下一个 pitch 执行,否则会调用 runSyncAsync 去执行 pitch 函数,执行完毕后悔根据 callback 传入的参数,如果有参数则 loaderIndex 减一开始执行前一个 loader 的 normal 函数,否则继续执行下一个 pitch,指导所有的 pitch 处理完毕后开始调用 processResource 方法获取源代码执行 normal。传入执行 pitch 的参数有 remainingRequest、previousRequest、data 主要是

1
2
3
4
5
6
7
8
9
10
11
12
function processResource(options, loaderContext, callback) {
//重置loaderIndex 改为loader长度减1
loaderContext.loaderIndex = loaderContext.loaders.length - 1;
let resourcePath = loaderContext.resourcePath;
//调用 fs.readFile方法读取资源内容
options.readResource(resourcePath, function (err, buffer) {
if (err) return callback(error);
options.resourceBuffer = buffer; //resourceBuffer放的是资源的原始内容
iterateNormalLoaders(options, loaderContext, [buffer], callback);
});
}
复制代码

processResource 函数获取到资源源代码后,开始调用 normal, 此时这个 loaderIndex 应该是 iteratePitchingLoaders 中加 1 后减一。

接下来介绍下 iterateNormalLoaders 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function iterateNormalLoaders(options, loaderContext, args, callback) {
//如果正常的normal loader全部执行完了
if (loaderContext.loaderIndex < 0) {
return callback(null, args);
}
let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
//如果说当这个normal已经执行过了,让索引减少1
if (currentLoaderObject.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, args, callback)
}
let normalFn = currentLoaderObject.normal;
currentLoaderObject.normalExecuted = true;

runSyncOrAsync(normalFn, loaderContext, args, function (err) {
if (err) return callback(err);
let args = Array.prototype.slice.call(arguments, 1);
iterateNormalLoaders(options, loaderContext, args, callback);
});
}
复制代码

iterateNormalLoaders 方法 获取到 normal 函数后设置 normal 标志位 true, 开始调用 runSyncOrAsync 执行代码,执行完毕后获取到 参数然后继续执行下一个 normal。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function runSyncOrAsync(fn, context, args, callback) {
let isSync = true; //默认是同步
let isDone = false; //是否完成,是否执行过此函数了,默认是false
//调用context.async this.async 可以把同步把异步,表示这个loader里的代码是异步的
context.async = function () {
isSync = false; //改为异步
return innerCallback;
}
const innerCallback = context.callback = function () {
isDone = true; //表示当前函数已经完成
isSync = false; //改为异步
callback.apply(null, arguments); //执行 callback
}
//第一次fn=pitch1,执行pitch1
let result = fn.apply(context, args);
//在执行pitch2的时候,还没有执行到pitch1 这行代码
if (isSync) {
isDone = true;
return callback(null, result);
}
}
复制代码

runSyncOrAsync 实质性 loader 的 pitch、normal 函数的,如果在函数内没有调用 this.async 则不会执行 context.aysnc 也就是说 isSync 还是 true 则直接执行方法,使用 loaderContext 作为 this 执行,返回值作为结果指导调用后续的流程, 如果是异步的函数,则需要等到该函数异步执行完毕后,然后再调用 callback 继续后续的执行。也就是说他们的执行时异步串行的,并不是异步并行执行的。

下面是 laoder-runner 的完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
const fs = require('fs');
const path = require('path');
const readFile = fs.readFile.bind(fs);

function parsePathQueryFragment(resource) { //resource =./src/index.js?name=wms#1
let result = /^([^?#]*)(\?[^#]*)?(#.*)?$/.exec(resource);
return {
path: result[1], //路径名 ./src/index.js
query: result[2], // ?name=wms
fragment: result[3] // #1
}
};

//loader绝对路径
// 比如:/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/post-loader1?{"presets":["/Users/anning/Desktop/webpack-demo/node_modules/@babel/preset-env"]}"
function createLoaderObject(loader) {
let obj = {
path: '', //当前loader的绝对路径
query: '', //当前loader的查询参数
fragment: '', //当前loader的片段
normal: null, //当前loader的normal函数,也就是loader本函数
pitch: null, //当前loader的pitch函数
raw: null, //是否是Buffer
data: {}, //自定义对象 每个loader都会有一个data自定义对象
pitchExecuted: false, //当前 loader的pitch函数已经执行过了,不需要再执行了
normalExecuted: false //当前loader的normal函数已经执行过了,不需要再执行
}
Object.defineProperty(obj, 'request', {
get() {
return obj.path + obj.query + obj.fragment;
},
set(value) {
let splittedRequest = parsePathQueryFragment(value);
obj.path = splittedRequest.path;
obj.query = splittedRequest.query;
obj.fragment = splittedRequest.fragment;
}
});
obj.request = loader;
return obj;
};

function loadLoader(loaderObject) {
let normal = require(loaderObject.path);
loaderObject.normal = normal;
loaderObject.pitch = normal.pitch;
loaderObject.raw = normal.raw;
};

function createLoaderContext(options) {
// 要加载的资源的绝对路径
const splittedResource = parsePathQueryFragment(options.resource || '');

// 准备loader对象数组
loaders = (options.loaders || []).map(createLoaderObject);

// loader执行时候的上下文对象 这个对象将会成为loader执行的时候的this指针
const loaderContext = {};

loaderContext.context = path.dirname(splittedResource.path); // 要加载的资源的所在目录
loaderContext.loaderIndex = 0; //当前处理的loader索引
loaderContext.loaders = loaders; // loader集合
loaderContext.resourcePath = splittedResource.path;//资源绝对路径
loaderContext.resourceQuery = splittedResource.query;// 资源路径中解析出的query
loaderContext.resourceFragment = splittedResource.fragment;// 资源路径中解析出的片段
loaderContext.async = null; //是一个方法,可以使loader的执行从同步改成异步
loaderContext.callback = null; //调用下一个loader

// 加载资源的完整路径
Object.defineProperty(loaderContext, 'resource', {
get() {
return loaderContext.resourcePath + loaderContext.resourceQuery + loaderContext.resourceFragment;
}
});
//request =loader1!loader2!loader3!./src/index.js
Object.defineProperty(loaderContext, 'request', {
get() {
return loaderContext.loaders.map(l => l.request).concat(loaderContext.resource).join('!')
}
});
//剩下的loader从当前的下一个loader开始取,加上resource
Object.defineProperty(loaderContext, 'remainingRequest', {
get() {
return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(l => l.request).concat(loaderContext.resource).join('!')
}
});
//当前loader从当前的loader开始取,加上resource
Object.defineProperty(loaderContext, 'currentRequest', {
get() {
return loaderContext.loaders.slice(loaderContext.loaderIndex).map(l => l.request).concat(loaderContext.resource).join('!')
}
});
//之前loader
Object.defineProperty(loaderContext, 'previousRequest', {
get() {
return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(l => l.request)
}
});
//当前loader的query, 如果配置中配置了options则使用,否则使用query中的
Object.defineProperty(loaderContext, 'query', {
get() {
let loader = loaderContext.loaders[loaderContext.loaderIndex];
return loader.options || loader.query;
}
});
//当前loader的data,可以在pitch normal函数中获取到
Object.defineProperty(loaderContext, 'data', {
get() {
let loader = loaderContext.loaders[loaderContext.loaderIndex];
return loader.data;
}
});
};

function convertArgs(args, raw) {
// 如果这个loader需要buffer, args[0]不是, 需要转成buffer
if (raw && !Buffer.isBuffer(args[0])) {
args[0] = Buffer.from(args[0], 'utf8');
} else if (!raw && Buffer.isBuffer(args[0])) {
args[0] = args[0].toString('utf8');
}
};

function runSyncOrAsync(fn, context, args, callback) {
let isSync = true; //默认是同步
let isDone = false; //是否完成,是否执行过此函数了,默认是false
//调用context.async this.async 可以把同步把异步,表示这个loader里的代码是异步的
context.async = function () {
isSync = false; //改为异步
return innerCallback;
}
const innerCallback = context.callback = function () {
isDone = true; //表示当前函数已经完成
isSync = false; //改为异步
callback.apply(null, arguments); //执行 callback
}
//第一次fn=pitch1,执行pitch1
let result = fn.apply(context, args);
//在执行pitch2的时候,还没有执行到pitch1 这行代码
if (isSync) {
isDone = true;
return callback(null, result);
}
}

function processResource(options, loaderContext, callback) {
//重置loaderIndex 改为loader长度减1
loaderContext.loaderIndex = loaderContext.loaders.length - 1;
let resourcePath = loaderContext.resourcePath;
//调用 fs.readFile方法读取资源内容
options.readResource(resourcePath, function (err, buffer) {
if (err) return callback(error);
options.resourceBuffer = buffer; //resourceBuffer放的是资源的原始内容
iterateNormalLoaders(options, loaderContext, [buffer], callback);
});
}

function iterateNormalLoaders(options, loaderContext, args, callback) {
//如果正常的normal loader全部执行完了
if (loaderContext.loaderIndex < 0) {
return callback(null, args);
}
let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
//如果说当这个normal已经执行过了,让索引减少1
if (currentLoaderObject.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, args, callback)
}
let normalFn = currentLoaderObject.normal;
currentLoaderObject.normalExecuted = true;
convertArgs(args, currentLoaderObject.raw);
runSyncOrAsync(normalFn, loaderContext, args, function (err) {
if (err) return callback(err);
let args = Array.prototype.slice.call(arguments, 1);
iterateNormalLoaders(options, loaderContext, args, callback);
});
}

function iteratePitchingLoaders(options, loaderContext, callback) {
// 所有的 pitch 处理完毕,开始获取源代码
if (loaderContext.loaderIndex >= loaderContext.loaders.length) {
return processResource(options, loaderContext, callback);
}
//获取当前的loader
const currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// 已经处理过pitch了,需要处理下一个loader的pitch
if (currentLoaderObject.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(options, loaderContext, callback)
}
// 加载laoder
loadLoader(currentLoaderObject);
let pitchFunction = currentLoaderObject.pitch;
currentLoaderObject.pitchExecuted = true;
if (!pitchFunction) {
return iteratePitchingLoaders(options, loaderContext, callback);
}

runSyncOrAsync(
pitchFunction, //要执行的pitch函数
loaderContext, //上下文对象
//这是要传递给pitchFunction的参数数组
[loaderContext.remainingRequest, loaderContext.previousRequest, loaderContext.data = {}],
//处理完成的回调
function (err, ...args) {
if (args.length > 0) { //如果args有值,说明这个pitch有返回值
loaderContext.loaderIndex--; //索引减1,开始回退了
iterateNormalLoaders(options, loaderContext, args, callback);
} else { //如果没有返回值,则执行下一个loader的pitch函数
iteratePitchingLoaders(options, loaderContext, callback)
}
}
);
};


exports.runLoaders = function (options, callback) {
createLoaderContext(options);
let processOptions = {
resourceBuffer: null, //最后我们会把loader执行的Buffer结果放在这里
readResource: options.readResource || readFile,
}
iteratePitchingLoaders(processOptions, loaderContext, function (err, result) {
if (err) {
return callback(err, {});
}
callback(null, {
result,
resourceBuffer: processOptions.resourceBuffer
});
});
};
复制代码

到此为止,我们已经了解了 loader 是什么,怎么在 webpack 中配置,loader 原理是怎么执行的,从 loader-runner 源码角度去进一步深入的了解。接下来是对 css-loader、style-loader 的简单实现。

开始编写 loader

下面是一些 loader简单实现:

css-loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
let postcss = require('postcss');
let loaderUtils = require('loader-utils');
let Tokenizer = require('css-selector-tokenizer');

function loader(cssString){
const cssPlugin = (options)=>{
return (cssRoot)=>{
//遍历语法树,找到所有的import语句
cssRoot.walkAtRules(/^import$/i,rule=>{
rule.remove();//删除 这个import
let imp = rule.params.slice(1,-1);
options.imports.push(imp);
});
cssRoot.walkDecls(decl=>{
let values = Tokenizer.parseValues(decl.value);
values.nodes.forEach(function(value){
value.nodes.forEach(item=>{
if(item.type === 'url'){
item.url = "`+require("+loaderUtils.stringifyRequest(this,item.url)+").default+`";
console.log('====item',item);
}
});
});
decl.value = Tokenizer.stringifyValues(values);
});
}
}
let callback = this.async();
let options = {imports:[]};//["./global.css"]
//源代码会经过流水线的一个个的插件
let pipeLine = postcss([cssPlugin(options)]);
pipeLine.process(cssString).then(result=>{
let importCSS = options.imports.map(url=>{
return "`+require("+loaderUtils.stringifyRequest(this,"!!css-loader2!"+url)+")+`";
}).join('\r\n');
let output = "module.exports = `"+importCSS+"\n"+result.css+"`";
output=output.replace(/\\"/g,'"');
callback(null,output);
});
}
module.exports = loader;
复制代码

style-loader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let loaderUtils = require('loader-utils');
function loader(source){

};

loader.pitch = function(remainingRequest,previousRequest,data) {
let script = `
let style = document.createElement('style');
style.innerHTML = require(${loaderUtils.stringifyRequest(this,"!!"+remainingRequest)});
document.head.appendChild(style);
`;
return script;
};

module.exports = loader;
复制代码

file-loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const path = require('path');
const { getOptions,interpolateName } = require('loader-utils');

function loader(content){
let options = getOptions(this)||{};
let filename = interpolateName(this, options.filename, {
content
});
this.emitFile(filename, content);
return `export default ${JSON.stringify(filename)}`;
}
//加载的二进制,处理的是 Buffer 类型数据
loader.raw = true;
module.exports = loader;
0%