Webpack源碼分析 - loader-runner

loader-runner

loader-runner在Webpack的構建模塊週期使用,負責將文件路徑轉換爲文件內容。咱們添加的loader最終會在這裏執行,另外資源文件也是在這裏被加載。文章使用2.4.0版本代碼調試。node

loader基礎功能

整個loader-runner基本上是圍繞如下功能實現,在進入源碼前首先咱們先了解它的功能場景:webpack

loader執行流程 normal 和 pitch

一個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都沒有pitchLoaderpitchLoader無返回值,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-loaderpitch函數返回了某個值,流程將會變成下面這樣: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.raw

咱們能夠經過這個參數指定loader接收一個buffer類型的資源或string類型的資源。緩存

監聽文件改變 addDependency

在Webpack啓用watch監聽時,若是loader添加文件依賴,那麼文件改變時會從新觸發loader。app

核心源碼解析

runLoaders - 入口函數

首先會對傳入參數解析並建立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
		});
    })
}
複製代碼

iteratePitchingLoaders - 執行pitchLoader

這裏採用了遞歸的方法來處理loader鏈式操做,當pitch都執行完開始加載資源,當pitch有返回值直接跳過加載資源,往回執行normalLoaderasync

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);
				}
			}
		);
	});
}
複製代碼

processResource - 加載資源文件

這裏會加載待處理的資源文件,並將其加入到文件監聽中,而後開始執行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);
    });
}
複製代碼

iterateNormalLoaders 執行normalLoader

這裏遞歸執行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);
	});
}
複製代碼

loadLoader - 加載loader

loader的pitchraw屬性都在這裏加載,使用node的require加載:

function loadLoader(loader, callback) {
    var module = require(loader.path);
    loader.normal = module.default;
    loader.pitch = module.pitch;
    loader.raw = module.raw;
    callback();
}
複製代碼

runSyncOrAsync - loader執行包裝

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);
    }
}

複製代碼

參考資料

loader-runner github

loader-api

loader集合

編寫一個loader

相關文章
相關標籤/搜索