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)
,【而后再为每个工种调配不同的工具】。局部外围代码如下:
// resolvefor (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-resolveplugins.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-fileplugins.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 源码。