想要更好的格式阅读体验,请查看原文:webpack 源码分析系列 ——loader

为什么需要 loader

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

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

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

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 是什么作用以及为什么需要 laoder 了。简单的来说,loader 就是处理module(模块、文件)的,能够将 module 处理成 webpack 能够解析的样子,同时还可以对解析的文件做一些再加工

接下来主要介绍在 webpack 中如何配置 loader;从宏观层面上聊一聊 loader 的工作原理是什么样的;同时带着一起实现一下 loader 中关键的模块 loader-runner。最后带领导大家一起手动编写上述讲到的 style-loader, css-loader, less-loader。

如何配置 loader

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

module.exports = {resolveLoader: {// 从根目录下那个文件中寻找 loadermodules: ['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']}]}
};

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

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

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 不要前置和普通 loader
! noAutoLoaders 不要普通 loader
!! noPrePostAutoLoaders 不要前后置和普通 loader,只要内联 loader

比如说对于如下:

import style from '-!style-loader!css-loader?modules!less-loader!./index.less'

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

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

loader 怎么工作

假设有如下文件和 rules:

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:

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 { // normalnormalLoaders.push(...rule.use);}}
}

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

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

loaders = [...postLoaders,...inlineLoaders,...normalLoaders,...preLoaders,];

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

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

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

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

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

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

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

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('!')) { // 不要 normalloaders = [...postLoaders,...inlineLoaders,...preLoaders,];
} else if(request.startsWith('-!')) { // 不要 normal、preloaders = [...postLoaders,...inlineLoaders];
} else if(request.startsWith('!!')) { // 不要 post、normal、preloaders = [...inlineLoaders,];
} else { // post、inline、normal、preloaders = [...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。如下图所示:

上一节已经介绍了 loaders 里面有一定顺序的 loader,然后会交给 laoder-runner 执行,有如下 post-loader1 代码所示,loader 可以添加一个 pitch 方法:

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 的入参。但是异步的会有一点点差别具体代码如下:

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 洋葱模型等都是差不多的,他们核心都是基于 职责链模式 这一设计模式的。具体关于职责链模式可以参考 https://www.yuque.com/wmaoshu/blog/rcf95o 对于职责链模式是使多个对象可以统一的处理,避免了请求方因为可能类型多种多样导致和接受处理方的耦合,为了保证请求可以经过多个接受按照规则处理,需要有一套机制将接收方行程一条链,然后请求方就会按照这个链路进行执行。所以职责链来说重要的是:一点为了保证请求的函数职责单一,需要具备通用性也就是函数签名和返回值应该保持一致,并且具备通知链条开始执行下一个的能力;二点为了保证请求的函数式开放封闭的,需要一个链条将这个过程串联起来。

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

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 的执行。

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 对象。

function parsePathQueryFragment(resource) { //resource =./src/index.js?name=wms#1let result = /^([^?#]*)(\?[^#]*)?(#.*)?$/.exec(resource);return {path: result[1], //路径名 ./src/index.jsquery: result[2], //   ?name=wmsfragment: 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, //是否是Bufferdata: {}, //自定义对象 每个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;// 资源路径中解析出的queryloaderContext.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.jsObject.defineProperty(loaderContext, 'request', {get() {return loaderContext.loaders.map(l => l.request).concat(loaderContext.resource).join('!')}});//剩下的loader从当前的下一个loader开始取,加上resourceObject.defineProperty(loaderContext, 'remainingRequest', {get() {return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(l => l.request).concat(loaderContext.resource).join('!')}});//当前loader从当前的loader开始取,加上resourceObject.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 代码:

function iteratePitchingLoaders(options, loaderContext, callback) {// 所有的 pitch 处理完毕,开始获取源代码if (loaderContext.loaderIndex >= loaderContext.loaders.length) {return processResource(options, loaderContext, callback);}//获取当前的loaderconst currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];// 已经处理过pitch了,需要处理下一个loader的pitchif (currentLoaderObject.pitchExecuted) {loaderContext.loaderIndex++;return iteratePitchingLoaders(options, loaderContext, callback)}// 加载laoderloadLoader(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 主要是

function processResource(options, loaderContext, callback) {//重置loaderIndex 改为loader长度减1loaderContext.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 函数:

function iterateNormalLoaders(options, loaderContext, args, callback) {//如果正常的normal loader全部执行完了if (loaderContext.loaderIndex < 0) {return callback(null, args);}let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];//如果说当这个normal已经执行过了,让索引减少1if (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。

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,执行pitch1let 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 的完整的代码:

const fs = require('fs');
const path = require('path');
const readFile = fs.readFile.bind(fs);function parsePathQueryFragment(resource) { //resource =./src/index.js?name=wms#1let result = /^([^?#]*)(\?[^#]*)?(#.*)?$/.exec(resource);return {path: result[1], //路径名 ./src/index.jsquery: result[2], //   ?name=wmsfragment: 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, //是否是Bufferdata: {}, //自定义对象 每个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;// 资源路径中解析出的queryloaderContext.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.jsObject.defineProperty(loaderContext, 'request', {get() {return loaderContext.loaders.map(l => l.request).concat(loaderContext.resource).join('!')}});//剩下的loader从当前的下一个loader开始取,加上resourceObject.defineProperty(loaderContext, 'remainingRequest', {get() {return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(l => l.request).concat(loaderContext.resource).join('!')}});//当前loader从当前的loader开始取,加上resourceObject.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]不是, 需要转成bufferif (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,执行pitch1let result = fn.apply(context, args);//在执行pitch2的时候,还没有执行到pitch1 这行代码if (isSync) {isDone = true;return callback(null, result);}
}function processResource(options, loaderContext, callback) {//重置loaderIndex 改为loader长度减1loaderContext.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已经执行过了,让索引减少1if (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);}//获取当前的loaderconst currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];// 已经处理过pitch了,需要处理下一个loader的pitchif (currentLoaderObject.pitchExecuted) {loaderContext.loaderIndex++;return iteratePitchingLoaders(options, loaderContext, callback)}// 加载laoderloadLoader(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

下面是 css-loader 的简单实现:

let postcss = require('postcss');
let loaderUtils  = require('loader-utils');
let Tokenizer = require('css-selector-tokenizer');
/*** postcss是用来处理CSS的,也是基于CSS抽象语法树的*/
function loader(cssString){const cssPlugin = (options)=>{return (cssRoot)=>{//遍历语法树,找到所有的import语句cssRoot.walkAtRules(/^import$/i,rule=>{rule.remove();//删除 这个importlet 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:

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;

webpack 源码分析系列 ——loader相关推荐

  1. dubbo源码分析系列(1)扩展机制的实现

    1 系列目录 dubbo源码分析系列(1)扩展机制的实现 dubbo源码分析系列(2)服务的发布 dubbo源码分析系列(3)服务的引用 dubbo源码分析系列(4)dubbo通信设计 2 SPI扩展 ...

  2. vue源码分析系列二:$mount()和new Watcher()的执行过程

    续vue源码分析系列一:new Vue的初始化过程 在initMixin()里面调用了$mount() if (vm.$options.el) {vm.$mount(vm.$options.el);/ ...

  3. jQuery源码分析系列

    声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://git ...

  4. MyBatis 源码分析系列文章合集

    1.简介 我从七月份开始阅读MyBatis源码,并在随后的40天内陆续更新了7篇文章.起初,我只是打算通过博客的形式进行分享.但在写作的过程中,发现要分析的代码太多,以至于文章篇幅特别大.在这7篇文章 ...

  5. MyBatis 源码分析系列文章导读

    1.本文速览 本篇文章是我为接下来的 MyBatis 源码分析系列文章写的一个导读文章.本篇文章从 MyBatis 是什么(what),为什么要使用(why),以及如何使用(how)等三个角度进行了说 ...

  6. Spring IOC 容器源码分析系列文章导读

    1. 简介 前一段时间,我学习了 Spring IOC 容器方面的源码,并写了数篇文章对此进行讲解.在写完 Spring IOC 容器源码分析系列文章中的最后一篇后,没敢懈怠,趁热打铁,花了3天时间阅 ...

  7. Spring IOC 容器源码分析系列文章导读 1

    1. 简介 Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本.经过十几年的迭代,现在的 Spring 框架已经非常成熟了.Spring ...

  8. idea 线程内存_Java线程池系列之-Java线程池底层源码分析系列(一)

    课程简介: 课程目标:通过本课程学习,深入理解Java线程池,提升自身技术能力与价值. 适用人群:具有Java多线程基础的人群,希望深入理解线程池底层原理的人群. 课程概述:多线程的异步执行方式,虽然 ...

  9. [转]jQuery源码分析系列

    文章转自:jQuery源码分析系列-Aaron 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://github.com/JsAaro ...

最新文章

  1. Python爬一下抖音上小姐姐的视频~
  2. 自动化运维工具----ansible
  3. java基础 多维数组_java基础:3.2 多维数组
  4. 如何用一句话证明你是程序员?41 个答案揭晓!
  5. 框架学习之Spring 第五节 SSH整合开发[Spring2.5+Hibernate3.3+Struts2]
  6. Lua和C语言的交互——C API
  7. sqoop2从hdfs导入mysql_sqoop2相关实例:hdfs和mysql互相导入(转)
  8. SSH框架总结(框架分析+环境搭建+实例源代码下载)
  9. canal client leader
  10. 通过MVC模式将Web视图和逻辑代码分离
  11. GTK+图形化应用程序开发学习笔记(五)—组装盒、组合表、固定容器构件
  12. 攻防世界web新手随记
  13. Luogu3825[NOI2017] 游戏
  14. ubuntu系统,网页版音乐播放器无声音
  15. 喜大普奔:史上最全的报表模板库免费用!
  16. C语言——输出一个正方形
  17. Linux网络知识--DHCP服务(理论部分)
  18. greenplum查询表结构java_Greenplum小把戏 - 几个常用数据库对象大小查询SQL
  19. 02 食尚年华石锅土鲫鱼需求简单描述
  20. html5支付成功页面,订单完成页面视图《 微信支付:H5 移动端支付 》

热门文章

  1. 六一儿童节到了,祝福普天下的宝宝节日快乐。
  2. 内存泄露(十)-- KOOM(高性能线上内存监控方案)
  3. 毅硕科技携手Sentieon独家赞助第21届亚太生物信息学大会(APBC 2023)
  4. BitBlt实现透明背景贴图
  5. Android应用性能优化!Android架构师必备框架技能核心笔记,一文轻松搞定
  6. 提高PPT操作APM的小技巧
  7. 赋予图片以png结尾的网址
  8. Android开发教程PDF,安卓开发教程PDF免费版
  9. 梦幻鸿蒙市质检,《梦幻西游》手游第十七届武神坛开辟鸿蒙VS斜月三星_《梦幻西游》手游官网-人人都玩,无处不在...
  10. C++使用eigen库做本征分解(eigendecomposition)