【webpack核心库】耗时7个小时,用近50张图来学习enhance-resolve中的数据流动和插件调度机制...

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

本篇文章 耗时 7个小时左右才完工,篇幅涉及到大量的源码及其分析的过程图解和数据,阅读前,请保证自己有充分的时间,尽情的去享受吸收知识进入脑子的过程

因为篇幅有限,希望你掌握以下前置知识:

  1. 已经学习过 `enhanced-resolve 工作流程和插拔式插件机制`,[点这里复习:webpack 核心库 enhanced-resolve 工作流程和插拔式插件机制](https://juejin.cn/post/7167978104881676319 "https://juejin.cn/post/7167978104881676319")
  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\)[1]

  2. 要和webpack一样,有强大的插件加载机制和良好的配置功能

自己可以心中默默的想一下如何实现上述功能点呢?

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

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

  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),【然后再为每个工种分配不同的工具】。部分核心代码如下:

// 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个:

4138cf2f6773ddf4a4b529c771faf8c6.jpeg
image.png

其中因为NextPlugin是流程推动性插件和业务逻辑无关,就过滤掉,还剩下 32个

8645e22e4504182fbd0083d3c8bca2ff.jpeg
image.png

2.2 开始调试正式流程吧 (流水线打开电源,跑起来了)

lib/Resolver.jsresolve 方法中是查找路径开始的起点,首先就是把 用户传入的 路径 path 和 要查找文件的路径 request 赋值给 obj 对象 【此 obj 是核心对象,将在各个插件中流转修改】。

57e15595a1294f8857d5e4593bda9fb8.jpeg
image.png

然后就开始调用自身的 doResolve 方法,正式开始流程了。

8c19fa9adb1ceeeb523c16778ae61690.jpeg
image.png

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

断点到 doResolve方法的 hook.callAsync 部分,看下相关的参数。

2d785ea7b32619d1aae34eacfb3c59e5.jpeg
image.png

从图中可以看出,此 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); 这里,处理完的数据如下:【思考一下吃了啥数据,吐出了啥数据?】

684d2dae6fdd1f59a0be74d5a834e1a4.jpeg
image.png
2ab0ee6d4f799728c13ff275d7942507.jpeg
image.png

ParsePlugin 吃了 obj,以后对其进行初步解析,增加了如下属性 【红色是吃进去的,绿色是吐出来的】

9cbdd337a5af53cd6e452619d498c7b0.jpeg
image.png

然后下一个要执行hook是parsedResolve,其绑定的业务插件是 DescriptionFilePluginNextPlugin插件属于流程插件,可以忽略。

3007d437864c407176f3c0820ea27468.jpeg
image.png

3.2 视察 DescriptionFilePlugin工种的工作

当前流程的 DescriptionFilePlugin 插件的核心是在 DescriptionFileUtils.loadDescriptionFile方法里,

e2ec704843dd38bdd62bbd40c3c73ecd.jpeg
image.png

当看到 ['package.json']的那一刻是不是可以联想并猜测到:此插件的作用就是在实现查找当前的路径 是否是一个 具有package.json文件的模块?继续debug loadDescriptionFile方法,

14a67b98ed3576b611eefbcab6663b78.jpeg
image.png

看到这个路径拼接,验证了猜想是正确的,继续 debug 发现,走到了此方法的 callback 函数里,执行了一个 cdUp 的方法。

a51b96fc9912f83560eedbec62f19aa8.jpeg
image.png

我们不去看方法实现,仅仅看变更,变量从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 文件给删除了,不删除的话找到这一级就停止了】 部分截图

d050dda17d84af828abe12ee9c44c40f.jpeg
image.png
bbd7ad994f044db15cdd797b1b2851cb.jpeg
image.png

最后找呀找呀,就是找不到一个目录具有package.json 文件,没办法只能走 callback 了。

16d4abd18693eb5d938ae5a7bed7eed7.jpeg
image.png

结果就是这个插件一顿 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 闪亮登场:

580f53c49df3afe251dfd9b592579cb2.jpeg
image.png

好家伙,下一个hook 是 rawResolve,让我们来看看他的监听者 都有谁,拉倒吧,还是 NextPlugin 外卖小哥,这就是外卖小哥点饭(外卖小哥送给外卖小哥)???

[3]

那就继续吧,看看这个 rawResolve 的下一个hook是谁,监听的插件都有谁?

3045d241d43b303c9763109fe78f4c54.jpeg
image.png

下一个 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 结果是 空【下图】,进入 第二个 插件,

5407bbcccbc0f9ec4e650fb9583b1669.jpeg
image.png

第二个插件的 callback 结果是 空【下图】, 进入 JoinRequestPlugin 插件

73c91d63f6dab43b04e42da42562b891.jpeg
image.png

3.4.2 视察 JoinRequestPlugin 插件的工作

看名字就知道是干啥的,任务比较简单,就是把 path 和 request 合并成新的路径 赋值给 path(绿色圈中部分),

resolver.join(request.path, request.request),
复制代码
1d2a99db2c3d52d5a1265466bf080dc9.jpeg
image.png

这个 hook 的事情完成了,有请下一个 hook relative,以及它的两位监听者们。

e69279cfb85e3d100c027d86e5f5a60c.jpeg
image.png

3.5 视察 hook 名为relative 下面的两个工种(插件)的工作

兜兜转转的又进入 DescriptionFilePlugin 插件了,但是 此时的参数和之前的不一样了,但是好像也没有什么不同,最后还是 callback 为空,灰头土脸的走进下一个插件了。

ec978e380c3ca1928a9fb92c97c3a30f.jpeg
image.png

继续走到 NextPlugin,然后被送到 describedRelative 的hook,此hook的监听者有:

f2e5bd9818ffae8b6a74ec6e3ffa35cc.jpeg
image.png

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"
 )
);
复制代码

eb414b99720203622abc1894b440afd8.jpeg 确实满足了不是文件夹的条件,推进到下一个hook rawFile,其相关的监听者有5个。

47bff3155c0945aad221e8f0e45017fe.jpeg
image.png

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 的监听插件有:

a1e6e66c5fced7ed0b4279419ddbc97f.jpeg
image.png

那就继续走 NextPlugin 插件的逻辑,然后走向了 finalFile 的 hook 【下图】,进入 FileExistsPlugin 插件的逻辑里。

2f2bc150152fe37427588c34dd71c288.jpeg
image.png

3.7 视察 hook 名为finalFile 下面的工种FileExistsPlugin插件的工作

代码比较简单:获取查找路径,直接判断是不是文件即可。

527e8edbb51e20692071b847907fb387.jpeg
image.png

发现不是文件,那就执行callback函数,此插件的callback函数是Resolver 中的 hook.callAsync 中的callback 函数

d24317517ed332772ca71fe0d9645830.jpeg
image.png

然后 Resolver 中的 hook.callAsync 中的 callback 函数接受到的 err 和 result 都是 undefined,就又走了 doResolve 中接受的 callback 函数,那就要开始从现在这个 finalFile 向前找了,查找的过程要忽略掉 外卖小哥型插件 比如TryNextPluginNextPlugin

5ffcb5a873a190a7a421a8b3b29d7756.jpeg
image.png

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);
 });
});
复制代码

执行结果如下:

71d8f89f167f6f42900b8f659b1b15a3.jpeg
image.png

这块的内容是定义了两个异步的hook,然后在hook1 调用 callAsync 的时候,里面传递了 hook2 的 callAsync 调用,这样就会在调用完 hook1 的触发事件,然后去接着调用 hook2 的触发事件。

这样是不是可以理解 多个hook 之前传递 callback 的逻辑了?

那么接下来就要找监听了hook名为 raw-file 的插件有哪些了,直接看 ResolverFactory 注册时间得知 【下图】,有3个插件监听了。而现在的顺序 又是按照监听顺序倒着执行callback的,那就应该是先执行 AppendPlugin 插件了,打上断点,跑一下

da2a238762802d41a927ad046c2d89f6.jpeg
image.png

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"]
});
复制代码
62ccbf9599ddd9ee54f51156081157e3.jpeg
image.png

然后断点到此处,看吃进去了啥,吐出来了啥。

0331cbf54d9c95988ad0aa6a5c946352.jpeg
image.png

然后下一个 hook 是 file,只有一个 NextPlugin 插件监听了此hook,用来推进流程【下图】。

d8f17bd73156b3e579990d91932bdbad.jpeg
image.png

NextPlugin 插件是将流程 从 file 推向了 final-file hook,走到 3.7 的流程,判断一下带有此后缀的文件是否存在,不存在的话,继续 重复 raw-file hook 的 AppendPlugin 的流程,此时的参数是 this.appending.js 【下图】

3dfe7dbc50fb9196caa351dcbcf4e8f8.jpeg
image.png

继续 重复以上的操作:NextPlugin 插件是将流程 从 file 推向了 final-file hook,然后 FileExistsPlugin 插件判断到,此文件存在,推进流程到 existingFile 的hook,此hook 有2个插件监听【下图】。

b8cd523f971fe1c67f588446c59ae6ca.jpeg
image.png

3.9 文件存在了,下一步去视察 hook 名为existingFile 下面的插件的工作

先去执行SymlinkPlugin 通过 fs.readlink 方法判断其是否是符号链接下的文件,符号链接symlink\_什么是符号链接或符号链接?如何为Windows和Linux创建Symlink?\_cunjiu9486的博客-CSDN博客[4]

再补充一点 硬链接和软链接的区别?\- 掘金 \(juejin.cn\)[5]

关于符号链接这里有特殊说明,假设你新建了 b.js,删除了当前目录下的 a.js,当前目录情况如下:

952b26d3d109b24f193e7fc31f1384b0.jpeg 建立硬链接 进行测试:

054c0f3559bb40ef25d856f389a30394.jpeg
image.png

建立软链接,进行测试:

8f6a8bfa9b4f1ad0881c40ec0e31057a.jpeg
image.png

其实软链接,还区分绝对路径和相对路径的情况【下图】,本次只考虑相对路径,大家可以使用绝对路径进行debug.

1082ef1051384232f8b91866af68f00f.jpeg
image.png

我们进行软链接的debug,最后发现查找到b.js 的路径,那么继续debug。

2274bb8115cade3a82aa9a9d5882d2a9.jpeg
image.png

到此是发现了软链接的源文件,那么下一步肯定是判断 此源文件是否是存在,又走到 existingFile的 hook 【下图】,重复3.9的步骤,又走 SymlinkPlugin 插件的逻辑(担心软链接的源文件还是软链接),

e14ad97b90333f9b5e236b9048f79c12.jpeg
image.png

继续debug SymlinkPlugin,发现走到了 callback() 的情况【下图】,那就是要进入下一个监听者 (NextPlugin)了,

ee37071f2cf1a647b1afc84dc1dc6eff.jpeg
image.png

NextPlugin中发现终于走到了最后的hook resolved,只有一个插件 ResultPlugin 进行监听。

76fc63ea4a46dddd60f0731f16d0dc84.jpeg
image.png

进入 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 函数里,

59548afd3c76514e5cd5852e93eef656.jpeg
image.png

继续debug 此 callback 函数,就会发现这个 callback 在一层一层的向上传递值,接着传到 Resolver 里的 resolve 函数里, 经过 finishResolved 处理解析一次【下图】,最后传递给 我们自身的callback 函数里。

15c6b2b1c870144abc53bd06cdb7f65e.jpeg
image.png

debug 停在我们自己监听的callback 函数里,至此完成整体流程。

a6d0e79e662b78b3e6df359067009f57.jpeg
image.png

4 完结撒花,回顾总结。

通过一步一步的debug,会发现 enhance-resolve 这个库,把 tapable 给用的出神入化,核心的处理逻辑都在 Resolver 上,而 ResolverFactory 则像是 流水线的 线长,借用Resolver 的能力,去指定流水线的流程,分配流水线每个流程应该协作的工种。

总的逻辑通下来,你会发现,所有的插件都是在对 obj 对象做数据变更,每个插件都有自己的职责,互不干涉,互不影响,通过 NextPlugin,这个外卖小哥插件,把 数据在各个 hook 流程之间进行流转,进而建立起一套高效的流水线系统,耦合性低,定制化程度高,功能强大

这里就不画流程图做总结了,偷个懒,因为此文章耗时 7个小时左右 (啊,我的眼镜),从头到尾 debug 下来,发现收获不少,以后完全可以模仿此库基于自己的业务流程,开发定制一套属于自己的高效可定制化的可插拔插件的工程。

希望大家看完此文章会有所收获,慢慢的开始自己的学习源码之路。冲吧,兄弟们。

另外放出一个基于此库开发的一个根据不同文件后缀进行条件编译的插件:\@fu1996/webapck-resolver-mode-plugin \- npm \(npmjs.com\)[6]

兄弟们,别忘记思考解答一下开头的问题,学有所获。下一篇文档 的方向是 解析webpack 源码。

5c54b5bfba181ed12b39563f81fa47fb.jpeg
image.png

关于本文

作者:付俊奎

https://juejin.cn/post/7204356282588676156

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

回复「算法」,加入前端编程源码算法群,每日一道面试题(工作日),第二天瓶子君都会很认真的解答哟!

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持

 》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值