限时领|《AI 百问百答》专栏课+实体书(包邮)! 了解详情
写点什么

webpack 系列之四 loader 详解 2

  • 2019-09-19
  • 本文字数:7843 字

    阅读完需:约 26 分钟

webpack系列之四loader详解2

上篇文章主要讲了 loader 的配置,匹配相关的机制。这篇主要会讲当一个 module 被创建之后,使用 loader 去处理这个 module 内容的流程机制。


GitHub: github. com/CommanderXL


首先我们来总体的看下整个的流程:



在 module 一开始构建的过程中,首先会创建一个 loaderContext 对象,它和这个 module 是一一对应的关系,而这个 module 所使用的所有 loaders 都会共享这个 loaderContext 对象,每个 loader 执行的时候上下文就是这个 loaderContext 对象,所以可以在我们写的 loader 里面通过 this 来访问。


// NormalModule.js
const { runLoaders } = require('loader-runner')
class NormalModule extends Module { ... createLoaderContext(resolver, options, compilation, fs) { const requestShortener = compilation.runtimeTemplate.requestShortener; // 初始化 loaderContext 对象,这些初始字段的具体内容解释在文档上有具体的解释(https://webpack.docschina.org/api/loaders/#this-data) const loaderContext = { version: 2, emitWarning: warning => {...}, emitError: error => {...}, exec: (code, filename) => {...}, resolve(context, request, callback) {...}, getResolve(options) {...}, emitFile: (name, content, sourceMap) => {...}, rootContext: options.context, // 项目的根路径 webpack: true, sourceMap: !!this.useSourceMap, _module: this, _compilation: compilation, _compiler: compilation.compiler, fs: fs };
// 触发 normalModuleLoader 的钩子函数,开发者可以利用这个钩子来对 loaderContext 进行拓展 compilation.hooks.normalModuleLoader.call(loaderContext, this); if (options.loader) { Object.assign(loaderContext, options.loader); }
return loaderContext; }
doBuild(options, compilation, resolver, fs, callback) { // 创建 loaderContext 上下文 const loaderContext = this.createLoaderContext( resolver, options, compilation, fs ) runLoaders( { resource: this.resource, // 这个模块的路径 loaders: this.loaders, // 模块所使用的 loaders context: loaderContext, // loaderContext 上下文 readResource: fs.readFile.bind(fs) // 读取文件的 node api }, (err, result) => { // do something } ) } ...}
复制代码


当 loaderContext 初始化完成后,开始调用 runLoaders 方法,这个时候进入到了 loaders 的执行阶段。runLoaders 方法是由 loader-runner 这个独立的 NPM 包提供的方法,那我们就一起来看下 runLoaders 方法内部是如何运行的。


首先根据传入的参数完成进一步的处理,同时对于 loaderContext 对象上的属性做进一步的拓展:


exports.runLoaders = function runLoaders(options, callback) {  // read options  var resource = options.resource || ""; // 模块的路径  var loaders = options.loaders || []; // 模块所需要使用的 loaders  var loaderContext = options.context || {}; // 在 normalModule 里面创建的 loaderContext  var readResource = options.readResource || readFile;
var splittedResource = resource && splitQuery(resource); var resourcePath = splittedResource ? splittedResource[0] : undefined; // 模块实际路径 var resourceQuery = splittedResource ? splittedResource[1] : undefined; // 模块路径 query 参数 var contextDirectory = resourcePath ? dirname(resourcePath) : null; // 模块的父路径
// execution state var requestCacheable = true; var fileDependencies = []; var contextDependencies = [];
// prepare loader objects loaders = loaders.map(createLoaderObject); // 处理 loaders
// 拓展 loaderContext 的属性 loaderContext.context = contextDirectory; loaderContext.loaderIndex = 0; // 当前正在执行的 loader 索引 loaderContext.loaders = loaders; loaderContext.resourcePath = resourcePath; loaderContext.resourceQuery = resourceQuery; loaderContext.async = null; // 异步 loader loaderContext.callback = null;
...
// 需要被构建的模块路径,将 loaderContext.resource -> getter/setter // 例如 /abc/resource.js?rrr Object.defineProperty(loaderContext, "resource", { enumerable: true, get: function() { if(loaderContext.resourcePath === undefined) return undefined; return loaderContext.resourcePath + loaderContext.resourceQuery; }, set: function(value) { var splittedResource = value && splitQuery(value); loaderContext.resourcePath = splittedResource ? splittedResource[0] : undefined; loaderContext.resourceQuery = splittedResource ? splittedResource[1] : undefined; } });
// 构建这个 module 所有的 loader 及这个模块的 resouce 所组成的 request 字符串 // 例如:/abc/loader1.js?xyz!/abc/node_modules/loader2/index.js!/abc/resource.js?rrr Object.defineProperty(loaderContext, "request", { enumerable: true, get: function() { return loaderContext.loaders.map(function(o) { return o.request; }).concat(loaderContext.resource || "").join("!"); } }); // 在执行 loader 提供的 pitch 函数阶段传入的参数之一,剩下还未被调用的 loader.pitch 所组成的 request 字符串 Object.defineProperty(loaderContext, "remainingRequest", { enumerable: true, get: function() { if(loaderContext.loaderIndex >= loaderContext.loaders.length - 1 && !loaderContext.resource) return ""; return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(function(o) { return o.request; }).concat(loaderContext.resource || "").join("!"); } }); // 在执行 loader 提供的 pitch 函数阶段传入的参数之一,包含当前 loader.pitch 所组成的 request 字符串 Object.defineProperty(loaderContext, "currentRequest", { enumerable: true, get: function() { return loaderContext.loaders.slice(loaderContext.loaderIndex).map(function(o) { return o.request; }).concat(loaderContext.resource || "").join("!"); } }); // 在执行 loader 提供的 pitch 函数阶段传入的参数之一,包含已经被执行的 loader.pitch 所组成的 request 字符串 Object.defineProperty(loaderContext, "previousRequest", { enumerable: true, get: function() { return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(function(o) { return o.request; }).join("!"); } }); // 获取当前正在执行的 loader 的query参数 // 如果这个 loader 配置了 options 对象的话,this.query 就指向这个 option 对象 // 如果 loader 中没有 options,而是以 query 字符串作为参数调用时,this.query 就是一个以 ? 开头的字符串 Object.defineProperty(loaderContext, "query", { enumerable: true, get: function() { var entry = loaderContext.loaders[loaderContext.loaderIndex]; return entry.options && typeof entry.options === "object" ? entry.options : entry.query; } }); // 每个 loader 在 pitch 阶段和正常执行阶段都可以共享的 data 数据 Object.defineProperty(loaderContext, "data", { enumerable: true, get: function() { return loaderContext.loaders[loaderContext.loaderIndex].data; } }); var processOptions = { resourceBuffer: null, // module 的内容 buffer readResource: readResource }; // 开始执行每个 loader 上的 pitch 函数 iteratePitchingLoaders(processOptions, loaderContext, function(err, result) { // do something... });}
复制代码


这里稍微总结下就是在 runLoaders 方法的初期会对相关参数进行初始化的操作,特别是将 loaderContext 上的部分属性改写为 getter/setter 函数,这样在不同的 loader 执行的阶段可以动态的获取一些参数。


接下来开始调用 iteratePitchingLoaders 方法执行每个 loader 上提供的 pitch 函数。大家写过 loader 的话应该都清楚,每个 loader 可以挂载一个 pitch 函数,每个 loader 提供的 pitch 方法和 loader 实际的执行顺序正好相反。这块的内容在 webpack 文档上也有详细的说明(请戳我)。


这些 pitch 函数并不是用来实际处理 module 的内容的,主要是可以利用 module 的 request,来做一些拦截处理的工作,从而达到在 loader 处理流程当中的一些定制化的处理需要,有关 pitch 函数具体的实战可以参见下一篇文档:


function iteratePitchingLoaders() {  // abort after last loader  if(loaderContext.loaderIndex >= loaderContext.loaders.length)    return processResource(options, loaderContext, callback);
// 根据 loaderIndex 来获取当前需要执行的 loader var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// iterate // 如果被执行过,那么直接跳过这个 loader 的 pitch 函数 if(currentLoaderObject.pitchExecuted) { loaderContext.loaderIndex++; return iteratePitchingLoaders(options, loaderContext, callback); }
// 加载 loader 模块 // load loader module loadLoader(currentLoaderObject, function(err) { // do something ... });}
复制代码


每次执行 pitch 函数前,首先根据 loaderIndex 来获取当前需要执行的 loader (currentLoaderObject),调用 loadLoader 函数来加载这个 loader,loadLoader 内部兼容了 SystemJS,ES Module,CommonJs 这些模块定义,最终会将 loader 提供的 pitch 方法和普通方法赋值到 currentLoaderObject 上:


// loadLoader.jsmodule.exports = function (loader, callback) {  ...  var module = require(loader.path)   ...  loader.normal = module
loader.pitch = module.pitch
loader.raw = module.raw
callback() ...}
复制代码


当 loader 加载完后,开始执行 loadLoader 的回调:


loadLoader(currentLoaderObject, function(err) {  var fn = currentLoaderObject.pitch; // 获取 pitch 函数  currentLoaderObject.pitchExecuted = true;  if(!fn) return iteratePitchingLoaders(options, loaderContext, callback); // 如果这个 loader 没有提供 pitch 函数,那么直接跳过
// 开始执行 pitch 函数 runSyncOrAsync( fn, loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}], function(err) { if(err) return callback(err); var args = Array.prototype.slice.call(arguments, 1); // Determine whether to continue the pitching process based on // argument values (as opposed to argument presence) in order // to support synchronous and asynchronous usages. // 根据是否有参数返回来判断是否向下继续进行 pitch 函数的执行 var hasArg = args.some(function(value) { return value !== undefined; }); if(hasArg) { loaderContext.loaderIndex--; iterateNormalLoaders(options, loaderContext, args, callback); } else { iteratePitchingLoaders(options, loaderContext, callback); } } );})
复制代码


这里出现了一个 runSyncOrAsync 方法,放到后文去讲,开始执行 pitch 函数,当 pitch 函数执行完后,执行传入的回调函数。我们看到回调函数里面会判断接收到的参数的个数,除了第一个 err 参数外,如果还有其他的参数(这些参数是 pitch 函数执行完后传入回调函数的),那么会直接进入 loader 的 normal 方法执行阶段,并且会直接跳过后面的 loader 执行阶段。如果 pitch 函数没有返回值的话,那么进入到下一个 loader 的 pitch 函数的执行阶段。让我们再回到 iteratePitchingLoaders 方法内部,当所有 loader 上面的 pitch 函数都执行完后,即 loaderIndex 索引值 >= loader 数组长度的时候:


function iteratePitchingLoaders () {  ...
if(loaderContext.loaderIndex >= loaderContext.loaders.length) return processResource(options, loaderContext, callback);
...}
function processResource(options, loaderContext, callback) {// set loader index to last loader loaderContext.loaderIndex = loaderContext.loaders.length - 1;
var resourcePath = loaderContext.resourcePath;if(resourcePath) { loaderContext.addDependency(resourcePath); // 添加依赖 options.readResource(resourcePath, function(err, buffer) {if(err) return callback(err); options.resourceBuffer = buffer; iterateNormalLoaders(options, loaderContext, [buffer], callback); }); } else { iterateNormalLoaders(options, loaderContext, [null], callback); }}
复制代码


在 processResouce 方法内部调用 node API readResouce 读取 module 对应路径的文本内容,调用 iterateNormalLoaders 方法,开始进入 loader normal 方法的执行阶段。


function iterateNormalLoaders () {  if(loaderContext.loaderIndex < 0)    return callback(null, args);
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// iterate if(currentLoaderObject.normalExecuted) { loaderContext.loaderIndex--; return iterateNormalLoaders(options, loaderContext, args, callback); }
var fn = currentLoaderObject.normal; currentLoaderObject.normalExecuted = true; if(!fn) { return iterateNormalLoaders(options, loaderContext, args, callback); }
// buffer 和 utf8 string 之间的转化 convertArgs(args, currentLoaderObject.raw);
runSyncOrAsync(fn, loaderContext, args, function(err) { if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1); iterateNormalLoaders(options, loaderContext, args, callback); });}
复制代码


在 iterateNormalLoaders 方法内部就是依照从右到左的顺序(正好与 pitch 方法执行顺序相反)依次执行每个 loader 上的 normal 方法。loader 不管是 pitch 方法还是 normal 方法的执行可为同步的,也可设为异步的。这里说下 normal 方法的,一般如果你写的 loader 里面可能涉及到计算量较大的情况时,可将你的 loader 异步化,在你 loader 方法里面调用 this.async 方法,返回异步的回调函数,当你 loader 内部实际的内容执行完后,可调用这个异步的回调来进入下一个 loader 的执行。


module.exports = function (content) {  const callback = this.async()  someAsyncOperation(content, function(err, result) {    if (err) return callback(err);    callback(null, result);  });}
复制代码


除了调用 this.async 来异步化 loader 之外,还有一种方式就是在你的 loader 里面去返回一个 promise,只有当这个 promise 被 resolve 之后,才会调用下一个 loader(具体实现机制见下文):


module.exports = function (content) {  return new Promise(resolve => {    someAsyncOpertion(content, function(err, result) {      if (err) resolve(err)      resolve(null, result)    })  })}
复制代码


这里还有一个地方需要注意的就是,上下游 loader 之间的数据传递过程中,如果下游的 loader 接收到的参数为一个,那么可以在上一个 loader 执行结束后,如果是同步就直接 return 出去:


module.exports = function (content) {  // do something  return content}
复制代码


如果是异步就直接调用异步回调传递下去(参见上面 loader 异步化)。如果下游 loader 接收的参数多于一个,那么上一个 loader 执行结束后,如果是同步那么就需要调用 loaderContext 提供的 callback 函数:


module.exports = function (content) {  // do something  this.callback(null, content, argA, argB)}
复制代码


如果是异步的还是继续调用异步回调函数传递下去(参见上面 loader 异步化)。具体的执行机制涉及到上文还没讲到的 runSyncOrAsync 方法,它提供了上下游 loader 调用的接口:


function runSyncOrAsync(fn, context, args, callback) {  var isSync = true; // 是否为同步  var isDone = false;  var isError = false; // internal error  var reportedError = false;  // 给 loaderContext 上下文赋值 async 函数,用以将 loader 异步化,并返回异步回调  context.async = function async() {    if(isDone) {      if(reportedError) return; // ignore      throw new Error("async(): The callback was already called.");    }    isSync = false; // 同步标志位置为 false    return innerCallback;  };  // callback 的形式可以向下一个 loader 多个参数  var innerCallback = context.callback = function() {    if(isDone) {      if(reportedError) return; // ignore      throw new Error("callback(): The callback was already called.");    }    isDone = true;    isSync = false;    try {      callback.apply(null, arguments);    } catch(e) {      isError = true;      throw e;    }  };  try {    // 开始执行 loader    var result = (function LOADER_EXECUTION() {      return fn.apply(context, args);    }());    // 如果为同步的执行    if(isSync) {      isDone = true;      // 如果 loader 执行后没有返回值,执行 callback 开始下一个 loader 执行      if(result === undefined)        return callback();      // loader 返回值为一个 promise 实例,待这个实例被resolve或者reject后执行下一个 loader。这也是 loader 异步化的一种方式      if(result && typeof result === "object" && typeof result.then === "function") {        return result.catch(callback).then(function(r) {          callback(null, r);        });      }      // 如果 loader 执行后有返回值,执行 callback 开始下一个 loader 执行      return callback(null, result);    }  } catch(e) {    // do something  }}
复制代码


以上就是对于 module 在构建过程中 loader 执行流程的源码分析。可能平时在使用 webpack 过程了解相关的 loader 执行规则和策略,再配合这篇对于内部机制的分析,应该会对 webpack loader 的使用有更加深刻的印象。


本文转载自公众号滴滴技术(ID:didi_tech)。


原文链接:


https://mp.weixin.qq.com/s/v8i02dhb7yRnfQbgcaR4QQ


2019-09-19 16:121016

评论

发布
暂无评论
发现更多内容

【原生Ajax】全面了解xhr的概念与使用。

坚毅的小解同志

ajax 11月月更

SAP UI5 应用和 Angular 应用视图里控件 id 生成逻辑的异同比较

汪子熙

前端开发 angular SAP SAP UI5 11月月更

小令观点 | 从大批QQ账号被盗,看账号安全与数据资产问题

令牌云数字身份

数据安全 账号安全

字节跳动基于数据湖技术的近实时场景实践

字节跳动数据平台

数据湖 火山引擎

HashData携手恒丰银行 入选信通院金融科技创新应用五大“最受关注案例”

酷克数据HashData

云数据仓库

MySQL中支持的字符集和排序规则

@下一站

MySQL 技术 字符集 11月月更

百度APP iOS端内存优化实践-内存管控方案

百度Geek说

android 后端 内存管理 企业号十月 PK 榜

华为云虚拟专用网络VPN,为企业铺就数据上云的安全路

路过的憨憨

DevOps制品管理——软件“工业革命”的里程碑式改革

嘉为蓝鲸

DevOps 制品管理

新力量,新希望|明道云伙伴大会2022秋圆满落幕

明道云

AI机器学习模型部署的典型策略

Baihai IDP

人工智能 AI MLOps 模型部署

“天翼云杯”厦门软件开发大赛开赛 为开发者提供“沃土”

极客天地

嘉为科技彭一宽:组织度量,先做造钟人,再做报时人

嘉为蓝鲸

DevOps 度量

新版阿里神级“高并发”教程《基础+实战+源码+面试+架构》

钟奕礼

Java java面试 java编程 程序员、

【jquery ajax】实现文件上传提交

坚毅的小解同志

ajax 11月月更

数维图智慧化工园区三维系统 助力危化企业安全生产

2D3D前端可视化开发

物联网 数据可视化 数字孪生 智慧化工园区 化工园区三维可视化

一周活动速递|Paper Time第五期;技术征文大赛即将收官

OceanBase 数据库

热门资讯:超大规模数字产业生态正在加快构建!

优秀

数字化转型

Mybatis中使用${}和使用#{}

@下一站

mybatis MyBatis标签 Java core 11月月更

云计算:基于互联网的超级计算

Finovy Cloud

云计算 云渲染

4.0体验站|OceanBase 4.0 我回来给你点个赞

OceanBase 数据库

小令观点丨现代版 “见令如见人”

令牌云数字身份

小令动态 | 令牌云成功通过国家等保三级认证

令牌云数字身份

SpringBoot邮件发送demo

@下一站

编程 email Java core 11月月更

一步一图带你深入理解 Linux 物理内存管理

bin的技术小屋

内存 内存管理 Linux Kenel 内核 内核源码

LR低代码快速开发平台 高效调整企业组织架构

力软低代码开发平台

小令动态 | 令牌云新获上海市创新资金立项支持,此前还有......

令牌云数字身份

python常用的内置对象

乔乔

11月月更

JS有哪些变态语法,你知道吗?

千锋IT教育

行业分析| 智慧消防对讲

anyRTC开发者

人工智能 监控 消防 调度 快对讲

京东二面:MySQL 主从延迟,读写分离 7 种解决方案

钟奕礼

Java 程序员 java面试 java编程

webpack系列之四loader详解2_文化 & 方法_肖磊_InfoQ精选文章