⚠️本文爲掘金社區首發簽約文章,未獲受權禁止轉載javascript
Webpack 是一個模塊化打包工具,它被普遍地應用在前端領域的大多數項目中。利用 Webpack 咱們不只能夠打包 JS 文件,還能夠打包圖片、CSS、字體等其餘類型的資源文件。而支持打包非 JS 文件的特性是基於 Loader 機制來實現的。所以要學好 Webpack,咱們就須要掌握 Loader 機制。本文阿寶哥將帶你們一塊兒深刻學習 Webpack 的 Loader 機制,閱讀完本文你將瞭解如下內容:css
raw
屬性有什麼做用?this.callback
和 this.async
方法是哪裏來的?由上圖可知,Loader 本質上是導出函數的 JavaScript 模塊。所導出的函數,可用於實現內容轉換,該函數支持如下 3 個參數:html
/** * @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
,具體配置方式以下所示:java
webpack.config.jsnode
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']
便可。webpack
Loader 本質上是導出函數的 JavaScript 模塊,而該模塊導出的函數(如果 ES6 模塊,則是默認導出的函數)就被稱爲 Normal Loader。須要注意的是,這裏咱們介紹的 Normal Loader 與 Webpack Loader 分類中定義的 Loader 是不同的。在 Webpack 中,loader 能夠被分爲 4 類:pre 前置、post 後置、normal 普通和 inline 行內。其中 pre 和 post loader,能夠經過 rule
對象的 enforce
屬性來指定:git
// 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。首先咱們先來建立一個新的目錄:es6
$ mkdir webpack-loader-demo
複製代碼
而後進入該目錄,使用 npm init -y
命令執行初始化操做。該命令成功執行後,會在當前目錄生成一個 package.json
文件:github
{
"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配置文件
複製代碼
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。
在開發 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 是如何被運行的。
要搞清楚 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
方法則更靈活,由於它容許傳遞多個參數。
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 最終返回的結果是如何被處理的的呢?下面咱們來簡單介紹一下。
// 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
函數進行進一步處理。這裏阿寶哥就不展開介紹了,感興趣的小夥伴能夠自行閱讀一下相關源碼。
最後咱們來回答前面留下的問題 —— 在 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 的時候,你可能就會使用到它們。若是你對如何編寫一個 Loader 感興趣的話,能夠閱讀 writing-a-loader 這個文檔或掘金上 手把手教你擼一個 Webpack Loader 這篇文章。