在上一節中,最後返回了一個resolver,本質上就是一個Resolver對象:vue
resolver = new Resolver(fileSystem);
這個對象的構造函數很是簡單,只是簡單的繼承了Tapable,並接收了fileSystem參數:webpack
function Resolver(fileSystem) { Tapable.call(this); this.fileSystem = fileSystem; } module.exports = Resolver;
resolveweb
而在make事件流中,調用的正是該類的原型方法resolve,如今能夠進行看一眼了:緩存
/* context => { issuer: '', compiler: undefined } path => 'd:\\workspace\\doc' request => './input.js' callback => [Function] */ Resolver.prototype.resolve = function resolve(context, path, request, callback) { if (arguments.length === 3) { throw new Error("Signature changed: context parameter added"); } var resolver = this; // 包裝參數 var obj = { context: context, path: path, request: request }; var localMissing; var log; // message => resolve './input.js' in 'd:\\workspace\\doc' var message = "resolve '" + request + "' in '" + path + "'"; function writeLog(msg) { log.push(msg); } function logAsString() { return log.join("\n"); } function onError(err, result) { /**/ } function onResolve(err, result) { /**/ } // 這兩個並不存在 onResolve.missing = callback.missing; onResolve.stack = callback.stack; // 調用另外一個原型方法 return this.doResolve("resolve", obj, message, onResolve); };
須要注意的是,該方法會在webpack編譯期間被調用屢次,這裏的參數僅僅是第一次被調用時的。app
doResolveide
簡單的說,resolve方法將參數進行二次包裝後,調用了另一個原型方法doResolve,源碼整理以下:函數
/* type => 'resolve' request => { context: { issuer: '', compiler: undefined }, path: 'd:\\workspace\\doc', request: './input.js' } message => resolve './input.js' in 'd:\\workspace\\doc' callback => doResolve() */ Resolver.prototype.doResolve = function doResolve(type, request, message, callback) { var resolver = this; // stackLine => resolve: (d:\workspace\doc) ./input.js var stackLine = type + ": (" + request.path + ") " + (request.request || "") + (request.query || "") + (request.directory ? " directory" : "") + (request.module ? " module" : ""); var newStack = [stackLine]; // 暫無 if (callback.stack) { /**/ } // 沒這個事件流 resolver.applyPlugins("resolve-step", type, request); // before-resolve var beforePluginName = "before-" + type; // 檢測是否存在對應的before事件流 if (resolver.hasPlugins(beforePluginName)) { /**/ } // 走正常流程 else { runNormal(); } }
因爲callback的missing、stack屬性均爲undefined,因此會直接跳過那個if判斷。this
而事件流resolve-step、before-resolve也不存在,因此會直接走最後的else,進入runNormal方法。spa
這裏全面描述一下doResolve,方法內部有5個函數,分別名爲beforeInnerCallback、runNormal、innerCallback、runAfter、afterInnerCallback,全部的callback函數都負責包裝對應事件流的回調函數。prototype
源碼以下:
// 先判斷是否存在before-type事件流 if (resolver.hasPlugins(beforePluginName)) { // 觸發完調用回調 resolver.applyPluginsAsyncSeriesBailResult1(beforePluginName, request, createInnerCallback(beforeInnerCallback, { log: callback.log, missing: callback.missing, stack: newStack }, message && ("before " + message), true)); } // 不存在跳過直接觸發type事件流 else { runNormal(); } function beforeInnerCallback(err, result) { if (arguments.length > 0) { if (err) return callback(err); if (result) return callback(null, result); return callback(); } // 這裏進入下一階段 runNormal(); } // 觸發type事件流 function runNormal() { if (resolver.hasPlugins(type)) { /**/ } else { runAfter(); } } function innerCallback(err, result) { /**/ } // 觸發after-type function runAfter() { var afterPluginName = "after-" + type; // 這裏就是直接調用callback了 if (resolver.hasPlugins(afterPluginName)) { /**/ } else { callback(); } } function afterInnerCallback(err, result) { /**/ }
能夠看到邏輯很簡單,每個事件流type存在3個類型:before-type、type、after-type,doResolve會嘗試依次觸發每個階段的事件流。
在上面的例子中,由於不存在before-resolve事件流,因此會調用runNormal方法去觸發resolve的事件流。
若是存在,觸發對應的事件流,並在回調函數中觸發下一階段的事件流。
因此這裏的調用就能夠用一句話歸納:嘗試觸發before-resolve、resolve、after-resolve事件流後,調用callback。
unsafeCache
resolve事件流均來源於上一節第三部分注入的開頭,以下:
// resolve if (unsafeCache) { plugins.push(new UnsafeCachePlugin("resolve", cachePredicate, unsafeCache, cacheWithContext, "new-resolve")); plugins.push(new ParsePlugin("new-resolve", "parsed-resolve")); } else { plugins.push(new ParsePlugin("resolve", "parsed-resolve")); }
UnsafeCachePlugin
這個unsafeCache雖然不知道是啥,可是通常不會去設置,默認狀況下是true,所以進入UnsafeCachePlugin插件,構造函數以下:
/* source => resolve filterPredicate => function(){return true} cache => {} withContext => false target => new-resolve */ function UnsafeCachePlugin(source, filterPredicate, cache, withContext, target) { this.source = source; this.filterPredicate = filterPredicate; this.withContext = withContext; this.cache = cache || {}; this.target = target; }
基本上只是對傳入參數的獲取,直接看事件流的內容:
function getCacheId(request, withContext) { // 直接用配置對象的字符串形式做爲緩存對象key // 貌似vue源碼的compile也是這樣的 return JSON.stringify({ context: withContext ? request.context : "", path: request.path, query: request.query, request: request.request }); } UnsafeCachePlugin.prototype.apply = function(resolver) { var filterPredicate = this.filterPredicate; var cache = this.cache; var target = this.target; var withContext = this.withContext; // 這裏注入resolve事件流 /* request => { context: { issuer: '', compiler: undefined }, path: 'd:\\workspace\\doc', request: './input.js' } callback => createInnerCallback(innerCallback,{...}) */ resolver.plugin(this.source, function(request, callback) { // 這裏永遠是true if (!filterPredicate(request)) return callback(); // 嘗試獲取緩存 var cacheId = getCacheId(request, withContext); var cacheEntry = cache[cacheId]; if (cacheEntry) { return callback(null, cacheEntry); } // 這裏再次調用了doResolve函數 // target => new-resolve resolver.doResolve(target, request, null, createInnerCallback(function(err, result) { if (err) return callback(err); if (result) return callback(null, cache[cacheId] = result); callback(); }, callback)); }); };
這樣就很明顯了,resolve事件只是爲了獲取緩存,若是不存在緩存,就再次調用doResolve方法,這一次傳入的type爲new-resolve。
ParsePlugin
new-resolve事件流並不存在before-xxx或者after-xxx的狀況,因此直接看事件流自己。注入地點在UnsafeCachePlugin插件的後面。
從上面的if/else能夠看出,不管如何都會調用該插件,只是會根據unsafeCache的值來決定是否取緩存。
這個插件內容比較簡單暴力,簡答過一下:
// source => new-resolve // target => parsed-resolve function ParsePlugin(source, target) { this.source = source; this.target = target; } module.exports = ParsePlugin; ParsePlugin.prototype.apply = function(resolver) { var target = this.target; resolver.plugin(this.source, function(request, callback) { // 解析 var parsed = resolver.parse(request.request); // 合併對象 var obj = Object.assign({}, request, parsed); if (request.query && !parsed.query) { obj.query = request.query; } if (parsed && callback.log) { if (parsed.module) callback.log("Parsed request is a module"); if (parsed.directory) callback.log("Parsed request is a directory"); } // 觸發target的doResolve resolver.doResolve(target, obj, null, callback); }); };
基本上都是一個套路了,觸發事件流,作點什麼,而後最後調用doResolve觸發下一輪。
這裏的核心就是parse方法,估計跟vue源碼的parse差很少,比較麻煩,下一節再講。
Resolver.prototype.parse
這個parse方法超級簡單,以下:
Resolver.prototype.parse = function parse(identifier) { if (identifier === "") return null; var part = { request: "", query: "", module: false, directory: false, file: false }; // 根據問號切割參數 var idxQuery = identifier.indexOf("?"); if (idxQuery === 0) { part.query = identifier; } else if (idxQuery > 0) { part.request = identifier.slice(0, idxQuery); part.query = identifier.slice(idxQuery); } else { part.request = identifier; } if (part.request) { // 判斷是文件仍是文件夾 part.module = this.isModule(part.request); part.directory = this.isDirectory(part.request); // 去掉文件夾最後的斜槓 if (part.directory) { part.request = part.request.substr(0, part.request.length - 1); } } return part; }; /* 匹配如下內容開頭的字符串 1 => . 2 => ./ or .\ 3 => .. 4 => ../ or ..\ 5 => / 6 => A-Z:/ or A-Z:\ */ var notModuleRegExp = /^\.$|^\.[\\\/]|^\.\.$|^\.\.[\/\\]|^\/|^[A-Z]:[\\\/]/i; Resolver.prototype.isModule = function isModule(path) { return !notModuleRegExp.test(path); }; /* 匹配以\ or /結尾的字符串 */ var directoryRegExp = /[\/\\]$/i; Resolver.prototype.isDirectory = function isDirectory(path) { return directoryRegExp.test(path); };
內容很簡單,就作了2件事:
一、根據問號切割參數
2.、判斷是文件仍是文件夾
最後返回了信息組成的對象。