likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 6

一、前文回顾

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

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

二、enhanced-resolve 与构建流程

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

2.1 getResolver

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

class NormalModuleFactory {
  // ....
  getResolver(type, resolveOptions) {
    return this.resolverFactory.get(type, resolveOptions);
  }
}

2.2 webpack/lib/ResolverFactory.get

该方法来自 webpack/lib/ResolverFactory.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;