共计 13953 个字符,预计需要花费 35 分钟才能阅读完成。
0. 食用本文的文档阐明:
本篇文章 耗时 7 个小时
左右才竣工,篇幅波及到 大量的源码及其剖析的过程图解和数据
,浏览前,请保障本人有充沛的工夫,纵情的去享受 排汇常识进入脑子的过程
。
因为篇幅无限,心愿你把握以下前置常识:
- 曾经学习过
enhanced-resolve 工作流程和插拔式插件机制
,点这里温习:webpack 外围库 enhanced-resolve 工作流程和插拔式插件机制 - 理解
tabaple
是一个订阅公布
的设计模式(晓得啥是订阅公布即可) - 大抵理解 node 中的模块查找机制,如:
require(‘./xxx.js’);
require('./xxx');
require('xxx');
通过本文你将学到如下内容(或者带着如下疑难去学习):
enhance-resolve
是如何在简单的插件调用之间传递数据的?Resolver 和 ResolverFactory
的关系是什么?Resolver
是如何设计实现的?软链接和硬链接
是什么?区别在哪里?- 如何开发一个
enhance-resolve
的插件利用到 webpack 中? - 如何去一步步的
debug
一个开源库?
1 webpack 和 enhance-resolve 的关系是什么?
webpack 作为一个弱小的打包工具,其弱小的不仅仅是插件机制,还有其外围包 enhance-resolve
来实现模块的门路查找。性能上来说它能够 加强 Webpack 的模块解析能力
,使其更容易找到所需的模块,从而进步 Webpack 的 性能和可维护性
。从配置上来说它能够为 Webpack 解析器增加额定的搜寻门路以及解析规定,让 Webpack 更好地解释门路和文件
,进而让 webpack 更加分心的做模块打包相干的事件。
理解完背景和需要当前,如果让咱们去实现一个 enhance-resolve 呢?
性能点:
- 首先解析器满足模块查找中的所有的规定 模块:通用 JS 模块 | 节点.js v14.21.3 文档 (nodejs.org)
- 要和 webpack 一样,有弱小的
插件加载机制和良好的配置性能
。
本人能够心中默默的想一下如何实现上述性能点呢?
2. 接下来就根据上述性能点通过代码去理解一下 enhance-resolve
咱们上回太强了,3000 字图文并茂的解析 webpack 外围库 enhanced-resolve 工作流程和插拔式插件机制,真香 – 掘金 (juejin.cn)说到:
- ResolverFactory.createResolver 依据
Resolver
类创立实例:myResolve
(吃了配置,吐出对象myResolve
) myResolve 上 注册并订阅
大量的 hook(枪支弹药贮备好,一刻激发)- 调用
myResolver.resolve
办法开始进行 文件解析 的主流程 - 外部通过
resolve.doResolve
办法,开始调用第一个 hook:this.hooks.resolve
- 找到之前 订阅 hook 的 plugin:
ParsePlugin
ParsePlugin
进行初步解析,而后 通过doResolve
执行下一个 hookparsed-resolve
,后期筹备工作完结,链式调用开始,真正的解析文件的流程
也开始。
从下面的第 2 步开始整起,第 2 步注册了哪些 hook 呢?接下来开始瞅瞅
2.1 细细回顾 myResolve
上注册的 hooks
代码跳转到 lib/ResolverFactory.js
的 295
行左右,代码如下:
//// pipeline ////
resolver.ensureHook("resolve");
resolver.ensureHook("internalResolve");
resolver.ensureHook("newInternalResolve");
resolver.ensureHook("parsedResolve");
resolver.ensureHook("describedResolve");
resolver.ensureHook("rawResolve");
resolver.ensureHook("normalResolve");
resolver.ensureHook("internal");
resolver.ensureHook("rawModule");
resolver.ensureHook("module");
resolver.ensureHook("resolveAsModule");
resolver.ensureHook("undescribedResolveInPackage");
resolver.ensureHook("resolveInPackage");
resolver.ensureHook("resolveInExistingDirectory");
resolver.ensureHook("relative");
resolver.ensureHook("describedRelative");
resolver.ensureHook("directory");
resolver.ensureHook("undescribedExistingDirectory");
resolver.ensureHook("existingDirectory");
resolver.ensureHook("undescribedRawFile");
resolver.ensureHook("rawFile");
resolver.ensureHook("file");
resolver.ensureHook("finalFile");
resolver.ensureHook("existingFile");
resolver.ensureHook("resolved");
为了便于了解,放出 ensureHook
的局部外围代码,其次要作用就是创立一个 AsyncSeriesBailHook
异步串行保险型的 hook,(所谓的 保险
你能够设想成漂泊星球 2 中的 饱和式救济
,1 个工作派出多个救援队【订阅多个 hook】,只有一个救援队胜利了【一个 hook 存在返回值】这次救济就算胜利了【这个订阅事件就算完结了】)
ensureHook(name) {if (typeof name !== "string") {return name;}
name = toCamelCase(name);
const hook = this.hooks[name];
if (!hook) {return (this.hooks[name] = new AsyncSeriesBailHook(["request", "resolveContext"],
name
));
}
return hook;
}
PS: ensureHook
的作用是
能够看到作者在头部特意写了一个简短的正文 //// pipeline ////
,翻译过去也就是流水线。
流水线是一种工业生产方式,它将 一个大型工程分解成若干个小步骤
,每个步骤都有 专门的工人或机器
来实现,从而进步生产效率。流水线的劣势在于能够 进步生产效率,缩小生产成本,进步产品质量,并且能够更快地实现大型工程
。在 IT 界就能够认为是 模块间解耦,进步代码可读性和可维护性
。
到这里流水线流程组装结束【可了解成为每个工种调配了相干的工作】,那下一步就是要开始组装每局部流程用到的 工具集(plugins)
,【而后再为每个工种调配不同的工具】。局部外围代码如下:
// resolve
for (const { source, resolveOptions} of [{ source: "resolve", resolveOptions: { fullySpecified} },
{source: "internal-resolve", resolveOptions: { fullySpecified: false} }
]) {if (unsafeCache) {
plugins.push(
new UnsafeCachePlugin(
source,
cachePredicate,
unsafeCache,
cacheWithContext,
`new-${source}`
)
);
plugins.push(new ParsePlugin(`new-${source}`, resolveOptions, "parsed-resolve")
);
} else {plugins.push(new ParsePlugin(source, resolveOptions, "parsed-resolve"));
}
}
// parsed-resolve
plugins.push(
new DescriptionFilePlugin(
"parsed-resolve",
descriptionFiles,
false,
"described-resolve"
)
);
plugins.push(new NextPlugin("after-parsed-resolve", "described-resolve"));
...... 此处省略局部注册插件逻辑
//// RESOLVER ////
for (const plugin of plugins) {if (typeof plugin === "function") {plugin.call(resolver, resolver);
} else {plugin.apply(resolver);
}
}
始终到最初把依据用户配置生成的相干的插件列表 plugins
给注册到 resolver
上,整个的resolver
的 hook 和 plugin 的绑定才胜利完结。
本次调试代码绑定的 总的插件的数量为 41 个
:
其中因为 NextPlugin
是流程推动性插件和业务逻辑无关,就过滤掉,还剩下 32 个
:
2.2 开始调试正式流程吧(流水线关上电源,跑起来了)
在 lib/Resolver.js
的 resolve
办法中是查找门路开始的终点,首先就是把 用户传入的 门路 path
和 要查找文件的门路 request
赋值给 obj 对象【此 obj 是外围对象,将在各个插件中流转批改】。
而后就开始调用本身的 doResolve
办法,正式开始流程了。
3. 从 resolve
hook 开始的流程,到完结
断点到 doResolve
办法的 hook.callAsync
局部,看下相干的参数。
从图中能够看出,此 hook 名为 resolve
,入参有两个:Array(2)[request,resolveContext]
,绑定此 hook 的插件只有一个 ParsePlugin
的插件,传递上来的参数是 request
对象:path
和 request
是重要的数据。
下一步就开始进入 ParsePlugin
插件看看它到底做了什么。
3.1 视察 ParsePlugin
工种的工作
ParsePlugin
其外围 apply
代码如下:
apply(resolver) {const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("ParsePlugin", (request, resolveContext, callback) => {
// 调用 resolver 中的 parse 办法初步解析
const parsed = resolver.parse(/** @type {string} */ (request.request));
// 合并成新的 obj 对象
const obj = {...request, ...parsed, ...this.requestOptions};
if (request.query && !parsed.query) {obj.query = request.query;}
if (request.fragment && !parsed.fragment) {obj.fragment = request.fragment;}
if (parsed && resolveContext.log) {if (parsed.module) resolveContext.log("Parsed request is a module");
if (parsed.directory)
resolveContext.log("Parsed request is a directory");
}
// There is an edge-case where a request with # can be a path or a fragment -> try both
if (obj.request && !obj.query && obj.fragment) {const directory = obj.fragment.endsWith("/");
const alternative = {
...obj,
directory,
request:
obj.request +
(obj.directory ? "/" : "") +
(directory ? obj.fragment.slice(0, -1) : obj.fragment),
fragment: ""
};
resolver.doResolve(
target,
alternative,
null,
resolveContext,
(err, result) => {if (err) return callback(err);
if (result) return callback(null, result);
resolver.doResolve(target, obj, null, resolveContext, callback);
}
);
return;
}
resolver.doResolve(target, obj, null, resolveContext, callback);
});
}
通过断点发现,obj
对象第一次进入这个 plugin
逛了一圈,而后最终走到了 resolver.doResolve(target, obj, null, resolveContext, callback);
这里,解决完的数据如下:【思考一下吃了啥数据,吐出了啥数据?】
ParsePlugin
吃了 obj,当前对其进行初步解析,减少了如下属性【红色是吃进去的,绿色是吐出来的】
而后下一个要执行 hook 是 parsedResolve
,其绑定的业务插件是 DescriptionFilePlugin
,NextPlugin
插件属于流程插件,能够疏忽。
3.2 视察 DescriptionFilePlugin
工种的工作
以后流程的 DescriptionFilePlugin
插件的外围是在 DescriptionFileUtils.loadDescriptionFile
办法里,
当看到 ['package.json']
的那一刻是不是能够联想并猜测到:此插件的作用就是在实现 查找以后的门路
是否是一个 具备package.json
文件的模块?持续 debug loadDescriptionFile
办法,
看到这个门路拼接,验证了猜测是正确的,持续 debug 发现,走到了此办法的 callback 函数里,执行了一个 cdUp
的办法。
咱们不去看办法实现,仅仅看变更,变量从 directory
变成了 dir
,数据从 /Users/fujunkui/Desktop/github-project/enhanced-resolve/demo/test-find-file
变成了/Users/fujunkui/Desktop/github-project/enhanced-resolve/demo
,卧槽,还真是进入了下级目录,cdUp
66666。
不出所料的话,他会始终 cdUp
晓得进入到根目录的, 查找 /package.json
为止【图中,我把 enhance-resolve 我的项目的 package.json 文件给删除了,不删除的话找到这一级就进行了】
局部截图
最初找呀找呀,就是找不到一个目录具备package.json
文件,没方法只能走 callback
了。
后果就是这个插件一顿 cdUp 操作,啥都没变,留神此处的 callback()
返回值为空,他就要进入此 hook 的下一个插件了,NextPlugin
正式退场。
3.3 外卖小哥 NextPlugin
正式退场
NextPlugin
外围代码如下:
apply(resolver) {const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("NextPlugin", (request, resolveContext, callback) => {resolver.doResolve(target, request, null, resolveContext, callback);
});
}
间接调用 resolver.doResolve
把上一个 hook 的丢出的数据,给下一个 hook 应用,不做任何扭转(像极了 辛苦帮商家送餐的外卖小哥,点赞)。
那就有请下一位 hook 闪亮退场:
好家伙,下一个 hook 是 rawResolve
,让咱们来看看他的监听者 都有谁,拉倒吧,还是 NextPlugin
外卖小哥,这就是外卖小哥点饭(外卖小哥送给外卖小哥)???
[](https://img.soogif.com/SkisPO…)
那就持续吧,看看这个 rawResolve
的下一个 hook 是谁,监听的插件都有谁?
下一个 hook 名叫 normalResolve
,居然有 3 个插件监听了此 hook,那么开始表演吧。
3.4 视察 hook 名为normalResolve
上面的三个工种(插件)的工作
3.4.1 第一位和第二位 靓仔都是 ConditionalPlugin
(翻译为中文就是:条件插件)
大抵猜想一下条件插件:就是满足了哪些条件才会继续执行上来。
两者的区别在初始化的传参里:
plugins.push(
new ConditionalPlugin(
"after-normal-resolve",
{module: true},
"resolve as module",
false,
"raw-module"
)
);
plugins.push(
new ConditionalPlugin(
"after-normal-resolve",
{internal: true},
"resolve as internal import",
false,
"internal"
)
);
总体代码是:
class ConditionalPlugin {constructor(source, test, message, allowAlternatives, target) {
this.source = source;
this.test = test;
this.message = message;
this.allowAlternatives = allowAlternatives;
this.target = target;
}
apply(resolver) {const target = resolver.ensureHook(this.target);
const {test, message, allowAlternatives} = this;
const keys = Object.keys(test);
resolver
.getHook(this.source)
.tapAsync("ConditionalPlugin", (request, resolveContext, callback) => {for (const prop of keys) {if (request[prop] !== test[prop]) return callback();}
resolver.doResolve(
target,
request,
message,
resolveContext,
allowAlternatives
? callback
: (err, result) => {if (err) return callback(err);
// Don't allow other alternatives
if (result === undefined) return callback(null, null);
callback(null, result);
}
);
});
}
};
执行后果如下:
第一次 插件的 callback 后果是 空【下图】,进入 第二个 插件,
第二个插件的 callback 后果是 空【下图】,进入 JoinRequestPlugin
插件
3.4.2 视察 JoinRequestPlugin
插件的工作
看名字就晓得是干啥的,工作比较简单,就是把 path 和 request 合并成新的门路 赋值给 path
(绿色圈中局部),
resolver.join(request.path, request.request),
这个 hook 的事件实现了,有请下一个 hook relative
,以及它的两位监听者们。
3.5 视察 hook 名为relative
上面的两个工种(插件)的工作
兜兜转转的又进入 DescriptionFilePlugin
插件了,然而 此时的参数和之前的不一样了,然而如同也没有什么不同,最初还是 callback 为空,灰头土脸的走进下一个插件了。
持续走到 NextPlugin
,而后被送到 describedRelative
的 hook,此 hook 的监听者有:
3.5 视察 hook 名为describedRelative
上面的两个工种(条件插件)的工作
条件插件要满足的第一个逻辑就是,不是文件夹,揣测咱们是满足的,开始 debug。
plugins.push(
new ConditionalPlugin(
"described-relative",
{directory: false},
null,
true,
"raw-file"
)
);
plugins.push(
new ConditionalPlugin(
"described-relative",
{fullySpecified: false},
"as directory",
true,
"directory"
)
);
的确满足了不是文件夹的条件,推动到下一个 hook rawFile
,其相干的监听者有 5 个。
3.6 视察 hook 名为rawFile
上面的工种的工作
不满足此插件,走进下一个插件TryNextPlugin
:
// raw-file
plugins.push(
new ConditionalPlugin(
"raw-file",
{fullySpecified: true},
null,
false,
"file"
)
);
TryNextPlugin
(尝试下一个插件)的代码如下:
apply(resolver) {const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("TryNextPlugin", (request, resolveContext, callback) => {
resolver.doResolve(
target,
request,
this.message,
resolveContext,
callback
);
});
}
个人感觉其实此处的逻辑更应该是尝试下一个 hook
,而不是 插件
,所以改为 TryNextHook
更好. 之所以这么说看上面的代码:
plugins.push(new TryNextPlugin("raw-file", "no extension", "file"));
下面代码简略了解为,被查找的文件是 不带扩大的文件,能够间接走到 名为 file
的 hook 里。此 hook 的监听插件有:
那就持续走 NextPlugin
插件的逻辑,而后走向了 finalFile
的 hook【下图】, 进入 FileExistsPlugin
插件的逻辑里。
3.7 视察 hook 名为 finalFile
上面的工种FileExistsPlugin
插件的工作
代码比较简单:获取查找门路,直接判断是不是文件即可。
发现不是文件,那就执行 callback 函数,此插件的 callback 函数是Resolver 中的 hook.callAsync
中的 callback 函数
而后 Resolver 中的 hook.callAsync
中的 callback 函数承受到的 err 和 result 都是 undefined,就又走了 doResolve
中承受的 callback 函数,那就要开始从当初这个 finalFile
向前找了,查找的过程要疏忽掉 外卖小哥型插件 比方 TryNextPlugin
和NextPlugin
。
finalFile
上一个是 file
的 hook 监听(NextPlugin
可疏忽),file
的上一个是 raw-file
,触发 raw-file
下的插件的监听,接下来就是查找监听了 hook 位 raw-file
的插件了。
这块的代码可能因为都叫 callback,并且跳来跳去的有些难以了解,能够参考我上面简化过的代码。
let {AsyncSeriesBailHook} = require("tapable");
const hook1 = new AsyncSeriesBailHook(["request", "resolveContext"], "hook1");
const hook2 = new AsyncSeriesBailHook(["request", "resolveContext"], "hook2");
const hook1Tap1 = hook1.tapAsync(
"hook1Tap1",
(request, resolveContext, callback) => {console.log("hook1Tap1", request, resolveContext);
return callback();}
);
const hook1Tap2 = hook1.tapAsync(
"hook1Tap2",
(request, resolveContext, callback) => {console.log("hook1Tap2", request, resolveContext);
return callback();}
);
const hook2Tap1 = hook2.tapAsync(
"hook2Tap1",
(request, resolveContext, callback) => {console.log("hook2Tap1", request, resolveContext);
return callback();}
);
const hook2Tap2 = hook2.tapAsync(
"hook2Tap2",
(request, resolveContext, callback) => {console.log("hook2Tap2", request, resolveContext);
return callback("err");
}
);
hook1.callAsync("111", "222", () => {console.log("hook1 callback");
hook2.callAsync("333", "455", err => {console.log("hook2 callback", err);
});
});
执行后果如下:
这块的内容是定义了两个 异步的 hook
, 而后在 hook1 调用 callAsync
的时候,外面传递了 hook2 的 callAsync
调用,这样就会在调用完 hook1
的触发事件,而后去接着调用 hook2
的触发事件。
这样是不是能够了解 多个 hook 之前传递 callback 的逻辑了?
那么接下来就要找监听了 hook 名为 raw-file
的插件有哪些了,间接看 ResolverFactory
注册工夫得悉【下图】,有 3 个插件监听了。而当初的程序 又是依照监听程序倒着执行 callback 的,那就应该是先执行 AppendPlugin
插件了,打上断点,跑一下
3.8 回首掏,去视察 hook 名为 raw-file
上面的工种AppendPlugin
插件的工作
AppendPlugin
代码较为简单,就是把传入的 this.appending
和 request.path
进行拼接,生成新的 request.path
,
module.exports = class AppendPlugin {constructor(source, appending, target) {
this.source = source;
this.appending = appending;
this.target = target;
}
apply(resolver) {const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("AppendPlugin", (request, resolveContext, callback) => {
const obj = {
...request,
path: request.path + this.appending,
relativePath:
request.relativePath && request.relativePath + this.appending
};
resolver.doResolve(
target,
obj,
this.appending,
resolveContext,
callback
);
});
}
};
查找 this.appending
是在实例化时候传入的,断点得悉。这个就是咱们传入的 extensions
配置
const myResolver = ResolverFactory.createResolver({fileSystem: new CachedInputFileSystem(fs, 4000),
extensions: [".json", ".js", ".ts"]
});
而后断点到此处,看吃进去了啥,吐出来了啥。
而后下一个 hook 是 file
,只有一个 NextPlugin
插件监听了此 hook,用来推动流程【下图】。
而 NextPlugin
插件是将流程 从 file
推向了 final-file
hook,走到 3.7 的流程,判断一下带有此后缀的文件是否存在,不存在的话,持续 反复 raw-file
hook 的 AppendPlugin
的流程,此时的参数是 this.appending
是 .js
【下图】
持续 反复以上的操作:NextPlugin
插件是将流程 从 file
推向了 final-file
hook,而后 FileExistsPlugin
插件判断到,此文件存在,推动流程到 existingFile
的 hook,此 hook 有 2 个插件监听【下图】。
3.9 文件存在了,下一步去视察 hook 名为existingFile
上面的插件的工作
先去执行SymlinkPlugin
通过 fs.readlink
办法判断其是否是符号链接下的文件,符号链接 symlink_什么是符号链接或符号链接?如何为 Windows 和 Linux 创立 Symlink?_cunjiu9486 的博客 -CSDN 博客,
再补充一点 硬链接和软链接的区别?– 掘金 (juejin.cn)
对于符号链接这里有非凡阐明,假如你新建了
b.js
,删除了当前目录下的a.js
,当前目录状况如下:
建设硬链接 进行测试:
建设软链接,进行测试:
其实软链接,还辨别绝对路径和相对路径的状况【下图】,本次只思考相对路径,大家能够应用绝对路径进行 debug.
咱们进行软链接的 debug,最初发现查找到 b.js 的门路,那么持续 debug。
到此是发现了软链接的源文件,那么下一步必定是判断 此源文件是否是存在,又走到 existingFile
的 hook【下图】,反复 3.9 的步骤,又走 SymlinkPlugin
插件的逻辑(放心软链接的源文件还是软链接),
持续 debug SymlinkPlugin
,发现走到了 callback()
的状况【下图】,那就是要进入下一个监听者(NextPlugin)了,
在 NextPlugin
中发现终于走到了最初的 hook resolved
,只有一个插件 ResultPlugin
进行监听。
进入 ResultPlugin
插件外部,其次要是调用了 result
的 hook,
apply(resolver) {
this.source.tapAsync(
"ResultPlugin",
(request, resolverContext, callback) => {const obj = { ...request};
if (resolverContext.log)
resolverContext.log("reporting result" + obj.path);
resolver.hooks.result.callAsync(obj, resolverContext, err => {if (err) return callback(err);
if (typeof resolverContext.yield === "function") {resolverContext.yield(obj);
callback(null, null);
} else {callback(null, obj);
}
});
}
);
}
debug 一下那些插件监听了此 hook,发现是空的,间接走到本身的 callback 函数里,
持续 debug 此 callback 函数,就会发现这个 callback 在一层一层的向上传递值,接着传到 Resolver 里的 resolve 函数
里,通过 finishResolved
解决解析一次【下图】,最初传递给 咱们本身的 callback 函数里。
debug 停在咱们本人监听的 callback 函数里,至此实现整体流程。
4 完结撒花,回顾总结。
通过一步一步的 debug,会发现 enhance-resolve 这个库,把 tapable 给用的炉火纯青,外围的解决逻辑都在 Resolver
上,而 ResolverFactory
则像是 流水线的 线长,借用Resolver
的能力,去指定流水线的流程,调配流水线每个流程应该合作的工种。
总的逻辑通下来,你会发现,所有的插件都是在对 obj 对象
做数据变更,每个插件都有本人的职责,互不干涉,互不影响,通过 NextPlugin
,这个外卖小哥插件,把 数据在各个 hook 流程之间进行流转,进而建设起一套高效的流水线零碎,耦合性低,定制化水平高,功能强大
。
这里就不画流程图做总结了,偷个懒,因为此文章耗时 7 个小时
左右(啊,我的眼镜),从头到尾 debug 下来,发现播种不少,当前齐全能够模拟此库基于本人的业务流程,开发定制一套属于本人的高效可定制化的可插拔插件的工程。
心愿大家看完此文章会有所播种,缓缓的开始本人的学习源码之路。冲吧,兄弟们。
别忘记思考解答一下结尾的问题,学有所获。下一篇文档 的方向是 解析 webpack 源码。