乐趣区

关于前端:太强了仅3000字解析webpack核心库enhancedresolve流程和插拔式插件机制

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 中的数据流动。

退出移动版