.30-浅析webpack源码之doResolve事件流(1)

时间:2021-08-04 15:48:50

  这里所有的插件都对应着一个小功能,画个图整理下目前流程:

.30-浅析webpack源码之doResolve事件流(1)

  上节是从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,下节讲吧,还好已经跑出来了,不然隔两天回来看根本不知道飞哪去了。