webpack 之 LoaderRunner 全方位揭祕

絕大多數人都在使用 webpack 做爲構建工具。那麼 loader 做爲處理各類資源的工具,你們確定也不會陌生。不少人沒寫過 loader,可是都對 loader 的具體怎麼寫,怎樣執行的一無所知。那麼本文就對 3.0.0 版本作一個全方位的揭祕。node

loader

所謂 loader 只是一個導出爲函數的 JavaScript 模塊。它接收上一個 loader 產生的結果或者資源文件(resource file)做爲入參。也能夠用多個 loader 函數組成 loader chain。compiler 須要獲得最後一個 loader 產生的處理結果。這個處理結果應該是 String 或者 Buffer(被轉換爲一個 string)。具體的用法,能夠看 Loader 官網的描述。接下來咱們從源碼的角度去分析,爲何能夠這樣作,爲何能夠實現同異步鉤子,內部究竟是怎麼實現的。那麼這就是 loader-runner 的做用所在。webpack

入口

loader-runner 是一個獨立出去的 npm 包,它的入口在 lib/LoaderRunner.jsgit

exports.runLoaders = function runLoaders(options, callback) {
	// read options
	var resource = options.resource || ""; // loaders 處理的資源
	var loaders = options.loaders || []; // loaders 配置
	var loaderContext = options.context || {}; // 全部 loaders 共享的數據
	var readResource = options.readResource || readFile; // 文件輸入系統

	//
	var splittedResource = resource && splitQuery(resource);
	var resourcePath = splittedResource ? splittedResource[0] : undefined; // 資源路徑
	var resourceQuery = splittedResource ? splittedResource[1] : undefined; // 資源的 query
	var contextDirectory = resourcePath ? dirname(resourcePath) : null; // 資源的目錄

	// execution state
	var requestCacheable = true; // 緩存的標識位
	var fileDependencies = []; // 文件依賴的緩存
	var contextDependencies = []; // 目錄依賴的緩存

	// prepare loader objects
	loaders = loaders.map(createLoaderObject); // 處理 loaders 的若干屬性
  // loaderContext 是在全部 loaders 處理資源時候共享的一份數據
  // loaderIndex 是一個指針,它控制了全部 loaders 的 pitch 與 normal 函數的執行
	loaderContext.context = contextDirectory;
	loaderContext.loaderIndex = 0;
	loaderContext.loaders = loaders;
	loaderContext.resourcePath = resourcePath;
	loaderContext.resourceQuery = resourceQuery;
	loaderContext.async = null; // 爲了實現異步 loader 的閉包函數
	loaderContext.callback = null; // 爲了實現同步或者異步 loader 的閉包函數
	loaderContext.cacheable = function cacheable(flag) {
		if(flag === false) {
			requestCacheable = false;
		}
	};
	loaderContext.dependency = loaderContext.addDependency = function addDependency(file) {
		fileDependencies.push(file);
	};
	loaderContext.addContextDependency = function addContextDependency(context) {
		contextDependencies.push(context);
	};
	loaderContext.getDependencies = function getDependencies() {
		return fileDependencies.slice();
  };
	loaderContext.getContextDependencies = function getContextDependencies() {
		return contextDependencies.slice();
  };
  // 清除全部緩存
	loaderContext.clearDependencies = function clearDependencies() {
		fileDependencies.length = 0;
		contextDependencies.length = 0;
		requestCacheable = true;
  };

  // 這些 getter/setter 都是爲了在 loader 函數裏面經過 this 求值能動態獲得對應的值
	Object.defineProperty(loaderContext, "resource", {
		enumerable: true,
		get: function() {
			if(loaderContext.resourcePath === undefined)
				return undefined;
			return loaderContext.resourcePath + loaderContext.resourceQuery;
		},
		set: function(value) {
			var splittedResource = value && splitQuery(value);
			loaderContext.resourcePath = splittedResource ? splittedResource[0] : undefined;
			loaderContext.resourceQuery = splittedResource ? splittedResource[1] : undefined;
		}
	});
	Object.defineProperty(loaderContext, "request", {
		enumerable: true,
		get: function() {
			return loaderContext.loaders.map(function(o) {
				return o.request;
			}).concat(loaderContext.resource || "").join("!");
		}
	});
	Object.defineProperty(loaderContext, "remainingRequest", {
		enumerable: true,
		get: function() {
			if(loaderContext.loaderIndex >= loaderContext.loaders.length - 1 && !loaderContext.resource)
				return "";
			return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(function(o) {
				return o.request;
			}).concat(loaderContext.resource || "").join("!");
		}
	});
	Object.defineProperty(loaderContext, "currentRequest", {
		enumerable: true,
		get: function() {
			return loaderContext.loaders.slice(loaderContext.loaderIndex).map(function(o) {
				return o.request;
			}).concat(loaderContext.resource || "").join("!");
		}
	});
	Object.defineProperty(loaderContext, "previousRequest", {
		enumerable: true,
		get: function() {
			return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(function(o) {
				return o.request;
			}).join("!");
		}
	});
	Object.defineProperty(loaderContext, "query", {
		enumerable: true,
		get: function() {
			var entry = loaderContext.loaders[loaderContext.loaderIndex];
			return entry.options && typeof entry.options === "object" ? entry.options : entry.query;
		}
	});
	Object.defineProperty(loaderContext, "data", {
		enumerable: true,
		get: function() {
			return loaderContext.loaders[loaderContext.loaderIndex].data;
		}
	});

	// 防止 loaderContext 加入新屬性
	if(Object.preventExtensions) {
		Object.preventExtensions(loaderContext);
	}

  // resourceBuffer 屬性是資源文件對應的 Buffer
	var processOptions = {
		resourceBuffer: null,
		readResource: readResource
  };
  // 整個 loaders 的 pitch 與 normal 函數的執行全流程
	iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
		// ...... 先省略
	});
};
複製代碼

runLoaders 函數接收 options 與 callback 做爲入參。es6

  1. options的處理

這一步是處理傳入的 options,而且生成一些屬性。其中 createLoaderObject 裏面是對每一個 loaders 的處理。github

function createLoaderObject(loader) {
  var obj = {
    path: null,
    query: null,
    options: null,
    ident: null,
    normal: null,
    pitch: null,
    raw: null,
    data: null,
    pitchExecuted: false,
    normalExecuted: false
  };
  Object.defineProperty(obj, "request", {
    enumerable: true,
    get: function() {
      return obj.path + obj.query;
    },
    set: function(value) {
      if(typeof value === "string") {
        var splittedRequest = splitQuery(value);
        obj.path = splittedRequest[0];
        obj.query = splittedRequest[1];
        obj.options = undefined;
        obj.ident = undefined;
      } else {
        if(!value.loader)
          throw new Error("request should be a string or object with loader and object (" + JSON.stringify(value) + ")");
        obj.path = value.loader;
        obj.options = value.options;
        obj.ident = value.ident;
        if(obj.options === null)
          obj.query = "";
        else if(obj.options === undefined)
          obj.query = "";
        else if(typeof obj.options === "string")
          obj.query = "?" + obj.options;
        else if(obj.ident)
          obj.query = "??" + obj.ident;
        else if(typeof obj.options === "object" && obj.options.ident)
          obj.query = "??" + obj.options.ident;
        else
          obj.query = "?" + JSON.stringify(obj.options);
      }
    }
  });
  obj.request = loader;
  if(Object.preventExtensions) {
    Object.preventExtensions(obj);
  }
  return obj;
}
複製代碼

loader 對象上會新增不少屬性,好比 normal 是對應 loader 暴露出來的函數,pitch 是 loader 的 pitch 函數。raw 也是 loader 暴露出來的屬性用來控制是否將資源轉成 Buffer 格式。pitchExecutednormalExecuted 用來標記當前 loader 的 pitch 與 normal 是否執行,在 iteratePitchingLoaders 內部是用來控制 loaderContext.loaderIndex 的變化。web

  1. loaderContext的處理

這個變量很是重要,它的 loaderIndex 屬性控制了全部 loaders 的 pitch 與 normal 執行的全部流程, async 是一個閉包函數,它是實現異步 loader 的關鍵,這是後話。 callback 也是一個閉包函數,不過它不只能實現同步 loader,還能實現異步 loader。npm

  1. getter/setter

接下來就是經過 Object.defineProperty 將 loaderContext 的一些屬性變成 getter/setter,這樣作的目的是爲了造成閉包的環境,每一個 loader 函數裏面經過 this 語法,可以動態的取到一些屬性,好比 dataqueryremainingRequest 等等。由於它們都會隨着 loaderIndex 變化而改變。api

  1. iteratePitchingLoaders

最後調用 iteratePitchingLoaders 開始全部 loaders 的 pitch 與 normal 函數的執行。數組

iteratePitchingLoaders

function iteratePitchingLoaders(options, loaderContext, callback) {
	// 若是全部 loaders 的 pitch 都執行完成
	if(loaderContext.loaderIndex >= loaderContext.loaders.length)
		return processResource(options, loaderContext, callback);

	var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

	// 進入下一個 loader 的 pitch 函數
	if(currentLoaderObject.pitchExecuted) {
		loaderContext.loaderIndex++;
		return iteratePitchingLoaders(options, loaderContext, callback);
	}

	// 加載當前 loader 的 pitch 與 normal 函數
	loadLoader(currentLoaderObject, function(err) {
		if(err) {
			loaderContext.cacheable(false);
			return callback(err);
		}
    var fn = currentLoaderObject.pitch;
    // 標識當前 loader 的 pitch 執行完成,就會走到上面的 loaderContext.loaderIndex++ 邏輯。
		currentLoaderObject.pitchExecuted = true;
		if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);

    // 調用當前 loader 的 pitch,決定是否進入下個 pitch,
    // 仍是跳事後面全部的 loader 的 pitch 與 normal(包括當前 normal)。
		runSyncOrAsync(
			fn,
			loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
			function(err) {
				if(err) return callback(err);
				var args = Array.prototype.slice.call(arguments, 1);

        // 若是當前 pitch 返回了一個不含有 `undefined` 的值
        // 那麼就放棄以後的 loader 的 pitch 與 normal 的執行。
				var hasArg = args.some(function(value) {
					return value !== undefined;
				});
				if(hasArg) {
					loaderContext.loaderIndex--;
					iterateNormalLoaders(options, loaderContext, args, callback);
				} else {
					iteratePitchingLoaders(options, loaderContext, callback);
				}
			}
		);
	});
}
複製代碼

能夠看出函數的內部,有多處調用 iteratePitchingLoaders 自身的邏輯,loaderContext.loaderIndexcurrentLoaderObject.pitchExecuted 在遞歸的邏輯當中發揮了決定性的做用。promise

在 webpack 官網當中,做者對 pitch loader 作了一些描述。

loaders 是從右向左開始執行,可是有些時候 loaders 只關心 metadata,也就是 loaderContext 上面的一些共享的屬性,這也就是 pitch 方法存在的意義。它的調用是從左向右執行,而且還能跳過某些 loader 的執行。

舉個例子:

module.exports = {
  //...
  module: {
    rules: [
      {
        //...
        use: [
          'a-loader',
          'b-loader',
          'c-loader'
        ]
      }
    ]
  }
};
複製代碼

那麼 iteratePitchingLoaders 方法的執行流程以下:

|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
      |- requested module is picked up as a dependency
    |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution
複製代碼

從原來分析來看:首先,loaderContext.loaderIndex 是從 0 開始遞增,說明 pitch 的執行就是從左向右的順序。接着就會走到 loadLoader 的邏輯,這個邏輯就是加載 loader 模塊。

module.exports = function loadLoader(loader, callback) {
	try {
		var module = require(loader.path);
	} catch(e) {
		// it is possible for node to choke on a require if the FD descriptor
		// limit has been reached. give it a chance to recover.
		if(e instanceof Error && e.code === "EMFILE") {
			var retry = loadLoader.bind(null, loader, callback);
			if(typeof setImmediate === "function") {
				// node >= 0.9.0
				return setImmediate(retry);
			} else {
				// node < 0.9.0
				return process.nextTick(retry);
			}
		}
		return callback(e);
	}
	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();
};
複製代碼

loadLoader 的執行很簡單,就是加載 loader 這個 module,而且掛載 normalpitchraw 三個屬性,raw 屬性用來控制讀入資源文件的是否是 Buffer 類型。

既然 loadLoader 加載完 loader 以後,那麼就走到它的回調函數裏面。

loadLoader(currentLoaderObject, function(err) {
  if(err) {
    loaderContext.cacheable(false);
    return callback(err);
  }
  var fn = currentLoaderObject.pitch;
  // 標識當前 loader 的 pitch 執行完成,就會走到上面的 loaderContext.loaderIndex++ 邏輯。
  currentLoaderObject.pitchExecuted = true;
  if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);

  // 調用當前 loader 的 pitch,決定是否進入下個 pitch,
  // 仍是跳事後面全部的 loader,執行當前 loader 的 normal 函數。
  runSyncOrAsync(
    fn,
    loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
    function(err) {
      if(err) return callback(err);
      var args = Array.prototype.slice.call(arguments, 1);

      // 若是當前 pitch 返回了一個不含有 `undefined` 的值
      // 那麼就放棄以後的 loader 的 pitch 與 normal 的執行。
      var hasArg = args.some(function(value) {
        return value !== undefined;
      });
      if(hasArg) {
        loaderContext.loaderIndex--;
        iterateNormalLoaders(options, loaderContext, args, callback);
      } else {
        iteratePitchingLoaders(options, loaderContext, callback);
      }
    }
  );
});
複製代碼

若是當前 fn(即 loader.pitch) 不存在,那麼就走進下一個 loader 的 pitch 的邏輯。這樣一步一步的遞歸 iteratePitchingLoaders,直到全部 loader 的 pitch 函數都執行完,那麼就走到下面處理資源文件的邏輯。

if(loaderContext.loaderIndex >= loaderContext.loaders.length)
    return processResource(options, loaderContext, callback);

// 開始讀入資源文件
function processResource(options, loaderContext, callback) {
	// set loader index to last loader
	loaderContext.loaderIndex = loaderContext.loaders.length - 1;

	var resourcePath = loaderContext.resourcePath;
	if(resourcePath) {
		loaderContext.addDependency(resourcePath);
		options.readResource(resourcePath, function(err, buffer) {
			if(err) return callback(err);
			options.resourceBuffer = buffer;
			iterateNormalLoaders(options, loaderContext, [buffer], callback);
		});
	} else {
		iterateNormalLoaders(options, loaderContext, [null], callback);
	}
}
複製代碼

processResource 的執行,表明着 pitch 的執行都完成了,開始讀入資源文件的 buffer。

loaderContext.loaderIndex = loaderContext.loaders.length - 1;
複製代碼

先將 loaderIndex 設置爲最後一個,即 normal 的執行是逆向的。接着調用 addDependency 將當前資源文件的路徑推入 fileDependencies 數組,也就是在整個資源文件被 loaders 處理的過程中,都能拿到這個 fileDependencies 數組的數據,進而開始調用 iterateNormalLoaders 來執行 loaders 的 normal 函數。咱們來看下 iterateNormalLoaders 函數的執行。

iterateNormalLoaders

function iterateNormalLoaders(options, loaderContext, args, callback) {
	if(loaderContext.loaderIndex < 0)
		return callback(null, args);

	var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

	// iterate
	if(currentLoaderObject.normalExecuted) {
		loaderContext.loaderIndex--;
		return iterateNormalLoaders(options, loaderContext, args, callback);
	}

	var fn = currentLoaderObject.normal;
	currentLoaderObject.normalExecuted = true;
	if(!fn) {
		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);
	});
}
複製代碼

若是 loaderContext.loaderIndex 已經小於 0,那麼就執行 iteratePitchingLoaders 的回調函數,進而退出遞歸。若是當前 loader 的 normal 函數執行完了,也就是當前 loader.normalExecuted 的值爲 true,就開始遞減 loaderContext.loaderIndex,接着執行下一個 loader,不然就執行當前 loader 的 normal 函數,而且將 normalExecuted 屬性設置爲 true,這樣下次遞歸 iterateNormalLoaders 的時候,就能進入下一個 loader 的執行了。執行當前 loader 會先調用 convertArgs 來決定是否將上一個 loader 傳入的 result 轉化爲 buffer。看下 convertArgs的邏輯:

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

邏輯很簡單,就是判斷若是當前 loader 導出的函數的 raw 屬性爲 true,那麼就轉化上一個 loader 傳入的 result 爲 buffer。

最後走入 runSyncOrAsync 函數,這個函數是 loader-runner 最核心的一步,它決定了當前 loader 的走向,支持異步 loader,同步 loader,也支持返回 promise 的 loader。

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;
	};
	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;
		}
	};
	try {
		var result = (function LOADER_EXECUTION() {
			return 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);
		}
	} catch(e) {
		if(isError) throw e;
		if(isDone) {
			// loader is already "done", so we cannot use the callback function
			// for better debugging we print the error on the console
			if(typeof e === "object" && e.stack) console.error(e.stack);
			else console.error(e);
			return;
		}
		isDone = true;
		reportedError = true;
		callback(e);
	}
}
複製代碼

函數內部的 isSyncisDone 很重要,isSync 是來控制同步仍是異步 loader的,isDone 是防止 callback 被觸發屢次。context.async 是一個閉包函數,它返回的是 innerCallback,而 innerCallback 內部纔是真正執行 runSyncOrAsync 的 callback 函數,這個 callback 會進入下一次的 iterateNormalLoaders 邏輯。同時 innerCallback 也是 context.callback 的一個引用。真正執行 loader 的 normal 的函數語句在下面的這個當即執行函數裏面。

try {
  var result = (function LOADER_EXECUTION() {
    return 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_EXECUTION 函數內部調用了 fn,即 loader 的 normal 函數,而且綁定了上下文 context,context 就是 runLoaders 內部聲明的 loaderContext。它擁有不少屬性和方法,這也就是爲啥咱們在 loader 裏面可以經過 this 獲取到它的屬性和方法。

module.exports = function(content, map, meta) {
  // 獲取到 loaderContext.async
  var callback = this.async();
  // 獲取 loaderContext.callback
  var callback = this.callback;
  // 獲取當前 loader 索引
  var index = this.loaderIndex
  // 等等。
};
複製代碼

而後根據 resultisDone 來決定如何調用 callback 來進入下一個 loader 到 normal 函數。這裏有三種狀況:

  1. 同步 loader
module.exports = function(content, map, meta) {
  return someSyncOperation(content);
};
module.exports = function(content, map, meta) {
  return this.callback(null, content);
};
複製代碼

若是是同步 loader,那麼 isSync 爲 true,這裏判斷若是 result 是一個 promise,那麼等這個 promise 完成以後,調用 callback,不然就調用 callback。

  1. 異步 loader
module.exports = function(content, map, meta) {
  var callback = this.async();
  someAsyncOperation(content, function(err, result) {
    if (err) return callback(err);
    callback(null, result, map, meta);
  });
};

module.exports = function(content, map, meta) {
  var callback = this.async();
  someAsyncOperation(content, function(err, result) {
    if (err) return callback(err);
    callback(null, result, map, meta);
  });
};
複製代碼

若是是異步 loader,那麼必須調用 this.callback(/** arguments */) 或者 this.aysnc(),由於這兩個語法其實就是執行 innerCallback 函數,內部會將 isSync 設置爲 false,這樣就不會走到同步 loader 的 if(isSync) 邏輯。並且 innerCallback 函數的內部會調用 callback,進而走到 iterateNormalLoaders 的執行,這樣又進入了下一個 loader 的 normal 函數了。

那麼 runLoaders 的總體執行流程以下圖:

loader 執行圖

iteratePitchingLoaders 內的 runSyncOrAsync

剛纔講到的是 loader 的 normal 函數的執行都是在 runSyncOrAsync 內部。其實在咱們將 pitch 的時候,也是會執行 runSyncOrAsync,而 pitch 函數的返回,會影響以後全部 loaders 的 pitch 和 normal 階段。它的邏輯在 loadLoader 的回調裏面,代碼以下:

loadLoader(currentLoaderObject, function(err) {
  if(err) {
    loaderContext.cacheable(false);
    return callback(err);
  }
  var fn = currentLoaderObject.pitch;
  // 標識當前 loader 的 pitch 執行完成,就會走到上面的 loaderContext.loaderIndex++ 邏輯。
  currentLoaderObject.pitchExecuted = true;
  if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);

  // 調用當前 loader 的 pitch,決定是否進入下個 pitch,
  // 仍是跳事後面全部的 loader 的 pitch 與 normal(包括當前 normal)函數。
  runSyncOrAsync(
    fn,
    loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
    function(err) {
      if(err) return callback(err);
      var args = Array.prototype.slice.call(arguments, 1);

      // 若是當前 pitch 返回了一個不含有 `undefined` 的值
      // 那麼就放棄以後的 loader 的 pitch 與 normal 的執行。
      var hasArg = args.some(function(value) {
        return value !== undefined;
      });
      if(hasArg) {
        loaderContext.loaderIndex--;
        iterateNormalLoaders(options, loaderContext, args, callback);
      } else {
        iteratePitchingLoaders(options, loaderContext, callback);
      }
    }
  );
});
複製代碼

根據上述咱們對 runSyncOrAsync 的分析,args 是取決於 pitch 函數的返回。若是 pitch 只要返回的值都是 undefined,那麼直接走到 iterateNormalLoaders 的邏輯,也就是跳過 processResource 與後面全部 loaders 的 pitch 與 normal 的執行(包括當前 loader 的 normal)。舉個列子:

// webpack rules 配置
rules: [
    {
      //...
      use: [
        'a-loader',
        'b-loader',
        'c-loader'
      ]
    }
  ]
}

// a-loader.js
module.exports = function(source) {
	return source + "-simple";
};

// b-loader.js(pitch)
module.exports = function(source) {
	return "I am b-loader.js";
};
module.exports.pitch = function(remainingRequest, previousRequest, data) {
	return 'pitching B'
};

// c-loader.js
module.exports = function(source) {
	return "this loader is ignored?";
};
module.exports.pitch = function(source) {
	return "pitching C won't be excuted";
};

複製代碼

因爲第二個 b-loader.js 含有 pitch,而且返回了不爲 undefined 的值,因此 b-loader.js 的 normal 函數不會執行。c-loader.js 的 pitch 與 normal 函數也不會被執行。

總結

從 LoaderRunner 的源碼來看,源碼的設計很是靈活,引入了 pitch 的概念,而且支持同異步的 loader 和返回 promise 的同步 loader。相信,經歷了這篇文章,你對 loader 的概念和執行已經很清晰了,接下來就是看看一些比較有名的 loader,鞏固如何去寫 loader 了。

相關文章
相關標籤/搜索