Webpack 是一个模块化打包工具,它被广泛地应用在前端领域的大多数项目中。利用 Webpack 我们不仅可以打包 JS 文件,还可以打包图片、CSS、字体等其他类型的资源文件。而支持打包非 JS 文件的特性是基于 Loader 机制来实现的。因此要学好 Webpack,我们就需要掌握 Loader 机制。本文阿宝哥将带大家一起深入学习 Webpack 的 Loader 机制,阅读完本文你将了解以下内容:
公司专注于为企业提供网站设计制作、成都网站建设、微信公众号开发、商城网站定制开发,微信小程序,软件定制设计等一站式互联网企业服务。凭借多年丰富的经验,我们会仔细了解各客户的需求而做出多方面的分析、设计、整合,为客户设计出具风格及创意性的商业解决方案,创新互联更提供一系列网站制作和网站推广的服务。
由上图可知,Loader 本质上是导出函数的 JavaScript 模块。所导出的函数,可用于实现内容转换,该函数支持以下 3 个参数:
- /**
- * @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:
- function simpleLoader(content, map, meta) {
- console.log("我是 SimpleLoader");
- return content;
- }
- module.exports = simpleLoader;
以上的 simpleLoader 并不会对输入的内容进行任何处理,只是在该 Loader 执行时输出相应的信息。Webpack 允许用户为某些资源文件配置多个不同的 Loader,比如在处理 .css 文件的时候,我们用到了 style-loader 和 css-loader,具体配置方式如下所示:
- 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'] 即可。
Loader 本质上是导出函数的 JavaScript 模块,而该模块导出的函数(若是 ES6 模块,则是默认导出的函数)就被称为 Normal Loader。需要注意的是,这里我们介绍的 Normal Loader 与 Webpack Loader 分类中定义的 Loader 是不一样的。在 Webpack 中,loader 可以被分为 4 类:pre 前置、post 后置、normal 普通和 inline 行内。其中 pre 和 post loader,可以通过 rule 对象的 enforce 属性来指定:
- // 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。首先我们先来创建一个新的目录:
- $ mkdir webpack-loader-demo
然后进入该目录,使用 npm init -y 命令执行初始化操作。该命令成功执行后,会在当前目录生成一个 package.json 文件:
- {
- "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;
接着我们使用以下命令,安装一下 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配置文件
Webpack Loader 示例 Webpack Loader 示例
- import Data from "./data.txt"
- const msgElement = document.querySelector("#message");
- msgElement.innerText = Data;
- 大家好,我是阿宝哥
- 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 属性呢?这里我们先不解释具体的原因,后面我们再来分析这个问题。
- function bLoader(content, map, meta) {
- console.log("开始执行bLoader Normal Loader");
- return content + "bLoader->";
- }
- module.exports = bLoader;
- 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 属性,具体的配置方式如下所示:
- 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。
在开发 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 文件:
- function bLoader(content, map, meta) {
- // 省略部分代码
- }
- bLoader.pitch = function (remainingRequest, precedingRequest, data) {
- console.log("开始执行bLoader Pitching Loader");
- console.log(remainingRequest, precedingRequest, data);
- };
- module.exports = bLoader;
- 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 是如何被运行的。
要搞清楚 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 模块所提供的功能。
- // 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 对象。
- // 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 属性的作用有了更深刻的了解。
Loader 可以分为同步 Loader 和异步 Loader,对于同步 Loader 来说,我们可以通过 return 语句或 this.callback 的方式来同步地返回转换后的结果。只是相比 return 语句,this.callback 方法则更灵活,因为它允许传递多个参数。
- module.exports = function(source) {
- return source + "-simple";
- };
- 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 函数:
- module.exports = function(source) {
- var callback = this.async();
- setTimeout(function() {
- callback(null, source + "-async-simple");
- }, 50);
- };
那么以上示例中,this.callback 和 this.async 方法是哪里来的呢?带着这个问题,我们来从 loader-runner 模块的源码中,一探究竟。
- // 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 都是指向同一个函数。
- // 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)
- r
网站标题:多图详解,一次性搞懂WebpackLoader
文章地址:http://www.shufengxianlan.com/qtweb/news34/137584.html网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联