經過非常非常長無聊的流程,只是將獲取到的module信息做了一些緩存,然後生成了loaderContext對象。 這裡上個圖整理一下這節的流程: 這一節來看webpack是如何將babel-loader與js文件結合的,首先總覽一下runLoaders函數: 傳入的4個參數都很直白: 1、待處理文件 ...
經過非常非常長無聊的流程,只是將獲取到的module信息做了一些緩存,然後生成了loaderContext對象。
這裡上個圖整理一下這節的流程:
這一節來看webpack是如何將babel-loader與js文件結合的,首先總覽一下runLoaders函數:
/* options => { resource: 'd:\\workspace\\doc\\input.js', loaders: [ { loader: 'd:\\workspace\\node_modules\\babel-loader\\lib\\index.js' } ], context: loaderContext, readResource: fs.readFile.bind(fs) } */ exports.runLoaders = function runLoaders(options, callback) { // read options var resource = options.resource || ""; var loaders = options.loaders || []; var loaderContext = options.context || {}; var readResource = options.readResource || readFile; // 簡單講就是獲取入口文件的絕對路徑、參數、目錄 var splittedResource = resource && splitQuery(resource); var resourcePath = splittedResource ? splittedResource[0] : undefined; var resourceQuery = splittedResource ? splittedResource[1] : undefined; var contextDirectory = resourcePath ? dirname(resourcePath) : null; // execution state var requestCacheable = true; var fileDependencies = []; var contextDependencies = []; // prepare loader objects loaders = loaders.map(createLoaderObject); // 將屬性都掛載到loaderContext上面 loaderContext.context = contextDirectory; loaderContext.loaderIndex = 0; loaderContext.loaders = loaders; loaderContext.resourcePath = resourcePath; loaderContext.resourceQuery = resourceQuery; loaderContext.async = null; loaderContext.callback = null; 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; }; // 定義大量的特殊屬性 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 // finish loader context if (Object.preventExtensions) { Object.preventExtensions(loaderContext); } var processOptions = { resourceBuffer: null, readResource: readResource }; iteratePitchingLoaders(processOptions, loaderContext, function(err, result) { if (err) { return callback(err, { cacheable: requestCacheable, fileDependencies: fileDependencies, contextDependencies: contextDependencies }); } callback(null, { result: result, resourceBuffer: processOptions.resourceBuffer, cacheable: requestCacheable, fileDependencies: fileDependencies, contextDependencies: contextDependencies }); }); };
傳入的4個參數都很直白:
1、待處理文件絕對路徑
2、文件尾碼對應的loader入口文件絕對路徑
3、對應的loaderContext對象
4、fs對象
前面所有的事都是為了生成前3個屬性,在這裡整合在一起開始做轉換處理。
createLoaderObject
這裡有一個需要簡單看的地方,就是對loaders數組做了一封封裝:
// prepare loader objects loaders = loaders.map(createLoaderObject);
簡單看一下這個函數:
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 }; // 定義request屬性的get/set 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 { // value => { loader: 'd:\\workspace\\node_modules\\babel-loader\\lib\\index.js' } if (!value.loader) throw new Error("request should be a string or object with loader and object (" + JSON.stringify(value) + ")"); // 這麼多行代碼其實只有第一行有用 // 即obj.path = 'd:\\workspace\\node_modules\\babel-loader\\lib\\index.js' 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); } } }); // 這裡會觸發上面的set obj.request = loader; // 封裝 if (Object.preventExtensions) { Object.preventExtensions(obj); } return obj; }
最後做封裝,然後返回一個obj。
將屬性全部掛載在loaderContext上面,最後也是調用Object.preventExtensions將屬性凍結,禁止添加任何新的屬性。
完成對象的安裝後,最後調用了迭代器方法,這裡看一下iteratePitchingLoaders方法內部實現:
function iteratePitchingLoaders(options, loaderContext, callback) { // abort after last loader // loaderIndex初始為0 if (loaderContext.loaderIndex >= loaderContext.loaders.length) return processResource(options, loaderContext, callback); // 取出之前的obj var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // iterate // 預設是false 代表當前loader未被載入過 if (currentLoaderObject.pitchExecuted) { loaderContext.loaderIndex++; return iteratePitchingLoaders(options, loaderContext, callback); } // load loader module loadLoader(currentLoaderObject, function(err) { // ... }); }
取出來loader對象後,調用loadLoader來載入loader,看一眼:
module.exports = function loadLoader(loader, callback) { // 不知道這個System是什麼環境下的變數 // node環境是global // 瀏覽器環境是window if (typeof System === "object" && typeof System.import === "function") { // ... } else { 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 loader !== "function" && typeof loader !== "object") throw new Error("Module '" + loader.path + "' is not a loader (export function or es6 module))"); // babel-loader返回的module是一個function 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") throw new Error("Module '" + loader.path + "' is not a loader (must have normal or pitch function)"); callback(); } };
這裡就涉及到loader的返回值,通過直接讀取babel-loader的入口文件,最後返回了一個function,後面兩個屬性babel-loader並沒有給,是undefined。
這裡把babel-loader返回值掛載到loader上後,就調用了無參回調函數,如下:
loadLoader(currentLoaderObject, function(err) { if (err) return callback(err); // 剛纔也說了這個是undefined var fn = currentLoaderObject.pitch; // 這個表明loader已經被調用了 下次再遇到就會直接跳過 currentLoaderObject.pitchExecuted = true; if (!fn) return iteratePitchingLoaders(options, loaderContext, callback); runSyncOrAsync( fn, loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}], function(err) { if (err) return callback(err); var args = Array.prototype.slice.call(arguments, 1); if (args.length > 0) { loaderContext.loaderIndex--; iterateNormalLoaders(options, loaderContext, args, callback); } else { iteratePitchingLoaders(options, loaderContext, callback); } } ); });
這裡把loader的一個標記置true,然後根據返回函數是否有pitch值來決定流程,很明顯這裡直接遞歸調用自身了。
第二次進來時,由於loader已經被載入,所以loaderIndex加1,然後再次遞歸。
第三次進來時,第一個判斷中表明所有的loader都被載入完,會調用processResource方法。
processResource
這裡的遞歸由於都是尾遞歸,所以在性能上不會有問題,直接看上面的方法:
// options => 包含fs方法的對象 // loaderContext => 包含loader路徑、返回值等的對象 function processResource(options, loaderContext, callback) { // 從後往前調用loader loaderContext.loaderIndex = loaderContext.loaders.length - 1; // 獲取入口文件路徑 var resourcePath = loaderContext.resourcePath; if (resourcePath) { /* loaderContext.dependency = loaderContext.addDependency = function addDependency(file) { fileDependencies.push(file); }; */ loaderContext.addDependency(resourcePath); // readResource => fs.readFile 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); } }
這個獲取入口文件路徑並調用fs模塊進行文件內容讀取,返迴文件的原始buffer後調用了iterateNormalLoaders方法。
function iterateNormalLoaders(options, loaderContext, args, callback) { // 當所有loader執行完後返回 if (loaderContext.loaderIndex < 0) return callback(null, args); // 取出當前的loader var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // iterate // 預設為false 跟另外一個標記類似 代表該loader在此方法是否被調用過 if (currentLoaderObject.normalExecuted) { loaderContext.loaderIndex--; return iterateNormalLoaders(options, loaderContext, args, callback); } // 讀取返回的module var fn = currentLoaderObject.normal; // 標記置true currentLoaderObject.normalExecuted = true; if (!fn) { return iterateNormalLoaders(options, loaderContext, args, callback); } /* function convertArgs(args, raw) { 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"); } function utf8BufferToString(buf) { var str = buf.toString("utf-8"); if (str.charCodeAt(0) === 0xFEFF) { return str.substr(1); } else { return str; } } */ // 該方法將原始的buffer轉換為utf-8的字元串 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); }); }
這裡的normal就是處理普通的js文件了,在讀取入口文件後將其轉換為utf-8的格式,然後依次獲取loader,調用runSyncOrAsync。
源碼如下:
/* fn => 讀取babel-loader返回的函數 context => loader的輔助對象 args => 讀取入口文件返回的字元串 */ 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 { // 可以可以 // 老子看了這麼久源碼就是等這個方法 // 還裝模作樣的弄個IIFE 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.catch(callback).then(function(r) { callback(null, r); }); } 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); } }
看了那麼多的垃圾代碼,終於來到了最關鍵的方法,可以看出,本質上loader就是將讀取到的字元串傳入,然後返回對應的字元串或者一個Promise。
這裡一路將結果一路返回到了最初的runLoaders方法中:
iteratePitchingLoaders(processOptions, loaderContext, function(err, result) { if (err) { return callback(err, { cacheable: requestCacheable, fileDependencies: fileDependencies, contextDependencies: contextDependencies }); } /* result => babel-loader轉換後的字元串 resourceBuffer => JS文件的原始buffer cacheable => [Function] fileDependencies => ['d:\\workspace\\doc\\input.js'] contextDependencies => [] */ callback(null, { result: result, resourceBuffer: processOptions.resourceBuffer, cacheable: requestCacheable, fileDependencies: fileDependencies, contextDependencies: contextDependencies }); });
因為案例比較簡單,所以返回的東西也比較少,這裡繼續callback,返回到doBuild:
doBuild(options, compilation, resolver, fs, callback) { this.cacheable = false; const loaderContext = this.createLoaderContext(resolver, options, compilation, fs); runLoaders({ resource: this.resource, loaders: this.loaders, context: loaderContext, readResource: fs.readFile.bind(fs) }, (err, result) => { // result => 上面的對象 if (result) { this.cacheable = result.cacheable; this.fileDependencies = result.fileDependencies; this.contextDependencies = result.contextDependencies; } if (err) { const error = new ModuleBuildError(this, err); return callback(error); } // 獲取對應的原始buffer、轉換後的字元串、sourceMap const resourceBuffer = result.resourceBuffer; const source = result.result[0]; // null const sourceMap = result.result[1]; if (!Buffer.isBuffer(source) && typeof source !== "string") { const error = new ModuleBuildError(this, new Error("Final loader didn't return a Buffer or String")); return callback(error); } /* function asString(buf) { if (Buffer.isBuffer(buf)) { return buf.toString("utf-8"); } return buf; } */ this._source = this.createSource(asString(source), resourceBuffer, sourceMap); return callback(); }); }
這次獲取處理完的對象屬性,然後調用另外一個createSource方法:
createSource(source, resourceBuffer, sourceMap) { // if there is no identifier return raw source if (!this.identifier) { return new RawSource(source); } // from here on we assume we have an identifier // 返回下麵這個東西 很久之前拼接的 // d:\workspace\node_modules\babel-loader\lib\index.js!d:\workspace\doc\input.js const identifier = this.identifier(); // 下麵兩個屬性根本沒出現過 if (this.lineToLine && resourceBuffer) { return new LineToLineMappedSource( source, identifier, asString(resourceBuffer)); } if (this.useSourceMap && sourceMap) { return new SourceMapSource(source, identifier, sourceMap); } // 直接進這裡 /* class OriginalSource extends Source { constructor(value, name) { super(); this._value = value; this._name = name; } //...原型方法 } */ return new OriginalSource(source, identifier); }
因為都比較簡單,所以直接看註釋就好了,沒啥好解釋的。
所有的new都只看看構造函數,方法那麼多,又不是全用。
返回的對象賦值給了NormalModule對象的_source屬性,然後又是callback,這次回到了build那裡:
build(options, compilation, resolver, fs, callback) { this.buildTimestamp = Date.now(); this.built = true; this._source = null; this.error = null; this.errors.length = 0; this.warnings.length = 0; this.meta = {}; return this.doBuild(options, compilation, resolver, fs, (err) => { this.dependencies.length = 0; this.variables.length = 0; this.blocks.length = 0; this._cachedSource = null; // if we have an error mark module as failed and exit if (err) { this.markModuleAsErrored(err); return callback(); } // check if this module should !not! be parsed. // if so, exit here; // undefined跳過 const noParseRule = options.module && options.module.noParse; if (this.shouldPreventParsing(noParseRule, this.request)) { return callback(); } try { this.parser.parse(this._source.source(), { current: this, module: this, compilation: compilation, options: options }); } catch (e) { const source = this._source.source(); const error = new ModuleParseError(this, source, e); this.markModuleAsErrored(error); return callback(); } return callback(); }); }
基本上不知道module.noParser選項哪個人會用,所以這裡一般都是直接跳過然後調用那個可怕對象parser對象的parse方法,開始進行解析。
這節的內容就這樣吧,總算是把loader跑完了,這個系列的目的也就差不多了。
其實總體來說過程就幾步,但是代碼的複雜程度真的是不想說了……