.31-浅析webpack源码之doResolve事件流(3)
放个流程图:
这里也放一下request对象内容,这节完事后如下(把vue-cli的package.json也复制过来了):
/* { context: { issuer: '', compiler: undefined }, path: 'd:\\workspace\\doc', request: './input.js', query: '', module: false, directory: false, file: false, descriptionFilePath: 'd:\\workspace\\doc\\package.json', descriptionFileData: *package.json内容*, descriptionFileRoot: 'd:\\workspace\\doc', relativePath: '.', __innerRequest_request: './input.js', __innerRequest_relativePath: '.', __innerRequest: './input.js' } */
上一节看到这:
// 调用的是callback() function innerCallback(err, result) { if (arguments.length > 0) { if (err) return callback(err); if (result) return callback(null, result); return callback(); } runAfter(); }
这里接下来会调用runAfter方法,之前有讲解过这个,简单讲就是触发after-type的事件流,这里的type为parsed-resolve,即触发after-parsed-resolve事件流。
来源如下:
plugins.push(new NextPlugin("after-parsed-resolve", "described-resolve"));
这个插件就非常简单了:
NextPlugin.prototype.apply = function(resolver) { var target = this.target; resolver.plugin(this.source, function(request, callback) { resolver.doResolve(target, request, null, callback); }); };
直接调用doResolve方法触发下一个target的事件流,比起有描述文件的情况,这里的区别就是request少了几个参数,触发下一个事件流时没有message。
刚发现事件流的名字代表着某阶段,此时代表描述文件解析完毕。
接下来的事件流来源于以下几个插件:
// described-resolve alias.forEach(function(item) { plugins.push(new AliasPlugin("described-resolve", item, "resolve")); }); plugins.push(new ConcordModulesPlugin("described-resolve", {}, "resolve")); aliasFields.forEach(function(item) { plugins.push(new AliasFieldPlugin("described-resolve", item, "resolve")); }); plugins.push(new ModuleKindPlugin("after-described-resolve", "raw-module")); plugins.push(new JoinRequestPlugin("after-described-resolve", "relative"));
AliasPlugin
先从第一个开始看,alias参数引用vue-cli的代码,这里的alias在上面的第二部分进行了转换(具体可参考28节)。
数组的元素作为参数传入了AliasPlugin插件中,源码如下:
/* 源配置 alias: { 'vue$': 'vue/dist/vue.esm.js', '@': '../src' } 转换后为 alias:[ { name: 'vue', onlyModule: true, alias: 'vue/dist/vue.esm.js' }, { name: '@', onlyModule: false, alias: '../src' } ] */ AliasPlugin.prototype.apply = function(resolver) { var target = this.target; var name = this.name; var alias = this.alias; var onlyModule = this.onlyModule; resolver.plugin(this.source, function(request, callback) { var innerRequest = request.request; if (!innerRequest) return callback(); // 两个元素传进来并不满足if条件 跳过 // startsWith可参考ES6的新方法http://es6.ruanyifeng.com/#docs/string#includes-startsWith-endsWith if (innerRequest === name || (!onlyModule && startsWith(innerRequest, name + "/"))) { if (innerRequest !== alias && !startsWith(innerRequest, alias + "/")) { var newRequestStr = alias + innerRequest.substr(name.length); var obj = Object.assign({}, request, { request: newRequestStr }); return resolver.doResolve(target, obj, "aliased with mapping '" + name + "': '" + alias + "' to '" + newRequestStr + "'", createInnerCallback(function(err, result) { if (arguments.length > 0) return callback(err, result); // don't allow other aliasing or raw request callback(null, null); }, callback)); } } return callback(); }); };
不太懂这里的处理是干什么,反正两个元素传进来都没有满足if条件,跳过。
ConcordModulesPlugin
described-resolve事件流还没有完,所以callback执行后只是记数,下一个插件源码如下:
ConcordModulesPlugin.prototype.apply = function(resolver) { var target = this.target; resolver.plugin(this.source, function(request, callback) { // 获取的还是'./input.js' var innerRequest = getInnerRequest(resolver, request); if (!innerRequest) return callback(); // request.descriptionFileData就是配置文件package.json中的内容 var concordField = DescriptionFileUtils.getField(request.descriptionFileData, "concord"); // 找不到该属性直接返回 if (!concordField) return callback(); // 下面的都不用跑了 var data = concord.matchModule(request.context, concordField, innerRequest); if (data === innerRequest) return callback(); if (data === undefined) return callback(); if (data === false) { var ignoreObj = Object.assign({}, request, { path: false }); return callback(null, ignoreObj); } var obj = Object.assign({}, request, { path: request.descriptionFileRoot, request: data }); resolver.doResolve(target, obj, "aliased from description file " + request.descriptionFilePath + " with mapping '" + innerRequest + "' to '" + data + "'", createInnerCallback(function(err, result) { if (arguments.length > 0) return callback(err, result); // Don't allow other aliasing or raw request callback(null, null); }, callback)); }); };
这里有两个工具方法:getInnerRequest、getFiled,第一个获取request的inner属性,代码如下:
module.exports = function getInnerRequest(resolver, request) { // 第一次进来是没有这些属性的 if (typeof request.__innerRequest === "string" && request.__innerRequest_request === request.request && request.__innerRequest_relativePath === request.relativePath) return request.__innerRequest; var innerRequest; // './input.js' if (request.request) { innerRequest = request.request; // 尝试获取relativePath属性进行拼接 if (/^\.\.?\//.test(innerRequest) && request.relativePath) { innerRequest = resolver.join(request.relativePath, innerRequest); } } else { innerRequest = request.relativePath; } // 属性添加 request.__innerRequest_request = request.request; request.__innerRequest_relativePath = request.relativePath; return request.__innerRequest = innerRequest; };
总的来说就是尝试获取__innerRequest属性,但是初次进来是没有的,所以会在后面进行添加,最后返回的仍然是'./input.js'。
第二个方法就比较简单了,只是从之前读取的package.json对象查询对应的字段,代码如下:
// content为package.json配置对象 function getField(content, field) { if (!content) return undefined; // 数组及单key模式 if (Array.isArray(field)) { var current = content; for (var j = 0; j < field.length; j++) { if (current === null || typeof current !== "object") { current = null; break; } current = current[field[j]]; } if (typeof current === "object") { return current; } } else { if (typeof content[field] === "object") { return content[field]; } } }
代码非常简单,这里就不讲了。
常规情况下,没人会去设置concord属性吧,在vue-cli我也没看到,这里先跳过。
AliasFieldPlugin
接下来是这个不知道干啥的插件,处理的是resolve.aliasFields参数,默认参数及插件源码如下:
// "aliasFields": ["browser"], AliasFieldPlugin.prototype.apply = function(resolver) { var target = this.target; var field = this.field; resolver.plugin(this.source, function(request, callback) { if (!request.descriptionFileData) return callback(); // 一样的 var innerRequest = getInnerRequest(resolver, request); if (!innerRequest) return callback(); // filed => browser var fieldData = DescriptionFileUtils.getField(request.descriptionFileData, field); if (typeof fieldData !== "object") { if (callback.log) callback.log("Field '" + field + "' doesn't contain a valid alias configuration"); return callback(); } var data1 = fieldData[innerRequest]; var data2 = fieldData[innerRequest.replace(/^\.\//, "")]; var data = typeof data1 !== "undefined" ? data1 : data2; if (data === innerRequest) return callback(); if (data === undefined) return callback(); if (data === false) { var ignoreObj = Object.assign({}, request, { path: false }); return callback(null, ignoreObj); } var obj = Object.assign({}, request, { path: request.descriptionFileRoot, request: data }); resolver.doResolve(target, obj, "aliased from description file " + request.descriptionFilePath + " with mapping '" + innerRequest + "' to '" + data + "'", createInnerCallback(function(err, result) { if (arguments.length > 0) return callback(err, result); // Don't allow other aliasing or raw request callback(null, null); }, callback)); }); };
开头跟之前那个是一样的,也是调用getField从package.json中获取对应的配置,但是这个默认的browser我在vue-cli也找不到,暂时跳过。
正常处理完described-resolve事件流,继续执行runafter触发after-described-resolve事件流,来源如下:
plugins.push(new ModuleKindPlugin("after-described-resolve", "raw-module")); plugins.push(new JoinRequestPlugin("after-described-resolve", "relative"));
ModuleKindPlugin
ModuleKindPlugin.prototype.apply = function(resolver) { var target = this.target; resolver.plugin(this.source, function(request, callback) { // 判断module属性 if (!request.module) return callback(); var obj = Object.assign({}, request); // 删除module属性 delete obj.module; // 直接触发下一个事件流 resolver.doResolve(target, obj, "resolve as module", createInnerCallback(function(err, result) { if (arguments.length > 0) return callback(err, result); // Don't allow other alternatives callback(null, null); }, callback)); }); };
这里的处理十分简单,判断request对象是否是module,是则直接触发下一个事件流。
而在第一次时进来的是入口文件,module属性为false,所以这里会跳过,后面处理module再回来讲。
JoinRequestPlugin
JoinRequestPlugin.prototype.apply = function(resolver) { var target = this.target; resolver.plugin(this.source, function(request, callback) { var obj = Object.assign({}, request, { // request.path => d:\\workspace\\doc // request.request => ./input.js // 在join方法中会被拼接成d:\workspace\doc\.\input.js // 最后格式化返回d:\workspace\doc\input.js path: resolver.join(request.path, request.request), // undefined relativePath: request.relativePath && resolver.join(request.relativePath, request.request), request: undefined }); resolver.doResolve(target, obj, null, callback); }); };
这个地方终于把入口文件的路径拼起来了,接下来调用下一个事件流,这节先到这里。
写完这节,总算对Resolver对象有所了解,总结如下:
1、该对象可以处理resolve参数、loader、module等等
2、插件的链式调用类似于if/else,比如说如果传进来的是一个module,插件会流向module事件流;如果是普通的文件,会流向本节所讲的方,每一种情况都有自己的结局。
3、一部分参数处理依赖于package.json的配置对象内容