0. 食用本文的文档阐明:

本篇文章 耗时 7个小时左右才竣工,篇幅波及到大量的源码及其剖析的过程图解和数据,浏览前,请保障本人有充沛的工夫,纵情的去享受排汇常识进入脑子的过程

因为篇幅无限,心愿你把握以下前置常识:

  1. 曾经学习过 enhanced-resolve 工作流程和插拔式插件机制,点这里温习:webpack 外围库 enhanced-resolve 工作流程和插拔式插件机制
  2. 理解 tabaple 是一个订阅公布的设计模式(晓得啥是订阅公布即可)
  3. 大抵理解 node 中的模块查找机制,如:
require(‘./xxx.js’);require('./xxx');require('xxx');

通过本文你将学到如下内容(或者带着如下疑难去学习):

  1. enhance-resolve是如何在简单的插件调用之间传递数据的?
  2. Resolver 和 ResolverFactory的关系是什么?
  3. Resolver是如何设计实现的?
  4. 软链接和硬链接是什么?区别在哪里?
  5. 如何开发一个enhance-resolve的插件利用到webpack 中?
  6. 如何去一步步的 debug 一个开源库?

1 webpack 和 enhance-resolve 的关系是什么?

webpack作为一个弱小的打包工具,其弱小的不仅仅是插件机制,还有其外围包enhance-resolve来实现模块的门路查找。性能上来说它能够加强Webpack的模块解析能力,使其更容易找到所需的模块,从而进步Webpack的性能和可维护性。从配置上来说它能够为Webpack解析器增加额定的搜寻门路以及解析规定,让Webpack更好地解释门路和文件,进而让webpack更加分心的做模块打包相干的事件。

理解完背景和需要当前,如果让咱们去实现一个enhance-resolve呢?

性能点:

  1. 首先解析器满足模块查找中的所有的规定 模块:通用JS模块 |节点.js v14.21.3 文档 (nodejs.org)
  2. 要和webpack一样,有弱小的插件加载机制和良好的配置性能

本人能够心中默默的想一下如何实现上述性能点呢?

2. 接下来就根据上述性能点通过代码去理解一下 enhance-resolve

咱们上回太强了,3000字图文并茂的解析 webpack 外围库 enhanced-resolve 工作流程和插拔式插件机制,真香 - 掘金 (juejin.cn)说到:

  1. ResolverFactory.createResolver 依据 Resolver 类创立实例: myResolve (吃了配置,吐出对象myResolve)
  2. myResolve 上 注册并订阅 大量的 hook (枪支弹药贮备好,一刻激发)
  3. 调用 myResolver.resolve 办法开始进行 文件解析 的主流程
  4. 外部通过 resolve.doResolve办法,开始调用第一个 hook: this.hooks.resolve
  5. 找到之前 订阅 hook 的 plugin:ParsePlugin
  6. ParsePlugin 进行初步解析,而后 通过doResolve 执行下一个 hook parsed-resolve,后期筹备工作完结,链式调用开始,真正的解析文件的流程也开始。

从下面的第2步开始整起,第2步注册了哪些hook呢?接下来开始瞅瞅

2.1 细细回顾 myResolve 上注册的hooks

代码跳转到 lib/ResolverFactory.js295 行左右,代码如下:

//// 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.jsresolve 办法中是查找门路开始的终点,首先就是把 用户传入的 门路 path 和 要查找文件的门路 request 赋值给 obj 对象 【此 obj 是外围对象,将在各个插件中流转批改】。

而后就开始调用本身的 doResolve 办法,正式开始流程了。

3. 从 resolve hook 开始的流程,到完结

断点到 doResolve办法的 hook.callAsync 局部,看下相干的参数。

从图中能够看出,此 hook 名为 resolve,入参有两个:Array(2)[request,resolveContext],绑定此 hook 的插件只有一个 ParsePlugin 的插件,传递上来的参数是 request 对象:pathrequest是重要的数据。

下一步就开始进入 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,其绑定的业务插件是 DescriptionFilePluginNextPlugin插件属于流程插件,能够疏忽。

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 向前找了,查找的过程要疏忽掉 外卖小哥型插件 比方TryNextPluginNextPlugin

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.appendingrequest.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 源码。