.29-浅析webpack源码之doResolve事件流(1)
在上一节中,最后返回了一个resolver,本质上就是一个Resolver对象:
resolver = new Resolver(fileSystem);
这个对象的构造函数非常简单,只是简单的继承了Tapable,并接收了fileSystem参数:
function Resolver(fileSystem) { Tapable.call(this); this.fileSystem = fileSystem; } module.exports = Resolver;
resolve
而在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编译期间被调用多次,这里的参数仅仅是第一次被调用时的。
doResolve
简单的说,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判断。
而事件流resolve-step、before-resolve也不存在,所以会直接走最后的else,进入runNormal方法。
这里全面描述一下doResolve,方法内部有5个函数,分别名为beforeInnerCallback、runNormal、innerCallback、runAfter、afterInnerCallback,所有的callback函数都负责包装对应事件流的回调函数。
源码如下:
// 先判断是否存在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件事:
1、根据问号切割参数
2.、判断是文件还是文件夹
最后返回了信息组成的对象。