Webpack 是一个模块化打包工具,它被普遍地应用在前端领域的大多数项目中。利用 Webpack 咱们不只能够打包 JS 文件,还能够打包图片、CSS、字体等其余类型的资源文件。而支持打包非 JS 文件的特性是基于 Loader 机制来实现的。所以要学好 Webpack,咱们就须要掌握 Loader 机制。本文阿宝哥将带你们一块儿深刻学习 Webpack 的 Loader 机制,阅读完本文你将了解如下内容:javascript
- Loader 的本质是什么?
- Normal Loader 和 Pitching Loader 是什么?
- Pitching Loader 的做用是什么?
- Loader 是如何被加载的?
- Loader 是如何被运行的?
- 多个 Loader 的执行顺序是什么?
- Pitching Loader 的熔断机制是如何实现的?
- Normal Loader 函数是如何被运行的?
- Loader 对象上
raw
属性有什么做用? - Loader 函数体中的
this.callback
和this.async
方法是哪里来的? - Loader 最终的返回结果是如何被处理的?
1、Loader 的本质是什么?
由上图可知,Loader 本质上是导出函数的 JavaScript 模块。所导出的函数,可用于实现内容转换,该函数支持如下 3 个参数:css
/** * @param {string|Buffer} content 源文件的内容 * @param {object} [map] 能够被 https://github.com/mozilla/source-map 使用的 SourceMap 数据 * @param {any} [meta] meta 数据,能够是任何内容 */ function webpackLoader(content, map, meta) { // 你的webpack loader代码 } module.exports = webpackLoader; 复制代码
了解完导出函数的签名以后,咱们就能够定义一个简单的 simpleLoader
:html
function simpleLoader(content, map, meta) { console.log("我是 SimpleLoader"); return content; } module.exports = simpleLoader; 复制代码
以上的 simpleLoader
并不会对输入的内容进行任何处理,只是在该 Loader 执行时输出相应的信息。Webpack 容许用户为某些资源文件配置多个不一样的 Loader,好比在处理 .css
文件的时候,咱们用到了 style-loader
和 css-loader
,具体配置方式以下所示:前端
webpack.config.jsjava
const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, module: { rules: [ { test: /\.css$/i, use: ['style-loader', 'css-loader'], }, ], }, }; 复制代码
Webpack 这样设计的好处,是能够保证每一个 Loader 的职责单一。同时,也方便后期 Loader 的组合和扩展。好比,你想让 Webpack 可以处理 Scss 文件,你只需先安装 sass-loader
,而后在配置 Scss 文件的处理规则时,设置 rule 对象的 use
属性为 ['style-loader', 'css-loader', 'sass-loader']
便可。node
2、Normal Loader 和 Pitching Loader 是什么?
2.1 Normal Loader
Loader 本质上是导出函数的 JavaScript 模块,而该模块导出的函数(如果 ES6 模块,则是默认导出的函数)就被称为 Normal Loader。须要注意的是,这里咱们介绍的 Normal Loader 与 Webpack Loader 分类中定义的 Loader 是不同的。在 Webpack 中,loader 能够被分为 4 类:pre 前置、post 后置、normal 普通和 inline 行内。其中 pre 和 post loader,能够经过 rule
对象的 enforce
属性来指定:webpack
// webpack.config.js const path = require("path"); module.exports = { module: { rules: [ { test: /\.txt$/i, use: ["a-loader"], enforce: "post", // post loader }, { test: /\.txt$/i, use: ["b-loader"], // normal loader }, { test: /\.txt$/i, use: ["c-loader"], enforce: "pre", // pre loader }, ], }, }; 复制代码
了解完 Normal Loader 的概念以后,咱们来动手写一下 Normal Loader。首先咱们先来建立一个新的目录:git
$ mkdir webpack-loader-demo 复制代码
而后进入该目录,使用 npm init -y
命令执行初始化操做。该命令成功执行后,会在当前目录生成一个 package.json
文件:es6
{ "name": "webpack-loader-demo", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" } 复制代码
提示:本地所使用的开发环境:Node v12.16.2;Npm 6.14.4;github
接着咱们使用如下命令,安装一下 webpack
和 webpack-cli
依赖包:
$ npm i webpack webpack-cli -D 复制代码
安装完项目依赖后,咱们根据如下目录结构来添加对应的目录和文件:
├── dist # 打包输出目录
│ └── index.html
├── loaders # loaders文件夹
│ ├── 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配置文件
复制代码
dist/index.html
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Webpack Loader 示例</title> </head> <body> <h3>Webpack Loader 示例</h3> <p id="message"></p> <script src="./bundle.js"></script> </body> </html> 复制代码
src/index.js
import Data from "./data.txt" const msgElement = document.querySelector("#message"); msgElement.innerText = Data; 复制代码
src/data.txt
你们好,我是阿宝哥
复制代码
loaders/a-loader.js
function aLoader(content, map, meta) { console.log("开始执行aLoader Normal Loader"); content += "aLoader]"; return `module.exports = '${content}'`; } module.exports = aLoader; 复制代码
在 aLoader
函数中,咱们会对 content
内容进行修改,而后返回 module.exports = '${content}'
字符串。那么为何要把 content
赋值给 module.exports
属性呢?这里咱们先不解释具体的缘由,后面咱们再来分析这个问题。
loaders/b-loader.js
function bLoader(content, map, meta) { console.log("开始执行bLoader Normal Loader"); return content + "bLoader->"; } module.exports = bLoader; 复制代码
loaders/c-loader.js
function cLoader(content, map, meta) { console.log("开始执行cLoader Normal Loader"); return content + "[cLoader->"; } module.exports = cLoader; 复制代码
在 loaders 目录下,咱们定义了以上 3 个 Normal Loader。这些 Loader 的实现都比较简单,只是在 Loader 执行时往 content
参数上添加当前 Loader 的相关信息。为了让 Webpack 可以识别 loaders 目录下的自定义 Loader,咱们还须要在 Webpack 的配置文件中,设置 resolveLoader
属性,具体的配置方式以下所示:
webpack.config.js
const path = require("path"); module.exports = { entry: "./src/index.js", output: { filename: "bundle.js", path: path.resolve(__dirname, "dist"), }, mode: "development", module: { rules: [ { test: /\.txt$/i, use: ["a-loader", "b-loader", "c-loader"], }, ], }, resolveLoader: { modules: [ 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 [emitted] (name: main)
runtime modules 937 bytes 4 modules
cacheable modules 187 bytes
./src/index.js 114 bytes [built] [code generated]
./src/data.txt 73 bytes [built] [code generated]
webpack 5.45.1 compiled successfully in 99 ms
复制代码
经过观察以上的输出结果,咱们能够知道 Normal Loader 的执行顺序是从右到左。此外,当打包完成后,咱们在浏览器中打开 dist/index.html 文件,在页面上你将看到如下信息:
Webpack Loader 示例
你们好,我是阿宝哥[cLoader->bLoader->aLoader]
复制代码
由页面上的输出信息 ”你们好,我是阿宝哥[cLoader->bLoader->aLoader]“ 可知,Loader 在执行的过程当中是以管道的形式,对数据进行处理,具体处理过程以下图所示:
如今你已经知道什么是 Normal Loader 及 Normal Loader 的执行顺序,接下来咱们来介绍另外一种 Loader —— Pitching Loader。
2.2 Pitching Loader
在开发 Loader 时,咱们能够在导出的函数上添加一个 pitch
属性,它的值也是一个函数。该函数被称为 Pitching Loader,它支持 3 个参数:
/** * @remainingRequest 剩余请求 * @precedingRequest 前置请求 * @data 数据对象 */ function (remainingRequest, precedingRequest, data) { // some code }; 复制代码
其中 data
参数,能够用于数据传递。即在 pitch
函数中往 data
对象上添加数据,以后在 normal
函数中经过 this.data
的方式读取已添加的数据。 而 remainingRequest
和 precedingRequest
参数究竟是什么?这里咱们先来更新一下 a-loader.js
文件:
function aLoader(content, map, meta) { // 省略部分代码 } aLoader.pitch = function (remainingRequest, precedingRequest, data) { console.log("开始执行aLoader Pitching Loader"); console.log(remainingRequest, precedingRequest, data) }; module.exports = aLoader; 复制代码
在以上代码中,咱们为 aLoader 函数增长了一个 pitch
属性并设置它的值为一个函数对象。在函数体中,咱们输出了该函数所接收的参数。接着,咱们以一样的方式更新 b-loader.js
和 c-loader.js
文件:
b-loader.js
function bLoader(content, map, meta) { // 省略部分代码 } bLoader.pitch = function (remainingRequest, precedingRequest, data) { console.log("开始执行bLoader Pitching Loader"); console.log(remainingRequest, precedingRequest, data); }; module.exports = bLoader; 复制代码
c-loader.js
function cLoader(content, map, meta) { // 省略部分代码 } cLoader.pitch = function (remainingRequest, precedingRequest, data) { console.log("开始执行cLoader Pitching Loader"); console.log(remainingRequest, precedingRequest, data); }; module.exports = cLoader; 复制代码
当全部文件都更新完成后,咱们在 webpack-loader-demo 项目的根目录再次执行 npx webpack
命令后,就会输出相应的信息。这里咱们以 b-loader.js
的 pitch
函数的输出结果为例,来分析一下 remainingRequest
和 precedingRequest
参数的输出结果:
/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 这个库来运行已配置的 loaders。
看到这里有的小伙伴可能会有疑问,Pitching Loader 除了能够提早运行以外,还有什么做用呢?其实当某个 Pitching Loader 返回非 undefined
值时,就会实现熔断效果。这里咱们更新一下 bLoader.pitch
方法,让它返回 "bLoader Pitching Loader->"
字符串:
bLoader.pitch = function (remainingRequest, precedingRequest, data) { console.log("开始执行bLoader Pitching Loader"); return "bLoader Pitching Loader->"; }; 复制代码
当更新完 bLoader.pitch
方法,咱们再次执行 npx webpack
命令以后,控制台会输出如下内容:
开始执行aLoader Pitching Loader
开始执行bLoader Pitching Loader
开始执行aLoader Normal Loader
asset bundle.js 4.53 KiB [compared for emit] (name: main)
runtime modules 937 bytes 4 modules
...
复制代码
由以上输出结果可知,当 bLoader.pitch
方法返回非 undefined
值时,跳过了剩下的 loader。具体执行流程以下图所示:
提示:Webpack 内部会使用 loader-runner 这个库来运行已配置的 loaders。
以后,咱们在浏览器中再次打开 dist/index.html 文件。此时,在页面上你将看到如下信息:
Webpack Loader 示例
bLoader Pitching 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": ["run", "debug"], "port": 5858 }] } 复制代码
利用以上配置信息,咱们建立了一个 Webpack Debug 的调试任务。当运行该任务的时候,会在当前工做目录下执行 npm run debug
命令。所以,接下来咱们须要在 package.json 文件中增长 debug 命令,具体内容以下所示:
// 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 = function runLoaders(options, callback) { // read options var resource = options.resource || ""; var loaders = options.loaders || []; var loaderContext = options.context || {}; // Loader上下文对象 var processResource = options.processResource || ((readResource, context, resource, callback) => { context.addDependency(resource); readResource(resource, callback); }).bind(null, options.readResource || readFile); // prepare loader objects loaders = loaders.map(createLoaderObject); loaderContext.context = contextDirectory; loaderContext.loaderIndex = 0; loaderContext.loaders = loaders; // 省略大部分代码 var processOptions = { resourceBuffer: null, processResource: processResource }; // 迭代PitchingLoaders iteratePitchingLoaders(processOptions, loaderContext, function(err, result) { // ... }); }; 复制代码
由以上代码可知,在 runLoaders
函数中,会先从 options
配置对象上获取 loaders
信息,而后调用 createLoaderObject
函数建立 Loader 对象,调用该方法后会返回包含 normal
、pitch
、raw
和 data
等属性的对象。目前该对象的大多数属性值都为 null
,在后续的处理流程中,就会填充相应的属性值。
// loader-runner/lib/LoaderRunner.js function createLoaderObject(loader) { var obj = { path: null, query: null, fragment: null, options: null, ident: null, normal: null, pitch: null, raw: null, data: null, pitchExecuted: false, normalExecuted: false }; // 省略部分代码 obj.request = loader; if(Object.preventExtensions) { Object.preventExtensions(obj); } return obj; } 复制代码
在建立完 Loader 对象及初始化 loaderContext 对象以后,就会调用 iteratePitchingLoaders
函数开始迭代 Pitching Loader。为了让你们对后续的处理流程有一个大体的了解,在看具体代码前,咱们再来回顾一下前面运行 txt loaders 的调用堆栈:
与之对应 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 模块所提供的功能。
4、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, currentLoaderObject.data = {}], function(err) { if(err) return callback(err); var args = Array.prototype.slice.call(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
对象。
5、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 = Array.prototype.slice.call(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
属性的做用有了更深入的了解。
6、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 最终返回的结果是如何被处理的的呢?下面咱们来简单介绍一下。
7、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
函数进行进一步处理。这里阿宝哥就不展开介绍了,感兴趣的小伙伴能够自行阅读一下相关源码。
8、为何要把 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
属性的目的是为了导出相应的内容。
9、总结
本文介绍了 Webpack Loader 的本质、Normal Loader 和 Pitching Loader 的定义和使用及 Loader 是如何被运行的等相关内容,但愿阅读完本文以后,你对 Webpack Loader 机制能有更深入的理解。文中阿宝哥只介绍了 loader-runner 模块,其实 loader-utils(Loader 工具库)和 schema-utils(Loader Options 验证库)这两个模块也与 Loader 息息相关。在编写 Loader 的时候,你可能就会使用到它们。
结语
创做不易,若是对你们有所帮助,但愿你们点赞支持,有什么问题也能够在评论区里讨论😄~