涨薪面技:写个 enhanced-resolve 插件(3)

一、前文回顾

本文详细介绍了 webpack.config.js.resolve 和 resolveLoader 选项对象的各个配置项及其作用。

下一篇我们正式进入到 createResolver 的重点环节 ------ 流水线钩子的注册!

二、enhanced-resolve 与构建流程

在 NormaoModuleFactory 模块构建过程中,参与模块路径的解析,将解析所得的 loader 和模块路径交 给后续的构建流程。

2.1 getResolver

NormalModuleFactory.prototype.getResolver 方法,调用 this.resolverFactory.get 方法

kotlin 复制代码
class NormalModuleFactory {
  // ....
  getResolver(type, resolveOptions) {
    return this.resolverFactory.get(type, resolveOptions);
  }
}

2.2 webpack/lib/ResolverFactory.get

该方法来自 webpack/lib/ResolverFactory.js 的静态方法:

js 复制代码
class ResolverFactory {
   get(type, resolveOptions = EMPTY_RESOLVE_OPTIONS) {
     let typedCaches = this.cache.get(type);
     if (!typedCaches) {
      typedCaches = {
       direct: new WeakMap(),
       stringified: new Map()
      };
      this.cache.set(type, typedCaches);
     }
     // 获取缓存
     const cachedResolver = typedCaches.direct.get(resolveOptions);
     if (cachedResolver) {
      // 命中缓存
      return cachedResolver;
     }
     const ident = JSON.stringify(resolveOptions);
     const resolver = typedCaches.stringified.get(ident);
     if (resolver) {
      typedCaches.direct.set(resolveOptions, resolver);
      return resolver;
     }
     
     // 无缓存创建新的 resolver
     const newResolver = this._create(type, resolveOptions);
     typedCaches.direct.set(resolveOptions, newResolver);
     typedCaches.stringified.set(ident, newResolver);
     return newResolver;
   }
   _create () {
   
    const originalResolveOptions = { ...resolveOptionsWithDepType };

    const resolveOptions = convertToResolveOptions(
     this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType)
    );
    const resolver = (
     // 新建 Resolver
     Factory.createResolver(resolveOptions)
    );

    const childCache = new WeakMap();
    resolver.withOptions = options => {
     const cacheEntry = childCache.get(options);
     if (cacheEntry !== undefined) return cacheEntry;
     const mergedOptions = cachedCleverMerge(originalResolveOptions, options);
     const resolver = this.get(type, mergedOptions);
     childCache.set(options, resolver);
     return resolver;
    };
    this.hooks.resolver
     .for(type)
     .call(resolver, resolveOptions, originalResolveOptions);
    return resolver;
   }
}

webpack/lib/ResolverFactory.js 的 _create 方法调用了 enhanced-resolve/lib/ResolverFactory.js 的 createResolver 方法:

三、流水线注册

3.1 // resolve: 开始 resolve

如果启用 unsafeCache 注册则在 new-resolvenew-internal-resolve source 钩子注册两个插件:UnsafeCachePlugin、ParsePlugin 插件,这两个插件的 target 都是 parsed-resolve;如果没未启用,则为 resolveinternal-resolve source 钩子注册 ParsePlugin,这两个钩子的 target 钩子都是 parsed-resolvewebpack.config.js.resolve.unsafeCache 传送门,缓存一切!

3.2 // parsed-resolve: request 解析阶段

parsed-resolve source 钩子注册 DescriptionFilePlugin 插件,该插件的 target 钩子为 described-resolve 注册 NextPlugin,引导从 described-resolved source 钩子到 raw-resolve target 钩子;

3.3 // described-resolve 描述文件已解析

如果 fallback.length 不为0,fallback 的作用是当解析失败时的解析重定向,详细配置 resolve.fallback 配置传送门。具体实现:为 described-resolved source 钩子注册 AliasPlugin,其 target 钩子为 internal-resolve , 这个 internal-resolve 在前面 【4】中注册了 ParsePlugin,这就说明这个执行流程被退回到了 internal-resolve 阶段。啥意思嘞,很简单,alias 被分析后需要重新解析,重新走流程;

3.4 // raw-resolve: 原始解析阶段

  1. 如果 alias.length 不为 0,说明配置了 resolve.alias 选项。则为 raw-resolve source 钩子注册 AliasPlguin,其 target 钩子为 internal-resolve
  2. 遍历 aliasFields 配置,该配置源数据来自 webpack.cofig.js.resolve.aliasFields 字段,表示的 resolve.aliasFields 传送门 ,这个玩意儿是当 webpack 构建的代码产物运行于浏览器环境时告知 resolver 解析模块过程中需要查找的 package.json 中代表浏览器入口文件的标识字段。有点拗口了。。。我也组织好几次才说出来。。。就是说:有的 npm 包里面的某些文件不能直接在浏览器运行,而 npm 包的作者提供了另一个文件专门用在浏览器环境,npm 包作者通过在它包里面的 package.json.browser 字段告诉你这个浏览器版本的文件是哪个,相当于官方给你 hack 好了的。这种情况下需要 enhanced-resolver 专门处理。处理插件就是 AliasFieldPlugin 插件,该插件注册的 source 钩子 raw-resolve,target 钩子 internal-resolve 钩子。
  3. 遍历 extensionAlias,给每个 extensionAlias 注册 ExtensionAliasPlugin 插件,该插件的 source 钩子 raw-resolve,target 钩子是 normal-resolve。extensionAlias 是扩展名的别名映射。上webpack.config.js.resolve.extensionAlias 传送门
  4. 注册 NextPlugin 插件,引导从 raw-resolve source 钩子到 normal-resolve target 钩子。

三、总结

受限于篇幅,这里并没有完全完成整个流水线的注册工作,之所以不在一篇写完也是不想给各位读者带来过大的压力!

我们大致回故一下从今天的流水线内容:

  1. resolve 开始解析:注册 UnsafeCachePlugin、ParsePlugin 插件;
  2. paresed-resolve:request 解析阶段;
  3. described-resolve 描述文件已解析;
  4. raw-resolve: 原始解析阶段,处理 alias/aliasFields/extension/extensionAlias;
相关推荐
未秃头的程序猿3 小时前
Java 26正式发布!这3个新特性,让代码量直接减半
java·后端·面试
默_笙4 小时前
🍞 我用 CSS 画了一个会转的 3D 立方体,同事以为我学了 Three.js(这节课真的很神奇,我很喜欢)
javascript
sarasuki4 小时前
JavaScript的对象、new的机制与原型包装类
javascript·后端
weedsfly4 小时前
JavaScript 事件流:彻底搞懂捕获、冒泡与事件委托
前端·javascript·react.js
AI人工智能_电脑小能手4 小时前
【大白话说Java面试题 第125题】【并发篇】第25题:说说 Java 线程的中断机制
java·后端·面试
candyTong5 小时前
阿里开源 AI Code Review 工具:ocr review 的执行链路解析
javascript·后端·架构
铁皮饭盒5 小时前
TypeBox 比 Zod.js 校验 快10倍, 还兼容AI 工具调用, 他做对了什么?
前端·javascript·后端
To_OC14 小时前
从一次栈溢出报错说起,我把递归彻底扒明白了
javascript·算法·程序员
kyriewen16 小时前
面试官问你:“AI 能写 80% 的代码了,公司为什么还需要你?”
前端·javascript·面试
冬奇Lab17 小时前
每日一个开源项目(第141篇):hiring-agent - HackerRank 开源了他们的简历评分系统,你的简历能得几分?
人工智能·面试·开源