loader-runner
在Webpack的構建模塊週期使用,負責將文件路徑轉換爲文件內容。咱們添加的loader
最終會在這裏執行,另外資源文件也是在這裏被加載。文章使用2.4.0
版本代碼調試。node
整個loader-runner
基本上是圍繞如下功能實現,在進入源碼前首先咱們先了解它的功能場景:webpack
一個loader能夠定義兩類函數,一個默認導出的函數normalLoader
,一個用於阻斷常規流程的函數pitchLoader
,定義方式以下:git
// a-loader.js
// normal loader:
function aLoader(resource) {
// ...
return resource
}
// pitch loader:
aLoader.pitch = function() {}
module.exports = aLoader
複製代碼
若是咱們配置use: [ 'a-loader', 'b-loader', 'c-loader' ]
,且三個loader都沒有pitchLoader
或pitchLoader
無返回值,loader將會以如下流程執行:github
// |- a-loader `pitch` 沒有或無返回值
// |- b-loader `pitch` 沒有或無返回值
// |- c-loader `pitch` 沒有或無返回值
// |- load resource
// |- c-loader normal execution
// |- b-loader normal execution
// |- a-loader normal execution
複製代碼
若是在b-loader
的pitch
函數返回了某個值,流程將會變成下面這樣:web
// |- a-loader `pitch`
// |- b-loader `pitch` 有返回值
// |- a-loader normal execution
複製代碼
loader能夠支持以同步或異步(callback, Promise)方式運行,調用this.async()
獲取回調,並在執行完畢後調用。api
module.exports = function(resource) {
const callback = this.async()
asyncFunc((err, res) => {
callback(err, res)
})
}
複製代碼
咱們能夠經過這個參數指定loader接收一個buffer
類型的資源或string
類型的資源。緩存
在Webpack啓用watch
監聽時,若是loader添加文件依賴,那麼文件改變時會從新觸發loader。app
首先會對傳入參數解析並建立loaderContext
,在執行loader過程當中this
指向就是這個上下文,這裏定義了一些變量及函數供loader使用,例如在loader-utils
中獲取loader參數就是經過loaderContext取得。在解析完參數後,就開始進入第一個階段pitch
:異步
function runLoaders(options, callback) {
// 定義上下文
var loaderContext = options.context || {};
// 待解析的文件
loaderContext.resourcePath = options.resource;
// 待解析文件的目錄
loaderContext.context = dirname(options.resource);
// 當前執行到第幾個loader
loaderContext.loaderIndex = 0;
// 建立loader對象
loaderContext.loaders = options.loaders
// 執行Pitch階段
var processOptions = {
resourceBuffer: null,
readResource: fs.readFile.bind(fs),
};
iteratePitchingLoaders(processOptions, loaderContext, (err, res) => {
callback(null, {
// 最後通過loader輸出的值,可能爲buffer或string
result: result,
// 最原始的資源buffer
resourceBuffer: processOptions.resourceBuffer,
// 是否須要緩存結果
cacheable: requestCacheable,
// loader須要監聽的文件
fileDependencies: fileDependencies,
// loader須要監聽的文件夾
contextDependencies: contextDependencies
});
})
}
複製代碼
這裏採用了遞歸的方法來處理loader鏈式操做,當pitch
都執行完開始加載資源,當pitch
有返回值直接跳過加載資源,往回執行normalLoader
:async
function iteratePitchingLoaders(options, loaderContext, callback) {
// 若是全部loader的pitch都執行完,就開始執行loader
if(loaderContext.loaderIndex >= loaderContext.loaders.length)
return processResource(options, loaderContext, callback);
var loader = loaderContext.loaders[loaderContext.loaderIndex];
// 奇葩的遞歸執行操做,循環遞增條件放在這裏
if(loader.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(options, loaderContext, callback);
}
// 加載執行loader.pitch
loadLoader(loader, function(err) {
var fn = loader.pitch;
loader.pitchExecuted = true;
if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
runSyncOrAsync(fn, loaderContext,
[loaderContext.remainingRequest, loaderContext.previousRequest, loader.data = {}],
function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
// pitch有返回值,直接跳事後面的loader,並把返回值給其餘loader
if(args.length > 0) {
loaderContext.loaderIndex--;
iterateNormalLoaders(options, loaderContext, args, callback);
} else {
iteratePitchingLoaders(options, loaderContext, callback);
}
}
);
});
}
複製代碼
這裏會加載待處理的資源文件,並將其加入到文件監聽中,而後開始執行normalLoadr
:
function processResource(options, loaderContext, callback) {
// 開始往回執行loader
loaderContext.loaderIndex = loaderContext.loaders.length - 1;
loaderContext.addDependency(loaderContext.resourcePath);
options.readResource(loaderContext.resourcePath, function(err, buffer) {
if(err) return callback(err);
options.resourceBuffer = buffer;
iterateNormalLoaders(options, loaderContext, [buffer], callback);
});
}
複製代碼
這裏遞歸執行normalLoader
,在執行前會進行資源類型的轉換:
function iterateNormalLoaders(options, loaderContext, args, callback) {
// 全部loader執行完畢
if(loaderContext.loaderIndex < 0)
return callback(null, args);
var loader = loaderContext.loaders[loaderContext.loaderIndex];
// 奇葩的遞歸執行操做,循環遞增條件放在這裏
if(loader.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, args, callback);
}
var fn = loader.normal;
loader.normalExecuted = true;
// 若是loader須要buffer類型的資源數據,在這裏進行轉換
if(!raw && Buffer.isBuffer(args[0]))
args[0] = utf8BufferToString(args[0]);
else if(raw && typeof args[0] === "string")
args[0] = new Buffer(args[0], "utf-8");
// 執行loader
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的pitch
和raw
屬性都在這裏加載,使用node的require
加載:
function loadLoader(loader, callback) {
var module = require(loader.path);
loader.normal = module.default;
loader.pitch = module.pitch;
loader.raw = module.raw;
callback();
}
複製代碼
loader之因此能以同步或異步方式運行,是在這裏作了兼容處理:
function runSyncOrAsync(fn, context, args, callback) {
var isSync = true;
var isDone = false;
var reportedError = false;
// 異步loader要調用這個函數,先標誌爲異步返回
context.async = function async() {
isSync = false;
return innerCallback;
};
// 異步回調
var innerCallback = context.callback = function() {
isDone = true;
isSync = false;
callback.apply(null, arguments);
};
// 執行loader函數
var result = fn.apply(context, args);
if(isSync) {
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);
}
}
複製代碼