新聞中心
Webpack 是一個(gè)模塊化打包工具,它被廣泛地應(yīng)用在前端領(lǐng)域的大多數(shù)項(xiàng)目中。利用 Webpack 我們不僅可以打包 JS 文件,還可以打包圖片、CSS、字體等其他類型的資源文件。而支持打包非 JS 文件的特性是基于 Loader 機(jī)制來實(shí)現(xiàn)的。因此要學(xué)好 Webpack,我們就需要掌握 Loader 機(jī)制。本文阿寶哥將帶大家一起深入學(xué)習(xí) Webpack 的 Loader 機(jī)制,閱讀完本文你將了解以下內(nèi)容:

公司專注于為企業(yè)提供網(wǎng)站設(shè)計(jì)制作、成都網(wǎng)站建設(shè)、微信公眾號(hào)開發(fā)、商城網(wǎng)站定制開發(fā),微信小程序,軟件定制設(shè)計(jì)等一站式互聯(lián)網(wǎng)企業(yè)服務(wù)。憑借多年豐富的經(jīng)驗(yàn),我們會(huì)仔細(xì)了解各客戶的需求而做出多方面的分析、設(shè)計(jì)、整合,為客戶設(shè)計(jì)出具風(fēng)格及創(chuàng)意性的商業(yè)解決方案,創(chuàng)新互聯(lián)更提供一系列網(wǎng)站制作和網(wǎng)站推廣的服務(wù)。
- Loader 的本質(zhì)是什么?
- Normal Loader 和 Pitching Loader 是什么?
- Pitching Loader 的作用是什么?
- Loader 是如何被加載的?
- Loader 是如何被運(yùn)行的?
- 多個(gè) Loader 的執(zhí)行順序是什么?
- Pitching Loader 的熔斷機(jī)制是如何實(shí)現(xiàn)的?
- Normal Loader 函數(shù)是如何被運(yùn)行的?
- Loader 對(duì)象上 raw 屬性有什么作用?
- Loader 函數(shù)體中的 this.callback 和 this.async 方法是哪里來的?
- Loader 最終的返回結(jié)果是如何被處理的?
一、Loader 的本質(zhì)是什么?
由上圖可知,Loader 本質(zhì)上是導(dǎo)出函數(shù)的 JavaScript 模塊。所導(dǎo)出的函數(shù),可用于實(shí)現(xiàn)內(nèi)容轉(zhuǎn)換,該函數(shù)支持以下 3 個(gè)參數(shù):
- /**
- * @param {string|Buffer} content 源文件的內(nèi)容
- * @param {object} [map] 可以被 https://github.com/mozilla/source-map 使用的 SourceMap 數(shù)據(jù)
- * @param {any} [meta] meta 數(shù)據(jù),可以是任何內(nèi)容
- */
- function webpackLoader(content, map, meta) {
- // 你的webpack loader代碼
- }
- module.exports = webpackLoader;
了解完導(dǎo)出函數(shù)的簽名之后,我們就可以定義一個(gè)簡(jiǎn)單的 simpleLoader:
- function simpleLoader(content, map, meta) {
- console.log("我是 SimpleLoader");
- return content;
- }
- module.exports = simpleLoader;
以上的 simpleLoader 并不會(huì)對(duì)輸入的內(nèi)容進(jìn)行任何處理,只是在該 Loader 執(zhí)行時(shí)輸出相應(yīng)的信息。Webpack 允許用戶為某些資源文件配置多個(gè)不同的 Loader,比如在處理 .css 文件的時(shí)候,我們用到了 style-loader 和 css-loader,具體配置方式如下所示:
webpack.config.js
- 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 這樣設(shè)計(jì)的好處,是可以保證每個(gè) Loader 的職責(zé)單一。同時(shí),也方便后期 Loader 的組合和擴(kuò)展。比如,你想讓 Webpack 能夠處理 Scss 文件,你只需先安裝 sass-loader,然后在配置 Scss 文件的處理規(guī)則時(shí),設(shè)置 rule 對(duì)象的 use 屬性為 ['style-loader', 'css-loader', 'sass-loader'] 即可。
二、Normal Loader 和 Pitching Loader 是什么?
2.1 Normal Loader
Loader 本質(zhì)上是導(dǎo)出函數(shù)的 JavaScript 模塊,而該模塊導(dǎo)出的函數(shù)(若是 ES6 模塊,則是默認(rèn)導(dǎo)出的函數(shù))就被稱為 Normal Loader。需要注意的是,這里我們介紹的 Normal Loader 與 Webpack Loader 分類中定義的 Loader 是不一樣的。在 Webpack 中,loader 可以被分為 4 類:pre 前置、post 后置、normal 普通和 inline 行內(nèi)。其中 pre 和 post loader,可以通過 rule 對(duì)象的 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 的概念之后,我們來動(dòng)手寫一下 Normal Loader。首先我們先來創(chuàng)建一個(gè)新的目錄:
- $ mkdir webpack-loader-demo
然后進(jìn)入該目錄,使用 npm init -y 命令執(zhí)行初始化操作。該命令成功執(zhí)行后,會(huì)在當(dāng)前目錄生成一個(gè) 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"
- }
提示:本地所使用的開發(fā)環(huán)境:Node v12.16.2;Npm 6.14.4;
接著我們使用以下命令,安裝一下 webpack 和 webpack-cli 依賴包:
- $ npm i webpack webpack-cli -D
安裝完項(xiàng)目依賴后,我們根據(jù)以下目錄結(jié)構(gòu)來添加對(duì)應(yīng)的目錄和文件:
- ├── dist # 打包輸出目錄
- │ └── index.html
- ├── loaders # loaders文件夾
- │ ├── a-loader.js
- │ ├── b-loader.js
- │ └── c-loader.js
- ├── node_modules
- ├── package-lock.json
- ├── package.json
- ├── src # 源碼目錄
- │ ├── data.txt # 數(shù)據(jù)文件
- │ └── index.js # 入口文件
- └── webpack.config.js # webpack配置文件
dist/index.html
Webpack Loader 示例 Webpack Loader 示例
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("開始執(zhí)行aLoader Normal Loader");
- content += "aLoader]";
- return `module.exports = '${content}'`;
- }
- module.exports = aLoader;
在 aLoader 函數(shù)中,我們會(huì)對(duì) content 內(nèi)容進(jìn)行修改,然后返回 module.exports = '${content}' 字符串。那么為什么要把 content 賦值給 module.exports 屬性呢?這里我們先不解釋具體的原因,后面我們?cè)賮矸治鲞@個(gè)問題。
loaders/b-loader.js
- function bLoader(content, map, meta) {
- console.log("開始執(zhí)行bLoader Normal Loader");
- return content + "bLoader->";
- }
- module.exports = bLoader;
loaders/c-loader.js
- function cLoader(content, map, meta) {
- console.log("開始執(zhí)行cLoader Normal Loader");
- return content + "[cLoader->";
- }
- module.exports = cLoader;
在 loaders 目錄下,我們定義了以上 3 個(gè) Normal Loader。這些 Loader 的實(shí)現(xiàn)都比較簡(jiǎn)單,只是在 Loader 執(zhí)行時(shí)往 content 參數(shù)上添加當(dāng)前 Loader 的相關(guān)信息。為了讓 Webpack 能夠識(shí)別 loaders 目錄下的自定義 Loader,我們還需要在 Webpack 的配置文件中,設(shè)置 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"),
- ],
- },
- };
當(dāng)目錄更新完成后,在 webpack-loader-demo 項(xiàng)目的根目錄下運(yùn)行 npx webpack 命令就可以開始打包了。以下內(nèi)容是阿寶哥運(yùn)行 npx webpack 命令之后,控制臺(tái)的輸出結(jié)果:
- 開始執(zhí)行cLoader Normal Loader
- 開始執(zhí)行bLoader Normal Loader
- 開始執(zhí)行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
通過觀察以上的輸出結(jié)果,我們可以知道 Normal Loader 的執(zhí)行順序是從右到左。此外,當(dāng)打包完成后,我們?cè)跒g覽器中打開 dist/index.html 文件,在頁面上你將看到以下信息:
- Webpack Loader 示例
- 大家好,我是阿寶哥[cLoader->bLoader->aLoader]
由頁面上的輸出信息 ”大家好,我是阿寶哥[cLoader->bLoader->aLoader]“ 可知,Loader 在執(zhí)行的過程中是以管道的形式,對(duì)數(shù)據(jù)進(jìn)行處理,具體處理過程如下圖所示:
現(xiàn)在你已經(jīng)知道什么是 Normal Loader 及 Normal Loader 的執(zhí)行順序,接下來我們來介紹另一種 Loader —— Pitching Loader。
2.2 Pitching Loader
在開發(fā) Loader 時(shí),我們可以在導(dǎo)出的函數(shù)上添加一個(gè) pitch 屬性,它的值也是一個(gè)函數(shù)。該函數(shù)被稱為 Pitching Loader,它支持 3 個(gè)參數(shù):
- /**
- * @remainingRequest 剩余請(qǐng)求
- * @precedingRequest 前置請(qǐng)求
- * @data 數(shù)據(jù)對(duì)象
- */
- function (remainingRequest, precedingRequest, data) {
- // some code
- };
其中 data 參數(shù),可以用于數(shù)據(jù)傳遞。即在 pitch 函數(shù)中往 data 對(duì)象上添加數(shù)據(jù),之后在 normal 函數(shù)中通過 this.data 的方式讀取已添加的數(shù)據(jù)。而 remainingRequest 和 precedingRequest 參數(shù)到底是什么?這里我們先來更新一下 a-loader.js 文件:
- function aLoader(content, map, meta) {
- // 省略部分代碼
- }
- aLoader.pitch = function (remainingRequest, precedingRequest, data) {
- console.log("開始執(zhí)行aLoader Pitching Loader");
- console.log(remainingRequest, precedingRequest, data)
- };
- module.exports = aLoader;
在以上代碼中,我們?yōu)?aLoader 函數(shù)增加了一個(gè) pitch 屬性并設(shè)置它的值為一個(gè)函數(shù)對(duì)象。在函數(shù)體中,我們輸出了該函數(shù)所接收的參數(shù)。接著,我們以同樣的方式更新 b-loader.js 和 c-loader.js 文件:
b-loader.js
- function bLoader(content, map, meta) {
- // 省略部分代碼
- }
- bLoader.pitch = function (remainingRequest, precedingRequest, data) {
- console.log("開始執(zhí)行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("開始執(zhí)行cLoader Pitching Loader");
- console.log(remainingRequest, precedingRequest, data);
- };
- module.exports = cLoader;
當(dāng)所有文件都更新完成后,我們?cè)?webpack-loader-demo 項(xiàng)目的根目錄再次執(zhí)行 npx webpack 命令后,就會(huì)輸出相應(yīng)的信息。這里我們以 b-loader.js 的 pitch 函數(shù)的輸出結(jié)果為例,來分析一下 remainingRequest 和 precedingRequest 參數(shù)的輸出結(jié)果:
- /Users/fer/webpack-loader-demo/loaders/c-loader.js!/Users/fer/webpack-loader-demo/src/data.txt #剩余請(qǐng)求
- /Users/fer/webpack-loader-demo/loaders/a-loader.js #前置請(qǐng)求
- {} #空的數(shù)據(jù)對(duì)象
除了以上的輸出信息之外,我們還可以很清楚的看到 Pitching Loader 和 Normal Loader 的執(zhí)行順序:
- 開始執(zhí)行aLoader Pitching Loader
- ...
- 開始執(zhí)行bLoader Pitching Loader
- ...
- 開始執(zhí)行cLoader Pitching Loader
- ...
- 開始執(zhí)行cLoader Normal Loader
- 開始執(zhí)行bLoader Normal Loader
- 開始執(zhí)行aLoader Normal Loader
很明顯對(duì)于我們的示例來說,Pitching Loader 的執(zhí)行順序是 從左到右,而 Normal Loader 的執(zhí)行順序是 從右到左。具體的執(zhí)行過程如下圖所示:
提示:Webpack 內(nèi)部會(huì)使用 loader-runner 這個(gè)庫來運(yùn)行已配置的 loaders。
看到這里有的小伙伴可能會(huì)有疑問,Pitching Loader 除了可以提前運(yùn)行之外,還有什么作用呢?其實(shí)當(dāng)某個(gè) Pitching Loader 返回非 undefined 值時(shí),就會(huì)實(shí)現(xiàn)熔斷效果。這里我們更新一下 bLoader.pitch 方法,讓它返回 "bLoader Pitching Loader->" 字符串:
- bLoader.pitch = function (remainingRequest, precedingRequest, data) {
- console.log("開始執(zhí)行bLoader Pitching Loader");
- return "bLoader Pitching Loader->";
- };
當(dāng)更新完 bLoader.pitch 方法,我們?cè)俅螆?zhí)行 npx webpack 命令之后,控制臺(tái)會(huì)輸出以下內(nèi)容:
- 開始執(zhí)行aLoader Pitching Loader
- 開始執(zhí)行bLoader Pitching Loader
- 開始執(zhí)行aLoader Normal Loader
- asset bundle.js 4.53 KiB [compared for emit] (name: main)
- runtime modules 937 bytes 4 modules
- ...
由以上輸出結(jié)果可知,當(dāng) bLoader.pitch 方法返回非 undefined 值時(shí),跳過了剩下的 loader。具體執(zhí)行流程如下圖所示:
提示:Webpack 內(nèi)部會(huì)使用 loader-runner 這個(gè)庫來運(yùn)行已配置的 loaders。
之后,我們?cè)跒g覽器中再次打開 dist/index.html 文件。此時(shí),在頁面上你將看到以下信息:
- Webpack Loader 示例
- bLoader Pitching Loader->aLoader]
介紹完 Normal Loader 和 Pitching Loader 的相關(guān)知識(shí),接下來我們來分析一下 Loader 是如何被運(yùn)行的。
三、Loader 是如何被運(yùn)行的?
要搞清楚 Loader 是如何被運(yùn)行的,我們可以借助斷點(diǎn)調(diào)試工具來找出 Loader 的運(yùn)行入口。這里我們以大家熟悉的 Visual Studio Code 為例,來介紹如何配置斷點(diǎn)調(diào)試環(huán)境:
當(dāng)你按照上述步驟操作之后,在當(dāng)前項(xiàng)目(webpack-loader-demo)下,會(huì)自動(dòng)創(chuàng)建 .vscode 目錄并在該目錄下自動(dòng)生成一個(gè) launch.json 文件。接著,我們復(fù)制以下內(nèi)容直接替換 launch.json 中的原始內(nèi)容。
- {
- "version": "0.2.0",
- "configurations": [{
- "type": "node",
- "request": "launch",
- "name": "Webpack Debug",
- "cwd": "${workspaceFolder}",
- "runtimeExecutable": "npm",
- "runtimeArgs": ["run", "debug"],
- "port": 5858
- }]
- }
利用以上配置信息,我們創(chuàng)建了一個(gè) Webpack Debug 的調(diào)試任務(wù)。當(dāng)運(yùn)行該任務(wù)的時(shí)候,會(huì)在當(dāng)前工作目錄下執(zhí)行 npm run debug 命令。因此,接下來我們需要在 package.json 文件中增加 debug 命令,具體內(nèi)容如下所示:
- // package.json
- {
- "scripts": {
- "debug": "node --inspect=5858 ./node_modules/.bin/webpack"
- },
- }
做好上述的準(zhǔn)備之后,我們就可以在 a-loader 的 pitch 函數(shù)中添加一個(gè)斷點(diǎn)。對(duì)應(yīng)的調(diào)用堆棧如下所示:
通過觀察以上的調(diào)用堆棧信息,我們可以看到調(diào)用 runLoaders 方法,該方法是來自于 loader-runner 模塊。所以要搞清楚 Loader 是如何被運(yùn)行的,我們就需要分析 runLoaders 方法。下面我們來開始分析項(xiàng)目中使用的 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上下文對(duì)象
- 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 函數(shù)中,會(huì)先從 options 配置對(duì)象上獲取 loaders 信息,然后調(diào)用 createLoaderObject 函數(shù)創(chuàng)建 Loader 對(duì)象,調(diào)用該方法后會(huì)返回包含 normal、pitch、raw 和 data 等屬性的對(duì)象。目前該對(duì)象的大多數(shù)屬性值都為 null,在后續(xù)的處理流程中,就會(huì)填充相應(yīng)的屬性值。
- // 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;
- }
在創(chuàng)建完 Loader 對(duì)象及初始化 loaderContext 對(duì)象之后,就會(huì)調(diào)用 iteratePitchingLoaders 函數(shù)開始迭代 Pitching Loader。為了讓大家對(duì)后續(xù)的處理流程有一個(gè)大致的了解,在看具體代碼前,我們?cè)賮砘仡櫼幌虑懊孢\(yùn)行 txt loaders 的調(diào)用堆棧:
與之對(duì)應(yīng) runLoaders 函數(shù)的 options 對(duì)象結(jié)構(gòu)如下所示:
基于上述的調(diào)用堆棧和相關(guān)的源碼,阿寶哥也畫了一張相應(yīng)的流程圖:
看完上面的流程圖和調(diào)用堆棧圖,接下來我們來分析一下流程圖中相關(guān)函數(shù)的核心代碼。這里我們先來分析 iteratePitchingLoaders:
- // loader-runner/lib/LoaderRunner.js
- function iteratePitchingLoaders(options, loaderContext, callback) {
- // abort after last loader
- if(loaderContext.loaderIndex >= loaderContext.loaders.length)
- // 在processResource函數(shù)內(nèi),會(huì)調(diào)用iterateNormalLoaders函數(shù)
- // 開始執(zhí)行normal loader
- return processResource(options, loaderContext, callback);
- // 首次執(zhí)行時(shí),loaderContext.loaderIndex的值為0
- var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
- // 如果當(dāng)前l(fā)oader對(duì)象的pitch函數(shù)已經(jīng)被執(zhí)行過了,則執(zhí)行下一個(gè)loader的pitch函數(shù)
- if(currentLoaderObject.pitchExecuted) {
- loaderContext.loaderIndex++;
- return iteratePitchingLoaders(options, loaderContext, callback);
- }
- // 加載loader模塊
- loadLoader(currentLoaderObject, function(err) {
- if(err) {
- loaderContext.cacheable(false);
- return callback(err);
- }
- // 獲取當(dāng)前l(fā)oader對(duì)象上的pitch函數(shù)
- var fn = currentLoaderObject.pitch;
- // 標(biāo)識(shí)loader對(duì)象已經(jīng)被iteratePitchingLoaders函數(shù)處理過
- currentLoaderObject.pitchExecuted = true;
- if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
- // 開始執(zhí)行pitch函數(shù)
- runSyncOrAsync(fn,loaderContext, ...);
- // 省略部分代碼
- });
- }
在 iteratePitchingLoaders 函數(shù)內(nèi)部,會(huì)從最左邊的 loader 對(duì)象開始處理,然后調(diào)用 loadLoader 函數(shù)開始加載 loader 模塊。在 loadLoader 函數(shù)內(nèi)部,會(huì)根據(jù) loader 的類型,使用不同的加載方式。對(duì)于我們當(dāng)前的項(xiàng)目來說,會(huì)通過 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) {
- // 省略相關(guān)代碼
- }
- // 處理已加載的模塊
- return handleResult(loader, module, callback);
- }
- };
不管使用哪種加載方式,在成功加載 loader 模塊之后,都會(huì)調(diào)用 handleResult 函數(shù)來處理已加載的模塊。該函數(shù)的作用是,獲取模塊中的導(dǎo)出函數(shù)及該函數(shù)上 pitch 和 raw 屬性的值并賦值給對(duì)應(yīng) loader 對(duì)象的相應(yīng)屬性:
- // 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 模塊之后,就會(huì)繼續(xù)調(diào)用傳入的 callback 回調(diào)函數(shù)。在該回調(diào)函數(shù)內(nèi),會(huì)先在當(dāng)前的 loader 對(duì)象上獲取 pitch 函數(shù),然后調(diào)用 runSyncOrAsync 函數(shù)來執(zhí)行 pitch 函數(shù)。對(duì)于我們的項(xiàng)目來說,就會(huì)開始執(zhí)行 aLoader.pitch 函數(shù)。
看到這里的小伙伴,應(yīng)該已經(jīng)知道 loader 模塊是如何被加載的及 loader 模塊中定義的 pitch 函數(shù)是如何被運(yùn)行的。由于篇幅有限,阿寶哥就不再詳細(xì)展開介紹 loader-runner 模塊中其他函數(shù)。接下來,我們將通過幾個(gè)問題來繼續(xù)分析 loader-runner 模塊所提供的功能。
四、Pitching Loader 的熔斷機(jī)制是如何實(shí)現(xiàn)的?
- // loader-runner/lib/LoaderRunner.js
- function iteratePitchingLoaders(options, loaderContext, callback) {
- // 省略部分代碼
- loadLoader(currentLoaderObject, function(err) {
- var fn = currentLoaderObject.pitch;
- // 標(biāo)識(shí)當(dāng)前l(fā)oader已經(jīng)被處理過
- currentLoaderObject.pitchExecuted = true;
- // 若當(dāng)前l(fā)oader對(duì)象上未定義pitch函數(shù),則處理下一個(gè)loader對(duì)象
- if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
- // 執(zhí)行l(wèi)oader模塊中定義的pitch函數(shù)
- 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 函數(shù)的回調(diào)函數(shù)內(nèi)部,會(huì)根據(jù)當(dāng)前 loader 對(duì)象 pitch 函數(shù)的返回值是否為 undefined 來執(zhí)行不同的處理邏輯。如果 pitch 函數(shù)返回了非 undefined 的值,則會(huì)出現(xiàn)熔斷。即跳過后續(xù)的執(zhí)行流程,開始執(zhí)行上一個(gè) loader 對(duì)象上的 normal loader 函數(shù)。具體的實(shí)現(xiàn)方式也很簡(jiǎn)單,就是 loaderIndex 的值減 1,然后調(diào)用 iterateNormalLoaders 函數(shù)來實(shí)現(xiàn)。而如果 pitch 函數(shù)返回 undefined,則繼續(xù)調(diào)用 iteratePitchingLoaders 函數(shù)來處理下一個(gè)未處理 loader 對(duì)象。
五、Normal Loader 函數(shù)是如何被運(yùn)行的?
- // 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的執(zhí)行順序是從右到左
- if(currentLoaderObject.normalExecuted) {
- loaderContext.loaderIndex--;
- return iterateNormalLoaders(options, loaderContext, args, callback);
- }
- // 獲取當(dāng)前l(fā)oader對(duì)象上的normal函數(shù)
- var fn = currentLoaderObject.normal;
- // 標(biāo)識(shí)loader對(duì)象已經(jīng)被iterateNormalLoaders函數(shù)處理過
- currentLoaderObject.normalExecuted = true;
- if(!fn) { // 當(dāng)前l(fā)oader對(duì)象未定義normal函數(shù),則繼續(xù)處理前一個(gè)loader對(duì)象
- 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 模塊內(nèi)部會(huì)通過調(diào)用 iterateNormalLoaders 函數(shù),來執(zhí)行已加載 loader 對(duì)象上的 normal loader 函數(shù)。與 iteratePitchingLoaders 函數(shù)一樣,在 iterateNormalLoaders 函數(shù)內(nèi)部也是通過調(diào)用 runSyncOrAsync 函數(shù)來執(zhí)行 fn 函數(shù)。不過在調(diào)用 normal loader 函數(shù)前,會(huì)先調(diào)用 convertArgs 函數(shù)對(duì)參數(shù)進(jìn)行處理。
convertArgs 函數(shù)會(huì)根據(jù) raw 屬性來對(duì) args[0](文件的內(nèi)容)進(jìn)行處理,該函數(shù)的具體實(shí)現(xiàn)如下所示:
- // 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對(duì)象轉(zhuǎn)換為utf-8格式的字符串
- function utf8BufferToString(buf) {
- var str = buf.toString("utf-8");
- if(str.charCodeAt(0) === 0xFEFF) {
- return str.substr(1);
- } else {
- return str;
- }
- }
相信看完 convertArgs 函數(shù)的相關(guān)代碼之后,你對(duì) raw 屬性的作用有了更深刻的了解。
六、Loader 函數(shù)體中的 this.callback 和 this.async 方法是哪里來的?
Loader 可以分為同步 Loader 和異步 Loader,對(duì)于同步 Loader 來說,我們可以通過 return 語句或 this.callback 的方式來同步地返回轉(zhuǎn)換后的結(jié)果。只是相比 return 語句,this.callback 方法則更靈活,因?yàn)樗试S傳遞多個(gè)參數(shù)。
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; // 當(dāng)調(diào)用 callback() 函數(shù)時(shí),總是返回 undefined
- };
需要注意的是 this.callback 方法支持 4 個(gè)參數(shù),每個(gè)參數(shù)的具體作用如下所示:
- this.callback(
- err: Error | null, // 錯(cuò)誤信息
- content: string | Buffer, // content信息
- sourceMap?: SourceMap, // sourceMap
- meta?: any // 會(huì)被 webpack 忽略,可以是任何東西
- );
而對(duì)于異步 loader,我們需要調(diào)用 this.async 方法來獲取 callback 函數(shù):
async-loader.js
- module.exports = function(source) {
- var callback = this.async();
- setTimeout(function() {
- callback(null, source + "-async-simple");
- }, 50);
- };
那么以上示例中,this.callback 和 this.async 方法是哪里來的呢?帶著這個(gè)問題,我們來從 loader-runner 模塊的源碼中,一探究竟。
this.async
- // loader-runner/lib/LoaderRunner.js
- function runSyncOrAsync(fn, context, args, callback) {
- var isSync = true; // 默認(rèn)是同步類型
- 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;
- };
- }
在前面我們已經(jīng)介紹過 runSyncOrAsync 函數(shù)的作用,該函數(shù)用于執(zhí)行 Loader 模塊中設(shè)置的 Normal Loader 或 Pitching Loader 函數(shù)。在 runSyncOrAsync 函數(shù)內(nèi)部,最終會(huì)通過 fn.apply(context, args) 的方式調(diào)用 Loader 函數(shù)。即會(huì)通過 apply 方法設(shè)置 Loader 函數(shù)的執(zhí)行上下文。
此外,由以上代碼可知,當(dāng)調(diào)用 this.async 方法之后,會(huì)先設(shè)置 isSync 的值為 false,然后返回 innerCallback 函數(shù)。其實(shí)該函數(shù)與 this.callback 都是指向同一個(gè)函數(shù)。
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 函數(shù)中,是通過 return 語句來返回處理結(jié)果的話,那么 isSync 值仍為 true,將會(huì)執(zhí)行以下相應(yīng)的處理邏輯:
- // 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語句返回處理結(jié)果
- isDone = true;
- if(result === undefined)
- r
文章題目:多圖詳解,一次性搞懂WebpackLoader
網(wǎng)站網(wǎng)址:http://m.fisionsoft.com.cn/article/dhejepd.html


咨詢
建站咨詢
