.30-浅析webpack源码之doResolve事件流(2)
这里所有的插件都对应着一个小功能,画个图整理下目前流程:
上节是从ParsePlugin中出来,对'./input.js'入口文件的路径做了处理,返回如下:
ParsePlugin.prototype.apply = function(resolver) { var target = this.target; resolver.plugin(this.source, function(request, callback) { // 分析request是否为模块或文件夹 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"); } // 拼接后的obj如下 /* { context: { issuer: '', compiler: undefined }, path: 'd:\\workspace\\doc', request: './input.js', query: '', module: false, directory: false, file: false } */ // target => parsed-resolve resolver.doResolve(target, obj, null, callback); }); };
该插件调用完后,进入下一个事件流,开始跑跑parsed-resolve相关的了。
回头看了一眼28节的大流程图,发现基本上这些事件流都是串联起来挨个注入的,还好不用自己去找在哪了。
createInnerCallback
这里先讲一下之前跳过的回调函数生成器,在Resolver中调用如下:
// before-callback createInnerCallback(beforeInnerCallback, { log: callback.log, missing: callback.missing, stack: newStack }, message && ("before " + message), true); // normal-callback createInnerCallback(innerCallback, { log: callback.log, missing: callback.missing, stack: newStack }, message); // after-callback createInnerCallback(afterInnerCallback, { log: callback.log, missing: callback.missing, stack: newStack }, message && ("after " + message), true);
方法的第一个参数都大同小异,取第一个为例:
function beforeInnerCallback(err, result) { // 根据调用callback时是否有参数决定调用回调函数还是进入下一阶段 if (arguments.length > 0) { if (err) return callback(err); if (result) return callback(null, result); return callback(); } runNormal(); }
剩下的两个也只是把runNormal变成了runAfter与callback而已。
有了参数,接下来看一下生成器的内部实现:
module.exports = function createInnerCallback(callback, options, message, messageOptional) { var log = options.log; // 无log时 if (!log) { // 基本上也是返回callback // 只是把options的两个方法挂载上去了 if (options.stack !== callback.stack) { var callbackWrapper = function callbackWrapper() { return callback.apply(this, arguments); }; callbackWrapper.stack = options.stack; callbackWrapper.missing = options.missing; return callbackWrapper; } return callback; } // 这个方法是批量取出本地log数组的内容然后调用options的log方法 function loggingCallbackWrapper() { var i; if (message) { if (!messageOptional || theLog.length > 0) { log(message); for (i = 0; i < theLog.length; i++) log(" " + theLog[i]); } } else { for (i = 0; i < theLog.length; i++) log(theLog[i]); } return callback.apply(this, arguments); } // 有log时 var theLog = []; loggingCallbackWrapper.log = function writeLog(msg) { theLog.push(msg); }; loggingCallbackWrapper.stack = options.stack; loggingCallbackWrapper.missing = options.missing; return loggingCallbackWrapper; };
这里的log大部分情况下都是undefined,所以暂时可以认为返回的基本上是第一个参数callback本身。
有log时也不复杂,等传入的options自带有效log时再看。
DescriptionFilePlugin
继续跑流程,这个插件就是对package.json配置文件进行解析,源码简化如下:
// request => 之前的obj // callback => createInnerCallback(...) (request, callback) => { const directory = request.path; /* resolver => 大对象 directory => 'd:\\workspace\\doc' filenames => ['package.json'] */ DescriptionFileUtils.loadDescriptionFile(resolver, directory, filenames, ((err, result) => { /**/ })); };
这里直接在内部调用了另外一个工具类的实例方法,源码如下:
var forEachBail = require("./forEachBail"); function loadDescriptionFile(resolver, directory, filenames, callback) { (function findDescriptionFile() { forEachBail(filenames, function(filename, callback) { /**/ }, function(err, result) { /**/ }); }()); }
forEachBail
内部引用了一个工具方法做迭代,继续看:
// 参数名字说明一切 module.exports = function forEachBail(array, iterator, callback) { if (array.length === 0) return callback(); var currentPos = array.length; var currentResult; var done = []; for (var i = 0; i < array.length; i++) { var itCb = createIteratorCallback(i); // 传入数组元素与生成的迭代器回调函数 iterator(array[i], itCb); if (currentPos === 0) break; } function createIteratorCallback(i) { return function() { if (i >= currentPos) return; // ignore var args = Array.prototype.slice.call(arguments); done.push(i); if (args.length > 0) { currentPos = i + 1; done = done.filter(function(item) { return item <= i; }); // 将该回调的参数赋值到外部变量 currentResult = args; } // 遍历完调用callback if (done.length === currentPos) { callback.apply(null, currentResult); currentPos = 0; } }; } };
由于本例中array只有一个数组元素,所以这个看似复杂的函数也比较简单了,需要关注的只有一行代码:
iterator(array[i], itCb);
第一个参数为package.json字符串,第二个为内部生成的一个回调,回到调用方法上,对应的iterator方法如下:
(filename, callback) => { // 路径拼接 var descriptionFilePath = resolver.join(directory, filename); // 这个readJson我是翻回去找了老久 // 来源于CachedInputFileSystem模块的191行 /* this._readJson = function(path, callback) { this.readFile(path, function(err, buffer) { if (err) return callback(err); try { var data = JSON.parse(buffer.toString("utf-8")); } catch (e) { return callback(e); } callback(null, data); }); }.bind(this); */ // 这两个方法根本没有什么卵区别 if (resolver.fileSystem.readJson) { resolver.fileSystem.readJson(descriptionFilePath, function(err, content) { if (err) { if (typeof err.code !== "undefined") return callback(); return onJson(err); } onJson(null, content); }); } else { resolver.fileSystem.readFile(descriptionFilePath, function(err, content) { if (err) return callback(); try { var json = JSON.parse(content); } catch (e) { onJson(e); } onJson(null, json); }); } // 在不出错的情况下传入null与读取到的json字符串 function onJson(err, content) { if (err) { if (callback.log) callback.log(descriptionFilePath + " (directory description file): " + err); else err.message = descriptionFilePath + " (directory description file): " + err; return callback(err); } callback(null, { content: content, directory: directory, path: descriptionFilePath }); } }
这里首先进行路径拼接,然后调用readFile方法读取对应路径的package.json文件,如果没有出错,将读取到的字符串与路径包装成对象传入callback。
Resolver.prototype.join
简单看一下路径的拼接函数。
var memoryFsJoin = require("memory-fs/lib/join"); var memoizedJoin = new Map(); // path => 目录 // request => 文件名 Resolver.prototype.join = function(path, request) { var cacheEntry; // 获取缓存目录 var pathCache = memoizedJoin.get(path); if (typeof pathCache === "undefined") { memoizedJoin.set(path, pathCache = new Map()); } else { // 获取目录缓存中对应的文件缓存 cacheEntry = pathCache.get(request); if (typeof cacheEntry !== "undefined") return cacheEntry; } // 初次获取文件 cacheEntry = memoryFsJoin(path, request); // 设置缓存 pathCache.set(request, cacheEntry); return cacheEntry; };
非常的简单明了,用了一个map缓存一个目录,目录的值也是一个map,缓存该目录下的文件。
这里看一下是第一次时,memoryFsJoin是如何处理路径的:
var normalize = require("./normalize"); // windows与linux系统绝对路径正则 var absoluteWinRegExp = /^[A-Z]:([\\\/]|$)/i; var absoluteNixRegExp = /^\//i; // path => 'd:\\workspace\\doc' // request => 'package.json' module.exports = function join(path, request) { if (!request) return normalize(path); // 检测是否绝对路径 if (absoluteWinRegExp.test(request)) return normalize(request.replace(/\//g, "\\")); if (absoluteNixRegExp.test(request)) return normalize(request); // 目录为/时 if (path == "/") return normalize(path + request); // 命中这里 注意正则后面的i // 替换拼接后 => d:\\workspace\\doc\\package.json if (absoluteWinRegExp.test(path)) return normalize(path.replace(/\//g, "\\") + "\\" + request.replace(/\//g, "\\")); if (absoluteNixRegExp.test(path)) return normalize(path + "/" + request); return normalize(path + "/" + request); };
果然还没完,在进行两个平台路径间的判断后,将两个参数拼接后传入normalize方法,参数已经在注释给出。
以该字符串为例,看一下normalize方法:
// path => d:\\workspace\\doc\\package.json module.exports = function normalize(path) { // parts => [ 'd:', '\\', 'workspace', '\\', 'doc', '\\', 'package.json' ] var parts = path.split(/(\\+|\/+)/); if (parts.length === 1) return path; var result = []; var absolutePathStart = 0; // sep主要用来标记切割数组中\\这种路径符号 for (var i = 0, sep = false; i < parts.length; i++, sep = !sep) { var part = parts[i]; //第一次弹入磁盘名 => result = ['d:'] if (i === 0 && /^([A-Z]:)?$/i.test(part)) { result.push(part); absolutePathStart = 2; } else if (sep) { // 如果是路径符号 直接弹入 // result = ['d:','\\'] result.push(part[0]); } // 接下来是对'..'与'.'符号进行处理 // 看一下注释就懂了 列举了各种情况 else if (part === "..") { switch (result.length) { case 0: // i. e. ".." => ".." // i. e. "../a/b/c" => "../a/b/c" result.push(part); break; case 2: // i. e. "a/.." => "" // i. e. "/.." => "/" // i. e. "C:\.." => "C:\" // i. e. "a/../b/c" => "b/c" // i. e. "/../b/c" => "/b/c" // i. e. "C:\..\a\b\c" => "C:\a\b\c" i++; sep = !sep; result.length = absolutePathStart; break; case 4: // i. e. "a/b/.." => "a" // i. e. "/a/.." => "/" // i. e. "C:\a\.." => "C:\" // i. e. "/a/../b/c" => "/b/c" if (absolutePathStart === 0) { result.length -= 3; } else { i++; sep = !sep; result.length = 2; } break; default: // i. e. "/a/b/.." => "/a" // i. e. "/a/b/../c" => "/a/c" result.length -= 3; break; } } else if (part === ".") { switch (result.length) { case 0: // i. e. "." => "." // i. e. "./a/b/c" => "./a/b/c" result.push(part); break; case 2: // i. e. "a/." => "a" // i. e. "/." => "/" // i. e. "C:\." => "C:\" // i. e. "C:\.\a\b\c" => "C:\a\b\c" if (absolutePathStart === 0) { result.length--; } else { i++; sep = !sep; } break; default: // i. e. "a/b/." => "a/b" // i. e. "/a/." => "/" // i. e. "C:\a\." => "C:\" // i. e. "a/./b/c" => "a/b/c" // i. e. "/a/./b/c" => "/a/b/c" result.length--; break; } } // 无意外直接弹入 else if (part) { result.push(part); } } // 给磁盘名后面拼接上路径符号 if (result.length === 1 && /^[A-Za-z]:$/.test(result)) return result[0] + "\\"; // 这是正常返回 return result.join(""); };
讲道理,只有不是乱写路径,这里都会普通的返回传进去的路径(后面会出现特殊情况)。
返回的路径,会被readFile作为参数调用,最终返回读取到的json字符串作为对应的content传入回调函数中。
/* callback(null, { content: content, directory: directory, path: descriptionFilePath }) */ function loadDescriptionFile(resolver, directory, filenames, callback) { (function findDescriptionFile() { forEachBail(filenames, function(filename, callback) { /**/ }, // 这里的callback为最外部的callback // 被这里的回调绕死了 // result为之前传进来的对象 注释有写 function(err, result) { if (err) return callback(err); if (result) { return callback(null, result); } else { directory = cdUp(directory); if (!directory) { return callback(); } else { return findDescriptionFile(); } } }); }()); }
这个callback一层一层的往外执行,最后回到了DescriptionFilePlugin中:
DescriptionFileUtils.loadDescriptionFile(resolver, directory, filenames, ((err, result) => { if (err) return callback(err); // 找不到package.json文件时 if (!result) { // 第一次也没有这两个属性 if (callback.missing) { filenames.forEach((filename) => { callback.missing.push(resolver.join(directory, filename)); }); } if (callback.log) callback.log("No description file found"); // 直接调用callback return callback(); } // 如果读取到了就会将描述文件的路径、目录、内容拼接到request对象上 // 路径转换为相对路径 const relativePath = "." + request.path.substr(result.directory.length).replace(/\\/g, "/"); const obj = Object.assign({}, request, { descriptionFilePath: result.path, descriptionFileData: result.content, descriptionFileRoot: result.directory, relativePath: relativePath }); // 触发下一个事件流 // 带有message resolver.doResolve(target, obj, "using description file: " + result.path + " (relative path: " + relativePath + ")", createInnerCallback((err, result) => { if (err) return callback(err); if (result) return callback(null, result); // Don't allow other description files or none at all callback(null, null); }, callback)); }));
如果没有package.json文件,就会直接调用callback并且不传任何参数,知道这个callback是哪个callback吗????
是这个:
function innerCallback(err, result) { if (arguments.length > 0) { if (err) return callback(err); if (result) return callback(null, result); return callback(); } runAfter(); }
什么是回调地狱?无限ajax内嵌?nonono,太单纯,来看webpack源码吧,一个callback可以传入地心,搞得我现在看到callback就头大。
很明显,这里没有传任何参数,直接进入runAfter,下节讲吧,还好已经跑出来了,不然隔两天回来看根本不知道飞哪去了。