.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,下节讲吧,还好已经跑出来了,不然隔两天回来看根本不知道飞哪去了。
.30-浅析webpack源码之doResolve事件流(2)的更多相关文章
- .30-浅析webpack源码之doResolve事件流(1)
这里所有的插件都对应着一个小功能,画个图整理下目前流程: 上节是从ParsePlugin中出来,对'./input.js'入口文件的路径做了处理,返回如下: ParsePlugin.prototype ...
- .29-浅析webpack源码之doResolve事件流(1)
在上一节中,最后返回了一个resolver,本质上就是一个Resolver对象: resolver = new Resolver(fileSystem); 这个对象的构造函数非常简单,只是简单的继承了 ...
- .31-浅析webpack源码之doResolve事件流(2)
放个流程图: 这里也放一下request对象内容,这节完事后如下(把vue-cli的package.json也复制过来了): /* { context: { issuer: '', compiler: ...
- .32-浅析webpack源码之doResolve事件流(4)
流程图如下: 重回DescriptionFilePlugin 上一节最后进入relative事件流,注入地点如下: // relative plugins.push(new DescriptionFi ...
- .33-浅析webpack源码之doResolve事件流(5)
file => FileExistsPlugin 这个事件流快接近尾声了,接下来是FileExistsPlugin,很奇怪的是在最后才来检验路径文件是否存在. 源码如下: FileExistsP ...
- .34-浅析webpack源码之事件流make(3)
新年好呀~过个年光打游戏,function都写不顺溜了. 上一节的代码到这里了: // NormalModuleFactory的resolver事件流 this.plugin("resolv ...
- 浅析libuv源码-node事件轮询解析(3)
好像博客有观众,那每一篇都画个图吧! 本节简图如下. 上一篇其实啥也没讲,不过node本身就是这么复杂,走流程就要走全套.就像曾经看webpack源码,读了300行代码最后就为了取package.js ...
- .27-浅析webpack源码之事件流make(2)
上一节跑到了NormalModuleFactory模块,调用了原型方法create后,依次触发了before-rsolve.factory.resolver事件流,这节从resolver事件流开始讲. ...
- .3-浅析webpack源码之预编译总览
写在前面: 本来一开始想沿用之前vue源码的标题:webpack源码之***,但是这个工具比较巨大,所以为防止有人觉得我装逼跑来喷我(或者随时鸽),加上浅析二字,以示怂. 既然是浅析,那么案例就不必太 ...
随机推荐
- 常见CEPH操作命令
1. rbd ls 查看ceph默认资源池rbd里面的镜像2. rbd info xxx.img 查看xxx.img的具体的具体信息3. rbd rm xxx.img 删除xxx.img4. rbd ...
- spring项目读取配置的demo
背景 读取配置是基础能力,研发这个模式不错,可以从不同配置中读取数据,如下图: 可以根据不同分类的文件来管理配置,然后统一在conf中配置哪些文件 package com.jwen.platform. ...
- 工作随笔——pinpoint分布式性能监控工具(docker安装)
在做性能压测的时候,你是不是有只能看到测试报告? 在做性能压测的时候,你是不是想知道每一个方法执行了多长时间? Pinpoint几乎可以帮助你查看你想看到的每一个细节. Pinpoint是什么? Pi ...
- 两台linux之间建立信任关系,实现免密码ssh远程登录或scp数据上传
两台linux之间建立信任关系,实现免密码远程登录或数据上传 1.执行ssh-keygen命令,生成建立安全信任关系的证书: linux1上:执行命令 ssh-keygen -t rsa 在程序提 ...
- ViewPager无限滑动
2016-6-19 前言 View轮播效果在app中很常见,一想到左右滑动的效果就很容易想到使用ViewPager来实现.对于像我们常说的banner这样的效果,具备无限滑动的功能是可以用ViewPa ...
- 用代码来细说Csrf漏洞危害以及防御
开头: 废话不多说,直接进主题. 0x01 CSRF介绍:CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session ...
- 设计模式《JAVA与模式》之解释器模式
在阎宏博士的<JAVA与模式>一书中开头是这样描述解释器(Interpreter)模式的: 解释器模式是类的行为模式.给定一个语言之后,解释器模式可以定义出其文法的一种表示,并同时提供一个 ...
- java filter过滤器及责任链设计模式
什么是Filter? Filter属于sevlet规范,翻译为过滤器. Filter在web开发中有什么作用? 案例一:一个web站点只有用户登录才能继续访问该站点的资源,那么需要用户每次访问都判断是 ...
- 【xsy3355】图 思维
题目大意:给你一个n个点m条无向边的图,问这个图是否能够: 1,被四染色(用四种颜色给图染色,且相邻点颜色不同). 2,找出一个奇环,满足在原图中去掉这个奇环后每个点依然相邻. 请输出1或者2中的任意 ...
- android屏幕密度规律及dp px转换
px和dp(sp) 之间转化公式: 1 乘以(dp转px)或者除以(px转dp) scal缩放因子,在上浮0.5f /** * 密度转换像素 * */ public static int dip2p ...