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

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

  • 心愿你最好理解 订阅公布模型
  • 心愿你晓得tapable 的 以下 3 个钩子函数AsyncSeriesBailHook, AsyncSeriesHook, SyncHook

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

  • 如何调试一个 nodejs 开源库
  • 理解 webpack 解析库 enhance-resolve 的大抵工作流程
  • 初步理解 webpack/enhance-resolve 中 tapable 的应用,以及插件机制实现的原理 (这里写 webpack,是因为二者的插件机制是一样的实现原理)

本文 GitHub 解析地址: fu1996/enhanced-resolve at feature-study-enhanced (github.com),先看全文,再思考要不要给个star⭐️。

1. 初步理解该库的作用,明确这个库是干啥的?

想初步理解一个库的作用,以及建设初衷,最好的形式就是浏览以后库的README.md(前提是该库作者保护了此文档 )。

README.md内容如下:


翻译为中文就是:


大家想理解对于原生的 require.resolve 的介绍能够看这篇文章 ===> node 的门路解析 require.resolve - 掘金 (juejin.cn)

该库也是作为 webpack 里外围的依赖解析库存在,在 webpack.config.js 里配置的 resolve 字段 实际上就是当做参数传递给该库的,所以深刻的理解一下该库的工作原理以及插件机制的实现,也有益于 webpack 的优化 和 前期浏览 webpack 源码。

2. 拉取并跑起来一个简略的 demo,初步理解该库对于 resolve 的 enhance (加强)

GitHub 地址如下:webpack/enhanced-resolve: Offers an async require.resolve function. It's highly configurable. (github.com) PS: 国外拜访较慢,强烈推荐 应用 Gitee 导入该仓库 【不会吧,不会吧,都 2023 年了,居然还有人不晓得这个办法?】

代码拉取结束当前,察看我的项目目录,发现应用的 yarn,执行 yarn install 进行装置依赖装置。如果没报错的话,写一个简略的 demo 小试牛刀。

新建一个 demo 文件夹,并创立 test-hook.js (名称能够自定义),而后写入如下内容:

const { ResolverFactory, CachedInputFileSystem } = require("../lib");const fs = require("fs");const path = require("path");const myResolver = ResolverFactory.createResolver({  fileSystem: new CachedInputFileSystem(fs, 4000),  extensions: [".json", ".js", ".ts"],});const context = {};const resolveContext = {};const lookupStartPath = path.resolve(__dirname);const request = "./a";myResolver.resolve(  context,  lookupStartPath,  request,  resolveContext,  (err, path, result) => {    if (err) {      console.log("createResolve err: ", err);    } else {      console.log("createResolve path: ", path);    }  });

新建 a.js 文件(不用写入内容,该库只做门路解析), 此时文件目录如下:

运行test-hook.js 输入如下:

demo 运行胜利,第 2 关通过

3. 开启 Debug 模式,剖析大体逻辑

自己喜爱用 webStorm 进行调试 (之前是搞 Python 开发的,习惯了)。

3.1 webStorm 应用 debug 模式 (不是本文重点,简略阐明一下)

webStorm 的只须要 以后文件 下 右击,而后 点击 Debug test-hook.js 即可

3.2 vscode 应用 debug 模式

vscode 的 debug 形式很多,这里只说一个 自带 debug 终端 的调试办法,此法也是很不便调试 node 程序的。


点击结束当前,产生一个新的终端:(下面的 ws 地址 请自行摸索)

新的终端默认是在 根目录下的,轻易在 test-hook.js 打一个 断点,而后 运行 node 命令:

node demo/test-hook.js

它就进来了。

3.2 剖析大体逻辑

3.2.1 应用 ResolverFactory 工厂类 调用 createResolver 办法 创立一个 resolver 实例

const myResolver = ResolverFactory.createResolver({  fileSystem: new CachedInputFileSystem(fs, 4000),  extensions: [".json", ".js", ".ts"],});

我看到这段的代码的次要逻辑就是去想:这办法吃了啥?吐出了啥?能依据变量名失去啥? 而后再去看办法的大抵实现。

  1. 这办法吃了 相似于 webpack resolver 里的配置
  2. 从命名来猜想 这办法吐出了 一个 myResolver 的 对象

3.2.2 进入 createResolver 办法 大抵剖析流程 (进入该办法:按住 Ctrl + 【鼠标左键点击】)

这里只贴局部外围代码

exports.createResolver = function (options) { // 解析并规范化用户传入的配置 const normalizedOptions = createOptions(options); const {  plugins: userPlugins, } = normalizedOptions; // 深拷贝一下 用户用到的 plugins const plugins = userPlugins.slice(); // 依据配置创立 resolver 实例 const resolver = customResolver  ? customResolver  : new Resolver(fileSystem, normalizedOptions); //// pipeline //// // 确保该 hook 存在,不存在则注册它 resolver.ensureHook("resolve"); resolver.ensureHook("internalResolve"); // 依据配置 把用到的 内置 plugin 丢到 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"));  } } // ...省略局部plugins.push的逻辑代码... //// RESOLVER //// // 遍历 plugins 列表 并传入resolver 实例 for (const plugin of plugins) {  if (typeof plugin === "function") {   // 是函数 this 指向 resolver   plugin.call(resolver, resolver);  } else {    // 是类, 开始调用apply 办法 ,apply 办法 会注册一些 下面ensure的 hook   plugin.apply(resolver);  } } // 返回resolve 对象 return resolver;};

一个简略的流程图如下:

plugin.apply(resolver); 所有的事件 都曾经胜利订阅。

所有的钩子都在 resolver 对象 身上了 (子弹曾经上膛,筹备发射)。

3.3 粗略过下 Resolver 类的办法

咱们应用 resolver 的 形式如下:

const context = {};const resolveContext = {};const lookupStartPath = path.resolve(__dirname);const request = "./a";myResolver.resolve(  context,  lookupStartPath,  request,  resolveContext,  (err, path, result) => {    if (err) {      console.log("createResolve err: ", err);    } else {      console.log("createResolve path: ", path);    }  });

那第一步就是 看 resolve 办法

3.3.1 初步理解 resolve 办法

外围代码如下:

看源代码时候不能心急,第一步 应该保大丢小,先把握全局视角,而后一一深刻,看到前期,会有豁然开朗的感觉,原来那块写的是这个意思啊。

class Resolver {  resolve(context, path, request, resolveContext, callback) {    // 所有流程的外围 就是这个 obj 对象    const obj = {      context: context,      path: path,      request: request,    };    const message = `resolve '${request}' in '${path}'`;    const finishResolved = (result) => {      return callback(        null,        result.path === false          ? false          : `${result.path.replace(/#/g, "\0#")}${              result.query ? result.query.replace(/#/g, "\0#") : ""            }${result.fragment || ""}`,        result      );    };    const finishWithoutResolve = (log) => {      /`       * @type {Error & {details?: string}}       */      const error = new Error("Can't " + message);      error.details = log.join("\n");      this.hooks.noResolve.call(obj, error);      return callback(error);    };    if (resolveContext.log) {      // We need log anyway to capture it in case of an error      const parentLog = resolveContext.log;      const log = [];      return this.doResolve(        this.hooks.resolve,        obj,        message,        {          log: (msg) => {            parentLog(msg);            log.push(msg);          },          yield: yield_,          fileDependencies: resolveContext.fileDependencies,          contextDependencies: resolveContext.contextDependencies,          missingDependencies: resolveContext.missingDependencies,          stack: resolveContext.stack,        },        (err, result) => {          if (err) return callback(err);          if (yieldCalled || (result && yield_)) return finishYield(result);          if (result) return finishResolved(result);          return finishWithoutResolve(log);        }      );    } else {      // Try to resolve assuming there is no error      // We don't log stuff in this case      return this.doResolve(        this.hooks.resolve,        obj,        message,        {          log: undefined,          yield: yield_,          fileDependencies: resolveContext.fileDependencies,          contextDependencies: resolveContext.contextDependencies,          missingDependencies: resolveContext.missingDependencies,          stack: resolveContext.stack,        },        (err, result) => {          if (err) return callback(err);          if (yieldCalled || (result && yield_)) return finishYield(result);          if (result) return finishResolved(result);          // log is missing for the error details          // so we redo the resolving for the log info          // this is more expensive to the success case          // is assumed by default          const log = [];          return this.doResolve(            this.hooks.resolve,            obj,            message,            {              log: (msg) => log.push(msg),              yield: yield_,              stack: resolveContext.stack,            },            (err, result) => {              if (err) return callback(err);              // In a case that there is a race condition and yield will be called              if (yieldCalled || (result && yield_)) return finishYield(result);              return finishWithoutResolve(log);            }          );        }      );    }  }}

大抵看完,发现这一步其实也是依据不同的条件去组装数据,把传入的数据,赋值到 obj 对象上,而后把 obj 对象传入doResolve 办法,当做此办法的第二个参数,真正调用的还是 doResolve 办法,下一步就是大抵瞅下doResolve办法。

3.3.2 初步理解 doResolve 办法

下面resolve传递的 obj 对象作为 doResolve 的第二个参数,命名为:request,一起来看下。

doResolve(hook, request, message, resolveContext, callback) { // 静态方法 依据以后 hook 信息 生成 调用栈信息 const stackEntry = Resolver.createStackEntry(hook, request); let newStack; // 以后 hook 调用栈信息 存入 newStack 里 if (resolveContext.stack) {  newStack = new Set(resolveContext.stack);  if (resolveContext.stack.has(stackEntry)) {   /`    * Prevent recursion    * @type {Error & {recursion?: boolean}}    */   const recursionError = new Error(    "Recursion in resolving\nStack:\n  " +     Array.from(newStack).join("\n  ")   );   recursionError.recursion = true;   if (resolveContext.log)    resolveContext.log("abort resolving because of recursion");   return callback(recursionError);  }  newStack.add(stackEntry); } else {  newStack = new Set([stackEntry]); } // 传入 hook, request 调用 resolveStep 的 hook this.hooks.resolveStep.call(hook, request); // 如果以后hook 被应用了 if (hook.isUsed()) {  const innerContext = createInnerContext(   {    log: resolveContext.log,    yield: resolveContext.yield,    fileDependencies: resolveContext.fileDependencies,    contextDependencies: resolveContext.contextDependencies,    missingDependencies: resolveContext.missingDependencies,    stack: newStack   },   message  );  // 触发以后hook 并传入 request 和 innerContext 当做参数  return hook.callAsync(request, innerContext, (err, result) => {   if (err) return callback(err);   if (result) return callback(null, result);   callback();  }); } else {  // 执行 callback 逻辑  callback(); }}

callback的逻辑比较简单,咱们应该看以后 hook (指的是:this.hooks.resolve)被应用的时候,resolve 的解决逻辑。

要害代码如下:

hook.callAsync(request, innerContext, (err, result) => {

以后 hook 间接调用了 callAsync 进行了 触发之前 plugin 的订阅事件,这时候咱们要去找到之前 plugin.apply(resolver); 的时候,哪一个 plugin 的订阅类型 为resolve 事件。

3.3.3 去 ResolverFactory.js 文件寻找注册了 resolve 事件的 钩子

场景切回到 ResolverFactory.js 文件,不言而喻的在 327 行左右 看到了这个注册事件,此 demo 的 unsafeCachefalse 所以此处 执行的是 347 行的代码 (对于此参数的作用,先 TODO 下,第一次看源码不能追深,应该追广)。这次要进入ParsePlugin 插件里,看它到底实现了哪些逻辑。(优良的开源库,对于事件和数据的解决就是这么 callback,必须急躁 )

3.3.4 去 ParsePlugin 插件里,看最初一层的解决逻辑,实现闭环

ParsePlugin 插件,是以后主流程 闭环的完结,也是 文件解析 流程的开始,因为 从文章结尾开始到当初,还没有真正的针对 文件解析 相干的事件 做相干操作,全是在注册一些 hook,实例化 Resolve 对象,解决格式化入参。

上代码,看具体逻辑,现身吧 我的小宝贝

/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra*/"use strict";/` @typedef {import("./Resolver")} Resolver *//` @typedef {import("./Resolver").ResolveRequest} ResolveRequest *//` @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */module.exports = class ParsePlugin { /`  * @param {string | ResolveStepHook} source source  * @param {Partial<ResolveRequest>} requestOptions request options  * @param {string | ResolveStepHook} target target  */ constructor(source, requestOptions, target) {  // 承受参数 并绑定到this 上  this.source = source;  this.requestOptions = requestOptions;  this.target = target; } /`  * @param {Resolver} resolver the resolver  * @returns {void}  */ apply(resolver) {  // 这个resolver 就是之前 创立的 Resolver  的实体类  const target = resolver.ensureHook(this.target);  resolver   // 失去 this.source 对应的 hook   .getHook(this.source)   // 监听 this.source 对应的 hook,并设置 订阅函数   .tapAsync("ParsePlugin", (request, resolveContext, callback) => {    // 先初步解析 失去大抵后果:    const parsed = resolver.parse(/` @type {string} */ (request.request));    // 合并参数    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: ""     };     // 这个 hook 做完了 它该做的事件了 进入 this.target 的 hook 逻辑吧,     // 并把以后hook 解决过的后果传递给this.target 的 hook     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);   }); }};

你会发现这个插件 的确开始 进行 request 字段的解析了,终于 它开始剖析你在 test-hook.js 传入的 "./a" 到底是文件夹,还是文件了。

const request = "./a";

在该插件又通过一系列的解析当前,发现又开始应用 resolver.doResolve 办法 流转到 this.target 的 hook 了。
场景回溯:

先回溯一下以后的 this.target 是代表的那个参数?

plugins.push(new ParsePlugin(source, resolveOptions, "parsed-resolve"));

而后回忆一下resolver.doResolve 办法做了啥? 此时 hook 的入参是 "parsed-resolve", request 参数代表的是 resolve hook 解决过的 alternative 变量。

doResolve(hook, request, message, resolveContext, callback) { // 静态方法 依据以后 hook 信息 生成 调用栈信息 const stackEntry = Resolver.createStackEntry(hook, request); let newStack; // 以后 hook 调用栈信息 存入 newStack 里 if (resolveContext.stack) {  newStack = new Set(resolveContext.stack);  if (resolveContext.stack.has(stackEntry)) {   /`    * Prevent recursion    * @type {Error & {recursion?: boolean}}    */   const recursionError = new Error(    "Recursion in resolving\nStack:\n  " +     Array.from(newStack).join("\n  ")   );   recursionError.recursion = true;   if (resolveContext.log)    resolveContext.log("abort resolving because of recursion");   return callback(recursionError);  }  newStack.add(stackEntry); } else {  newStack = new Set([stackEntry]); } // 传入 hook, request 调用 resolveStep 的 hook this.hooks.resolveStep.call(hook, request); // 如果以后hook 被应用了 if (hook.isUsed()) {  const innerContext = createInnerContext(   {    log: resolveContext.log,    yield: resolveContext.yield,    fileDependencies: resolveContext.fileDependencies,    contextDependencies: resolveContext.contextDependencies,    missingDependencies: resolveContext.missingDependencies,    stack: newStack   },   message  );  // 触发以后hook 并传入 request 和 innerContext 当做参数  return hook.callAsync(request, innerContext, (err, result) => {   if (err) return callback(err);   if (result) return callback(null, result);   callback();  }); } else {  // 执行 callback 逻辑  callback(); }}

所以以后的this.target 指的是parsed-resolve 相干的 hook,相当的见名知意。至于接下来的流程,打算另开一篇文章去 讲解 resolver 具体的 hook 流转过程,感兴趣的兄弟们能够本人拉代码进行学习。

4. 完结撒花

终于,通过了一路的兜兜转转,这个 resolve 终于开始解析了。来张流程图,总结一下全文。

  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,后期筹备工作完结,链式调用开始,真正的解析文件的流程也开始。

本文 GitHub 解析地址: fu1996/enhanced-resolve at feature-study-enhanced (github.com),看到这里,如果感觉头痒(是要长常识了),学到了一丢丢常识,欢送各位大佬点start
初步确定下一篇文档:enhance-resolve 中的数据流动。