日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
多圖詳解,一次性搞懂WebpackLoader

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è)、微信公眾號開發(fā)、商城網(wǎng)站定制開發(fā),微信小程序,軟件定制設(shè)計(jì)等一站式互聯(lián)網(wǎng)企業(yè)服務(wù)。憑借多年豐富的經(jīng)驗(yàn),我們會仔細(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 對象上 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ù):

 
 
 
 
  1. /**
  2.  * @param {string|Buffer} content 源文件的內(nèi)容
  3.  * @param {object} [map] 可以被 https://github.com/mozilla/source-map 使用的 SourceMap 數(shù)據(jù)
  4.  * @param {any} [meta] meta 數(shù)據(jù),可以是任何內(nèi)容
  5.  */
  6. function webpackLoader(content, map, meta) {
  7.   // 你的webpack loader代碼
  8. }
  9. module.exports = webpackLoader;

了解完導(dǎo)出函數(shù)的簽名之后,我們就可以定義一個(gè)簡單的 simpleLoader:

 
 
 
 
  1. function simpleLoader(content, map, meta) {
  2.   console.log("我是 SimpleLoader");
  3.   return content;
  4. }
  5. module.exports = simpleLoader;

以上的 simpleLoader 并不會對輸入的內(nèi)容進(jìn)行任何處理,只是在該 Loader 執(zhí)行時(shí)輸出相應(yīng)的信息。Webpack 允許用戶為某些資源文件配置多個(gè)不同的 Loader,比如在處理 .css 文件的時(shí)候,我們用到了 style-loader 和 css-loader,具體配置方式如下所示:

webpack.config.js

 
 
 
 
  1. const path = require('path');
  2. module.exports = {
  3.   entry: './src/index.js',
  4.   output: {
  5.      filename: 'bundle.js',
  6.      path: path.resolve(__dirname, 'dist'),
  7.   },
  8.   module: {
  9.     rules: [
  10.       {
  11.         test: /\.css$/i,
  12.         use: ['style-loader', 'css-loader'],
  13.       },
  14.     ],
  15.   },
  16. };

Webpack 這樣設(shè)計(jì)的好處,是可以保證每個(gè) Loader 的職責(zé)單一。同時(shí),也方便后期 Loader 的組合和擴(kuò)展。比如,你想讓 Webpack 能夠處理 Scss 文件,你只需先安裝 sass-loader,然后在配置 Scss 文件的處理規(guī)則時(shí),設(shè)置 rule 對象的 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 對象的 enforce 屬性來指定:

 
 
 
 
  1. // webpack.config.js
  2. const path = require("path");
  3. module.exports = {
  4.   module: {
  5.     rules: [
  6.       {
  7.         test: /\.txt$/i,
  8.         use: ["a-loader"],
  9.         enforce: "post", // post loader
  10.       },
  11.       {
  12.         test: /\.txt$/i,
  13.         use: ["b-loader"], // normal loader
  14.       },
  15.       {
  16.         test: /\.txt$/i,
  17.         use: ["c-loader"],
  18.         enforce: "pre", // pre loader
  19.       },
  20.     ],
  21.   },
  22. };

了解完 Normal Loader 的概念之后,我們來動手寫一下 Normal Loader。首先我們先來創(chuàng)建一個(gè)新的目錄:

 
 
 
 
  1. $ mkdir webpack-loader-demo

然后進(jìn)入該目錄,使用 npm init -y 命令執(zhí)行初始化操作。該命令成功執(zhí)行后,會在當(dāng)前目錄生成一個(gè) package.json 文件:

 
 
 
 
  1. {
  2.   "name": "webpack-loader-demo",
  3.   "version": "1.0.0",
  4.   "description": "",
  5.   "main": "index.js",
  6.   "scripts": {
  7.     "test": "echo \"Error: no test specified\" && exit 1"
  8.   },
  9.   "keywords": [],
  10.   "author": "",
  11.   "license": "ISC"
  12. }

提示:本地所使用的開發(fā)環(huán)境:Node v12.16.2;Npm 6.14.4;

接著我們使用以下命令,安裝一下 webpack 和 webpack-cli 依賴包:

 
 
 
 
  1. $ npm i webpack webpack-cli -D

安裝完項(xiàng)目依賴后,我們根據(jù)以下目錄結(jié)構(gòu)來添加對應(yīng)的目錄和文件:

 
 
 
 
  1. ├── dist # 打包輸出目錄
  2. │   └── index.html
  3. ├── loaders # loaders文件夾
  4. │   ├── a-loader.js
  5. │   ├── b-loader.js
  6. │   └── c-loader.js
  7. ├── node_modules
  8. ├── package-lock.json
  9. ├── package.json
  10. ├── src # 源碼目錄
  11. │   ├── data.txt # 數(shù)據(jù)文件
  12. │   └── index.js # 入口文件
  13. └── webpack.config.js # webpack配置文件

dist/index.html

 
 
 
 
  1.     
  2.     
  3.     
  4.     Webpack Loader 示例
  5.     

    Webpack Loader 示例

  6.     

  7.     

src/index.js

 
 
 
 
  1. import Data from "./data.txt"
  2. const msgElement = document.querySelector("#message");
  3. msgElement.innerText = Data;

src/data.txt

 
 
 
 
  1. 大家好,我是阿寶哥

loaders/a-loader.js

 
 
 
 
  1. function aLoader(content, map, meta) {
  2.   console.log("開始執(zhí)行aLoader Normal Loader");
  3.   content += "aLoader]";
  4.   return `module.exports = '${content}'`;
  5. }
  6. module.exports = aLoader;

在 aLoader 函數(shù)中,我們會對 content 內(nèi)容進(jìn)行修改,然后返回 module.exports = '${content}' 字符串。那么為什么要把 content 賦值給 module.exports 屬性呢?這里我們先不解釋具體的原因,后面我們再來分析這個(gè)問題。

loaders/b-loader.js

 
 
 
 
  1. function bLoader(content, map, meta) {
  2.   console.log("開始執(zhí)行bLoader Normal Loader");
  3.   return content + "bLoader->";
  4. }
  5. module.exports = bLoader;

loaders/c-loader.js

 
 
 
 
  1. function cLoader(content, map, meta) {
  2.   console.log("開始執(zhí)行cLoader Normal Loader");
  3.   return content + "[cLoader->";
  4. }
  5. module.exports = cLoader;

在 loaders 目錄下,我們定義了以上 3 個(gè) Normal Loader。這些 Loader 的實(shí)現(xiàn)都比較簡單,只是在 Loader 執(zhí)行時(shí)往 content 參數(shù)上添加當(dāng)前 Loader 的相關(guān)信息。為了讓 Webpack 能夠識別 loaders 目錄下的自定義 Loader,我們還需要在 Webpack 的配置文件中,設(shè)置 resolveLoader 屬性,具體的配置方式如下所示:

webpack.config.js

 
 
 
 
  1. const path = require("path");
  2. module.exports = {
  3.   entry: "./src/index.js",
  4.   output: {
  5.     filename: "bundle.js",
  6.     path: path.resolve(__dirname, "dist"),
  7.   },
  8.   mode: "development",
  9.   module: {
  10.     rules: [
  11.       {
  12.         test: /\.txt$/i,
  13.         use: ["a-loader", "b-loader", "c-loader"],
  14.       },
  15.     ],
  16.   },
  17.   resolveLoader: {
  18.     modules: [
  19.       path.resolve(__dirname, "node_modules"),
  20.       path.resolve(__dirname, "loaders"),
  21.     ],
  22.   },
  23. };

當(dāng)目錄更新完成后,在 webpack-loader-demo 項(xiàng)目的根目錄下運(yùn)行 npx webpack 命令就可以開始打包了。以下內(nèi)容是阿寶哥運(yùn)行 npx webpack 命令之后,控制臺的輸出結(jié)果:

 
 
 
 
  1. 開始執(zhí)行cLoader Normal Loader
  2. 開始執(zhí)行bLoader Normal Loader
  3. 開始執(zhí)行aLoader Normal Loader
  4. asset bundle.js 4.55 KiB [emitted] (name: main)
  5. runtime modules 937 bytes 4 modules
  6. cacheable modules 187 bytes
  7.   ./src/index.js 114 bytes [built] [code generated]
  8.   ./src/data.txt 73 bytes [built] [code generated]
  9. webpack 5.45.1 compiled successfully in 99 ms

通過觀察以上的輸出結(jié)果,我們可以知道 Normal Loader 的執(zhí)行順序是從右到左。此外,當(dāng)打包完成后,我們在瀏覽器中打開 dist/index.html 文件,在頁面上你將看到以下信息:

 
 
 
 
  1. Webpack Loader 示例
  2. 大家好,我是阿寶哥[cLoader->bLoader->aLoader]

由頁面上的輸出信息 ”大家好,我是阿寶哥[cLoader->bLoader->aLoader]“ 可知,Loader 在執(zhí)行的過程中是以管道的形式,對數(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ù):

 
 
 
 
  1. /**
  2.  * @remainingRequest 剩余請求
  3.  * @precedingRequest 前置請求
  4.  * @data 數(shù)據(jù)對象
  5.  */
  6. function (remainingRequest, precedingRequest, data) {
  7.  // some code
  8. };

其中 data 參數(shù),可以用于數(shù)據(jù)傳遞。即在 pitch 函數(shù)中往 data 對象上添加數(shù)據(jù),之后在 normal 函數(shù)中通過 this.data 的方式讀取已添加的數(shù)據(jù)。而 remainingRequest 和 precedingRequest 參數(shù)到底是什么?這里我們先來更新一下 a-loader.js 文件:

 
 
 
 
  1. function aLoader(content, map, meta) {
  2.   // 省略部分代碼
  3. }
  4. aLoader.pitch = function (remainingRequest, precedingRequest, data) {
  5.   console.log("開始執(zhí)行aLoader Pitching Loader");
  6.   console.log(remainingRequest, precedingRequest, data)
  7. };
  8. module.exports = aLoader;

在以上代碼中,我們?yōu)?aLoader 函數(shù)增加了一個(gè) pitch 屬性并設(shè)置它的值為一個(gè)函數(shù)對象。在函數(shù)體中,我們輸出了該函數(shù)所接收的參數(shù)。接著,我們以同樣的方式更新 b-loader.js 和 c-loader.js 文件:

b-loader.js

 
 
 
 
  1. function bLoader(content, map, meta) {
  2.   // 省略部分代碼
  3. }
  4. bLoader.pitch = function (remainingRequest, precedingRequest, data) {
  5.   console.log("開始執(zhí)行bLoader Pitching Loader");
  6.   console.log(remainingRequest, precedingRequest, data);
  7. };
  8. module.exports = bLoader;

c-loader.js

 
 
 
 
  1. function cLoader(content, map, meta) {
  2.   // 省略部分代碼
  3. }
  4. cLoader.pitch = function (remainingRequest, precedingRequest, data) {
  5.   console.log("開始執(zhí)行cLoader Pitching Loader");
  6.   console.log(remainingRequest, precedingRequest, data);
  7. };
  8. module.exports = cLoader;

當(dāng)所有文件都更新完成后,我們在 webpack-loader-demo 項(xiàng)目的根目錄再次執(zhí)行 npx webpack 命令后,就會輸出相應(yīng)的信息。這里我們以 b-loader.js 的 pitch 函數(shù)的輸出結(jié)果為例,來分析一下 remainingRequest 和 precedingRequest 參數(shù)的輸出結(jié)果:

 
 
 
 
  1. /Users/fer/webpack-loader-demo/loaders/c-loader.js!/Users/fer/webpack-loader-demo/src/data.txt #剩余請求
  2. /Users/fer/webpack-loader-demo/loaders/a-loader.js #前置請求
  3. {} #空的數(shù)據(jù)對象

除了以上的輸出信息之外,我們還可以很清楚的看到 Pitching Loader 和 Normal Loader 的執(zhí)行順序:

 
 
 
 
  1. 開始執(zhí)行aLoader Pitching Loader
  2. ...
  3. 開始執(zhí)行bLoader Pitching Loader
  4. ...
  5. 開始執(zhí)行cLoader Pitching Loader
  6. ...
  7. 開始執(zhí)行cLoader Normal Loader
  8. 開始執(zhí)行bLoader Normal Loader
  9. 開始執(zhí)行aLoader Normal Loader

很明顯對于我們的示例來說,Pitching Loader 的執(zhí)行順序是 從左到右,而 Normal Loader 的執(zhí)行順序是 從右到左。具體的執(zhí)行過程如下圖所示:

提示:Webpack 內(nèi)部會使用 loader-runner 這個(gè)庫來運(yùn)行已配置的 loaders。

看到這里有的小伙伴可能會有疑問,Pitching Loader 除了可以提前運(yùn)行之外,還有什么作用呢?其實(shí)當(dāng)某個(gè) Pitching Loader 返回非 undefined 值時(shí),就會實(shí)現(xiàn)熔斷效果。這里我們更新一下 bLoader.pitch 方法,讓它返回 "bLoader Pitching Loader->" 字符串:

 
 
 
 
  1. bLoader.pitch = function (remainingRequest, precedingRequest, data) {
  2.   console.log("開始執(zhí)行bLoader Pitching Loader");
  3.   return "bLoader Pitching Loader->";
  4. };

當(dāng)更新完 bLoader.pitch 方法,我們再次執(zhí)行 npx webpack 命令之后,控制臺會輸出以下內(nèi)容:

 
 
 
 
  1. 開始執(zhí)行aLoader Pitching Loader
  2. 開始執(zhí)行bLoader Pitching Loader
  3. 開始執(zhí)行aLoader Normal Loader
  4. asset bundle.js 4.53 KiB [compared for emit] (name: main)
  5. runtime modules 937 bytes 4 modules
  6. ...

由以上輸出結(jié)果可知,當(dāng) bLoader.pitch 方法返回非 undefined 值時(shí),跳過了剩下的 loader。具體執(zhí)行流程如下圖所示:

提示:Webpack 內(nèi)部會使用 loader-runner 這個(gè)庫來運(yùn)行已配置的 loaders。

之后,我們在瀏覽器中再次打開 dist/index.html 文件。此時(shí),在頁面上你將看到以下信息:

 
 
 
 
  1. Webpack Loader 示例
  2. bLoader Pitching Loader->aLoader]

介紹完 Normal Loader 和 Pitching Loader 的相關(guān)知識,接下來我們來分析一下 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)下,會自動創(chuàng)建 .vscode 目錄并在該目錄下自動生成一個(gè) launch.json 文件。接著,我們復(fù)制以下內(nèi)容直接替換 launch.json 中的原始內(nèi)容。

 
 
 
 
  1. {
  2.     "version": "0.2.0",
  3.     "configurations": [{
  4.        "type": "node",
  5.        "request": "launch",
  6.        "name": "Webpack Debug",
  7.        "cwd": "${workspaceFolder}",
  8.        "runtimeExecutable": "npm",
  9.        "runtimeArgs": ["run", "debug"],
  10.        "port": 5858
  11.     }]
  12. }

利用以上配置信息,我們創(chuàng)建了一個(gè) Webpack Debug 的調(diào)試任務(wù)。當(dāng)運(yùn)行該任務(wù)的時(shí)候,會在當(dāng)前工作目錄下執(zhí)行 npm run debug 命令。因此,接下來我們需要在 package.json 文件中增加 debug 命令,具體內(nèi)容如下所示:

 
 
 
 
  1. // package.json
  2. {  
  3.   "scripts": {
  4.     "debug": "node --inspect=5858 ./node_modules/.bin/webpack"
  5.   },
  6. }

做好上述的準(zhǔn)備之后,我們就可以在 a-loader 的 pitch 函數(shù)中添加一個(gè)斷點(diǎn)。對應(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 文件中:

 
 
 
 
  1. // loader-runner/lib/LoaderRunner.js
  2. exports.runLoaders = function runLoaders(options, callback) {
  3.   // read options
  4.  var resource = options.resource || "";
  5.  var loaders = options.loaders || [];
  6.  var loaderContext = options.context || {}; // Loader上下文對象
  7.  var processResource = options.processResource || ((readResource, context, 
  8.     resource, callback) => {
  9.   context.addDependency(resource);
  10.   readResource(resource, callback);
  11.  }).bind(null, options.readResource || readFile);
  12.  // prepare loader objects
  13.  loaders = loaders.map(createLoaderObject);
  14.   loaderContext.context = contextDirectory;
  15.  loaderContext.loaderIndex = 0;
  16.  loaderContext.loaders = loaders;
  17.   
  18.   // 省略大部分代碼
  19.  var processOptions = {
  20.   resourceBuffer: null,
  21.   processResource: processResource
  22.  };
  23.   // 迭代PitchingLoaders
  24.  iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
  25.   // ...
  26.  });
  27. };

由以上代碼可知,在 runLoaders 函數(shù)中,會先從 options 配置對象上獲取 loaders 信息,然后調(diào)用 createLoaderObject 函數(shù)創(chuàng)建 Loader 對象,調(diào)用該方法后會返回包含 normal、pitch、raw 和 data 等屬性的對象。目前該對象的大多數(shù)屬性值都為 null,在后續(xù)的處理流程中,就會填充相應(yīng)的屬性值。

 
 
 
 
  1. // loader-runner/lib/LoaderRunner.js
  2. function createLoaderObject(loader) {
  3.  var obj = {
  4.   path: null,
  5.     query: null, 
  6.     fragment: null,
  7.   options: null, 
  8.     ident: null,
  9.   normal: null, 
  10.     pitch: null,
  11.   raw: null, 
  12.     data: null,
  13.   pitchExecuted: false,
  14.   normalExecuted: false
  15.  };
  16.  // 省略部分代碼
  17.  obj.request = loader;
  18.  if(Object.preventExtensions) {
  19.   Object.preventExtensions(obj);
  20.  }
  21.  return obj;
  22. }

在創(chuàng)建完 Loader 對象及初始化 loaderContext 對象之后,就會調(diào)用 iteratePitchingLoaders 函數(shù)開始迭代 Pitching Loader。為了讓大家對后續(xù)的處理流程有一個(gè)大致的了解,在看具體代碼前,我們再來回顧一下前面運(yùn)行 txt loaders 的調(diào)用堆棧:

與之對應(yīng) runLoaders 函數(shù)的 options 對象結(jié)構(gòu)如下所示:

基于上述的調(diào)用堆棧和相關(guān)的源碼,阿寶哥也畫了一張相應(yīng)的流程圖:

看完上面的流程圖和調(diào)用堆棧圖,接下來我們來分析一下流程圖中相關(guān)函數(shù)的核心代碼。這里我們先來分析 iteratePitchingLoaders:

 
 
 
 
  1. // loader-runner/lib/LoaderRunner.js
  2. function iteratePitchingLoaders(options, loaderContext, callback) {
  3.  // abort after last loader
  4.  if(loaderContext.loaderIndex >= loaderContext.loaders.length)
  5.     // 在processResource函數(shù)內(nèi),會調(diào)用iterateNormalLoaders函數(shù)
  6.     // 開始執(zhí)行normal loader
  7.   return processResource(options, loaderContext, callback);
  8.   // 首次執(zhí)行時(shí),loaderContext.loaderIndex的值為0
  9.  var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
  10.  // 如果當(dāng)前l(fā)oader對象的pitch函數(shù)已經(jīng)被執(zhí)行過了,則執(zhí)行下一個(gè)loader的pitch函數(shù)
  11.  if(currentLoaderObject.pitchExecuted) {
  12.   loaderContext.loaderIndex++;
  13.   return iteratePitchingLoaders(options, loaderContext, callback);
  14.  }
  15.  // 加載loader模塊
  16.  loadLoader(currentLoaderObject, function(err) {
  17.     if(err) {
  18.    loaderContext.cacheable(false);
  19.    return callback(err);
  20.   }
  21.     // 獲取當(dāng)前l(fā)oader對象上的pitch函數(shù)
  22.   var fn = currentLoaderObject.pitch;
  23.     // 標(biāo)識loader對象已經(jīng)被iteratePitchingLoaders函數(shù)處理過
  24.   currentLoaderObject.pitchExecuted = true;
  25.   if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
  26.     // 開始執(zhí)行pitch函數(shù)
  27.   runSyncOrAsync(fn,loaderContext, ...);
  28.   // 省略部分代碼
  29.  });
  30. }

在 iteratePitchingLoaders 函數(shù)內(nèi)部,會從最左邊的 loader 對象開始處理,然后調(diào)用 loadLoader 函數(shù)開始加載 loader 模塊。在 loadLoader 函數(shù)內(nèi)部,會根據(jù) loader 的類型,使用不同的加載方式。對于我們當(dāng)前的項(xiàng)目來說,會通過 require(loader.path) 的方式來加載 loader 模塊。具體的代碼如下所示:

 
 
 
 
  1. // loader-runner/lib/loadLoader.js
  2. module.exports = function loadLoader(loader, callback) {
  3.  if(loader.type === "module") {
  4.   try {
  5.     if(url === undefined) url = require("url");
  6.    var loaderUrl = url.pathToFileURL(loader.path);
  7.    var modulePromise = eval("import(" + JSON.stringify(loaderUrl.toString()) + ")");
  8.    modulePromise.then(function(module) {
  9.     handleResult(loader, module, callback);
  10.    }, callback);
  11.    return;
  12.   } catch(e) {
  13.    callback(e);
  14.   }
  15.  } else {
  16.   try {
  17.    var module = require(loader.path);
  18.   } catch(e) {
  19.    // 省略相關(guān)代碼
  20.   }
  21.     // 處理已加載的模塊
  22.   return handleResult(loader, module, callback);
  23.  }
  24. };

不管使用哪種加載方式,在成功加載 loader 模塊之后,都會調(diào)用 handleResult 函數(shù)來處理已加載的模塊。該函數(shù)的作用是,獲取模塊中的導(dǎo)出函數(shù)及該函數(shù)上 pitch 和 raw 屬性的值并賦值給對應(yīng) loader 對象的相應(yīng)屬性:

 
 
 
 
  1. // loader-runner/lib/loadLoader.js
  2. function handleResult(loader, module, callback) {
  3.  if(typeof module !== "function" && typeof module !== "object") {
  4.   return callback(new LoaderLoadingError(
  5.    "Module '" + loader.path + "' is not a loader (export function or es6 module)"
  6.   ));
  7.  }
  8.  loader.normal = typeof module === "function" ? module : module.default;
  9.  loader.pitch = module.pitch;
  10.  loader.raw = module.raw;
  11.  if(typeof loader.normal !== "function" && typeof loader.pitch !== "function") {
  12.   return callback(new LoaderLoadingError(
  13.    "Module '" + loader.path + "' is not a loader (must have normal or pitch function)"
  14.   ));
  15.  }
  16.  callback();
  17. }

在處理完已加載的 loader 模塊之后,就會繼續(xù)調(diào)用傳入的 callback 回調(diào)函數(shù)。在該回調(diào)函數(shù)內(nèi),會先在當(dāng)前的 loader 對象上獲取 pitch 函數(shù),然后調(diào)用 runSyncOrAsync 函數(shù)來執(zhí)行 pitch 函數(shù)。對于我們的項(xiàng)目來說,就會開始執(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)的?

 
 
 
 
  1. // loader-runner/lib/LoaderRunner.js
  2. function iteratePitchingLoaders(options, loaderContext, callback) {
  3.  // 省略部分代碼
  4.  loadLoader(currentLoaderObject, function(err) {
  5.   var fn = currentLoaderObject.pitch;
  6.     // 標(biāo)識當(dāng)前l(fā)oader已經(jīng)被處理過
  7.   currentLoaderObject.pitchExecuted = true;
  8.     // 若當(dāng)前l(fā)oader對象上未定義pitch函數(shù),則處理下一個(gè)loader對象
  9.   if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
  10.     // 執(zhí)行l(wèi)oader模塊中定義的pitch函數(shù)
  11.   runSyncOrAsync(
  12.    fn,
  13.    loaderContext, [loaderContext.remainingRequest, 
  14.         loaderContext.previousRequest, currentLoaderObject.data = {}],
  15.    function(err) {
  16.     if(err) return callback(err);
  17.     var args = Array.prototype.slice.call(arguments, 1);
  18.     var hasArg = args.some(function(value) {
  19.      return value !== undefined;
  20.     });
  21.     if(hasArg) {
  22.      loaderContext.loaderIndex--;
  23.      iterateNormalLoaders(options, loaderContext, args, callback);
  24.     } else {
  25.      iteratePitchingLoaders(options, loaderContext, callback);
  26.     }
  27.    }
  28.   );
  29.  });
  30. }

在以上代碼中,runSyncOrAsync 函數(shù)的回調(diào)函數(shù)內(nèi)部,會根據(jù)當(dāng)前 loader 對象 pitch 函數(shù)的返回值是否為 undefined 來執(zhí)行不同的處理邏輯。如果 pitch 函數(shù)返回了非 undefined 的值,則會出現(xiàn)熔斷。即跳過后續(xù)的執(zhí)行流程,開始執(zhí)行上一個(gè) loader 對象上的 normal loader 函數(shù)。具體的實(shí)現(xiàn)方式也很簡單,就是 loaderIndex 的值減 1,然后調(diào)用 iterateNormalLoaders 函數(shù)來實(shí)現(xiàn)。而如果 pitch 函數(shù)返回 undefined,則繼續(xù)調(diào)用 iteratePitchingLoaders 函數(shù)來處理下一個(gè)未處理 loader 對象。

五、Normal Loader 函數(shù)是如何被運(yùn)行的?

 
 
 
 
  1. // loader-runner/lib/LoaderRunner.js
  2. function iterateNormalLoaders(options, loaderContext, args, callback) {
  3.  if(loaderContext.loaderIndex < 0)
  4.   return callback(null, args);
  5.  var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
  6.  // normal loader的執(zhí)行順序是從右到左
  7.  if(currentLoaderObject.normalExecuted) {
  8.   loaderContext.loaderIndex--;
  9.   return iterateNormalLoaders(options, loaderContext, args, callback);
  10.  }
  11.   // 獲取當(dāng)前l(fā)oader對象上的normal函數(shù)
  12.  var fn = currentLoaderObject.normal;
  13.   // 標(biāo)識loader對象已經(jīng)被iterateNormalLoaders函數(shù)處理過
  14.  currentLoaderObject.normalExecuted = true;
  15.  if(!fn) { // 當(dāng)前l(fā)oader對象未定義normal函數(shù),則繼續(xù)處理前一個(gè)loader對象
  16.   return iterateNormalLoaders(options, loaderContext, args, callback);
  17.  }
  18.  convertArgs(args, currentLoaderObject.raw);
  19.  runSyncOrAsync(fn, loaderContext, args, function(err) {
  20.   if(err) return callback(err);
  21.   var args = Array.prototype.slice.call(arguments, 1);
  22.   iterateNormalLoaders(options, loaderContext, args, callback);
  23.  });
  24. }

由以上代碼可知,在 loader-runner 模塊內(nèi)部會通過調(diào)用 iterateNormalLoaders 函數(shù),來執(zhí)行已加載 loader 對象上的 normal loader 函數(shù)。與 iteratePitchingLoaders 函數(shù)一樣,在 iterateNormalLoaders 函數(shù)內(nèi)部也是通過調(diào)用 runSyncOrAsync 函數(shù)來執(zhí)行 fn 函數(shù)。不過在調(diào)用 normal loader 函數(shù)前,會先調(diào)用 convertArgs 函數(shù)對參數(shù)進(jìn)行處理。

convertArgs 函數(shù)會根據(jù) raw 屬性來對 args[0](文件的內(nèi)容)進(jìn)行處理,該函數(shù)的具體實(shí)現(xiàn)如下所示:

 
 
 
 
  1. // loader-runner/lib/LoaderRunner.js
  2. function convertArgs(args, raw) {
  3.  if(!raw && Buffer.isBuffer(args[0]))
  4.   args[0] = utf8BufferToString(args[0]);
  5.  else if(raw && typeof args[0] === "string")
  6.   args[0] = Buffer.from(args[0], "utf-8");
  7. }
  8. // 把buffer對象轉(zhuǎn)換為utf-8格式的字符串
  9. function utf8BufferToString(buf) {
  10.  var str = buf.toString("utf-8");
  11.  if(str.charCodeAt(0) === 0xFEFF) {
  12.   return str.substr(1);
  13.  } else {
  14.   return str;
  15.  }
  16. }

相信看完 convertArgs 函數(shù)的相關(guān)代碼之后,你對 raw 屬性的作用有了更深刻的了解。

六、Loader 函數(shù)體中的 this.callback 和 this.async 方法是哪里來的?

Loader 可以分為同步 Loader 和異步 Loader,對于同步 Loader 來說,我們可以通過 return 語句或 this.callback 的方式來同步地返回轉(zhuǎn)換后的結(jié)果。只是相比 return 語句,this.callback 方法則更靈活,因?yàn)樗试S傳遞多個(gè)參數(shù)。

sync-loader.js

 
 
 
 
  1. module.exports = function(source) {
  2.  return source + "-simple";
  3. };

sync-loader-with-multiple-results.js

 
 
 
 
  1. module.exports = function (source, map, meta) {
  2.   this.callback(null, source + "-simple", map, meta);
  3.   return; // 當(dāng)調(diào)用 callback() 函數(shù)時(shí),總是返回 undefined
  4. };

需要注意的是 this.callback 方法支持 4 個(gè)參數(shù),每個(gè)參數(shù)的具體作用如下所示:

 
 
 
 
  1. this.callback(
  2.   err: Error | null,    // 錯(cuò)誤信息
  3.   content: string | Buffer,    // content信息
  4.   sourceMap?: SourceMap,    // sourceMap
  5.   meta?: any    // 會被 webpack 忽略,可以是任何東西
  6. );

而對于異步 loader,我們需要調(diào)用 this.async 方法來獲取 callback 函數(shù):

async-loader.js

 
 
 
 
  1. module.exports = function(source) {
  2.  var callback = this.async();
  3.  setTimeout(function() {
  4.   callback(null, source + "-async-simple");
  5.  }, 50);
  6. };

那么以上示例中,this.callback 和 this.async 方法是哪里來的呢?帶著這個(gè)問題,我們來從 loader-runner 模塊的源碼中,一探究竟。

this.async

 
 
 
 
  1. // loader-runner/lib/LoaderRunner.js
  2. function runSyncOrAsync(fn, context, args, callback) {
  3.  var isSync = true; // 默認(rèn)是同步類型
  4.  var isDone = false; // 是否已完成
  5.  var isError = false; // internal error
  6.  var reportedError = false;
  7.   
  8.  context.async = function async() {
  9.   if(isDone) {
  10.    if(reportedError) return; // ignore
  11.    throw new Error("async(): The callback was already called.");
  12.   }
  13.   isSync = false;
  14.   return innerCallback;
  15.  };
  16. }

在前面我們已經(jīng)介紹過 runSyncOrAsync 函數(shù)的作用,該函數(shù)用于執(zhí)行 Loader 模塊中設(shè)置的 Normal Loader 或 Pitching Loader 函數(shù)。在 runSyncOrAsync 函數(shù)內(nèi)部,最終會通過 fn.apply(context, args) 的方式調(diào)用 Loader 函數(shù)。即會通過 apply 方法設(shè)置 Loader 函數(shù)的執(zhí)行上下文。

此外,由以上代碼可知,當(dāng)調(diào)用 this.async 方法之后,會先設(shè)置 isSync 的值為 false,然后返回 innerCallback 函數(shù)。其實(shí)該函數(shù)與 this.callback 都是指向同一個(gè)函數(shù)。

this.callback

 
 
 
 
  1. // loader-runner/lib/LoaderRunner.js
  2. function runSyncOrAsync(fn, context, args, callback) {
  3.   // 省略部分代碼
  4.  var innerCallback = context.callback = function() {
  5.   if(isDone) {
  6.    if(reportedError) return; // ignore
  7.    throw new Error("callback(): The callback was already called.");
  8.   }
  9.   isDone = true;
  10.   isSync = false;
  11.   try {
  12.    callback.apply(null, arguments);
  13.   } catch(e) {
  14.    isError = true;
  15.    throw e;
  16.   }
  17.  };
  18. }

如果在 Loader 函數(shù)中,是通過 return 語句來返回處理結(jié)果的話,那么 isSync 值仍為 true,將會執(zhí)行以下相應(yīng)的處理邏輯:

 
 
 
 
  1. // loader-runner/lib/LoaderRunner.js
  2. function runSyncOrAsync(fn, context, args, callback) {
  3.   // 省略部分代碼
  4.  try {
  5.   var result = (function LOADER_EXECUTION() {
  6.    return fn.apply(context, args);
  7.   }());
  8.   if(isSync) { // 使用return語句返回處理結(jié)果
  9.    isDone = true;
  10.    if(result === undefined)
  11.     r
    網(wǎng)站標(biāo)題:多圖詳解,一次性搞懂WebpackLoader
    網(wǎng)頁URL:http://www.5511xx.com/article/dhejepd.html