涨薪面技:如何写 webpack 路径解析插件(1)

一、前文回顾

上文主要讨论了 NormalModuleFactory 类型在模块的创建过程中的主要作用,该类型上的 NMF.prototype.create 方法用于创建模块,其核心原理如下:

  1. 触发 nmf.hooks.beforeResolve 接着触发 nmf.hooks.factorize 钩子;
  2. 在 nmf 实例创建的过程中会订阅 nmf.hooks.factorize 钩子,主要做了以下工作:
    • 2.2 触发 nmf.hooks.resolve 完成 loader、normal 的解析工作;
    • 2.2 创建 NormalModule 实例并处理各种 dependencies;
    • 2.3 调用 callback 交付新建模块实例;

接着我们还讲述了 nmf.hooks.resolve 的工作流程,主要分为两部分:

  1. 解析 normal/pre/post 三种 laoder 的路径;
  2. 解析常规模块资源路径;
  3. 将解析所得的结果交付到 nmf.hooks.factory 当中的调用;

从前面的回顾的过程中我们可以得知,factory.create 的第一步就是 resolve,而 resovle 的目标就是 loader 和资源模块。

这一篇我们聊 resolve 前的准备工作。

二、resolve & resolver

resolve 的流程也是 webpack 的核心工作流程的重要一环,后面的几篇小作文我们将会围绕这个主题展开,其中包括 resolve 前的准备工作、resolve 的核心库 enahanced-resolve 的源码阅读。

resolve 译为"解析",resolver 为了区别 parser 我称 resolver 为"路径解析器",parser 叫解析器(源代码分析);

三、resolver

resolve 准备工作主要在 NormalModuleFactory 的构造函数执行时注册 nmf.hooks.resolve 钩子当中,前文只是介绍了她的主干流程,今天我们来具体分析其中的工作,但是这部分仅限于 resolve 的准备,不进入 resolve 的过程中。

3.1 创建 laoder Resolver

在 NMF 中,无论是 loader resolver 还是 normal resolver 都是通过调用 nmf.getResolver 方法完成;

js 复制代码
this.hooks.resolve.tapAsync(
    {
        name: "NormalModuleFactory",
        stage: 100
    },
    (data, callback) => {
        
        // 1.
        const loaderResolver = this.getResolver("loader");

以上就是 nmf 在 resolve 中先创建 loaderResolver 时调用的 nmf.getResolver 方法,下面我们来看 getResolver 方法;

3.2 nmf.getResolver 方法

该方法是 NMF 的 原型方法,代码如下:

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

这里面的逻辑很简单,将接收到的 参数 type 和 resolveOptions 传给 nmf.resolverFactory.get 方法即可获得一个 resolver;

3.2.1 参数

  1. type: 类型,在我们的项目里,只有两种类型 loader 和 normal,其中 laoder 是创建 loader 类型的解析器,normal 则是普通的路径解析器;
  2. resolveOptions:用于创建解析器时传递的参数,这就是 webpack.config.js.resolve 或者 resolveLoader 配置项(注意,该配置项在 WebpackOptionsApply.prototype.process 方法执行时通过订阅 resolverFactory.hooks.resoverOpitons.for 时合并),用于定制解析器的具体行为;

3.2.2 nmf.resolverFactory 哪儿来的?

nmf.resolverFactory 顾名思义,是创建解析器的工厂,这个值在创建 NormalModuleFactory 的实例过程中设定:

js 复制代码
class NormalModuleFactory extends NormalModule {
    // ...
    constructor ({ resolverFactory }) {
        this.resolverFactory = resolverFactory
    }
}

可见这玩意儿是传进来的,你还记得是哪里传进来的吗?

当然是实例化 NMF 的时候,这个过程在创建 compiler 实例时传递的给 Compiler 构造函数的,而 NMF 实例作为 compilation 对象实例化的参数创建的。

  • 创建 compiler 实例:
js 复制代码
const ResolverFactory = require("./ResolverFactory");
class Compiler {
    constructor () {
        this.resovlerFactory = new ResolverFactory()
    }
}
  • 创建 compilation 实例:
js 复制代码
class Compiler {
    createNormalModuleFactory() {  
        const normalModuleFactory = new NormalModuleFactory({  
            resolverFactory: this.resolverFactory // 创建 nmf 实例的时候传入的 ResolverFactory 
        });  
        
        return normalModuleFactory;  
    }
    
    newCompilationParams() {  
        const params = {  
            normalModuleFactory: this.createNormalModuleFactory() // ResolverFactory 实例
        };  
        return params;  
    }
    
    compile(callback) {  
        // 
        const params = this.newCompilationParams();  
       
        // 治理就传给 compilation 了
        const compilation = this.newCompilation(params);
    }
}

上面是 nmf.resolverFactory 的由来;

从 compiler 的代码处可以看到 resolverFactory 来自 ResolverFactory 模块,下面我们来看看这个模块。

四、ResolverFactory

  • 该模块位于:webpack/lib/ResolverFactory.js,简化后的大致结构如下:
js 复制代码
module.exports = class ResolverFactory {  
    constructor() {  
        this.hooks = Object.freeze({  
           resolveOptions: new HookMap(() => new SyncWaterfallHook(["resolveOptions"])),  
           resolver: new HookMap(() => new SyncHook(["resolver", "resolveOptions","userResolveOptions"]))  
        });  
        
        this.cache = new Map();  
    }
    

    get(type, resolveOptions = EMPTY_RESOLVE_OPTIONS) {
       // get 方法
    }
    
    _create(type, resolveOptionsWithDepType) {
        // 创建 resolver 实例
    }
}

4.1 构造函数

构造函数无参数,大致逻辑如下:

  1. 声明 resolver.hooks,值得注意的是,resolver.hooks 并非是 Hook 类型,而是 HookMap 类型;其中包含两个钩子:

    • 1.1 resolverOptions:这个钩子为配置 resolverOptions 设置,通过订阅这个钩子,可以定制 resolver 的行为;
    • 1.2 resolver:这个钩子可以用于修改 resolver 的一些默认行为,比如在 webpack 中的 ResolverCachePlugin.js 这个插件中,就是通过修改 HookMap.intercept 方式拦截 resolver,在 resolver 解析的过程中加入优先取用缓存的逻辑;
  2. 定义 resolver.cache 对象,这个对象在下面的 get 方法中有用到,对于相同配置的 resolver 创建诉求,resolver 内部返回之前创建过的 resolver 对象。

4.2 get 方法

get 方法用于获取 resolver 对象,也是前面 NMF.prototype.getResolver 内部核心实现的原理。下面我们看看这个方法:

以下是整个核心的实现代码:

js 复制代码
module.exports = class ResolverFactory {  
    get(type, resolveOptions = EMPTY_RESOLVE_OPTIONS) {
        // 1.
        let typedCaches = this.cache.get(type);
        if (!typedCaches) {
            typedCaches = {
                direct: new WeakMap(),
                stringified: new Map()
            };
            this.cache.set(type, typedCaches);
        }
        
        // 2.
        const cachedResolver = typedCaches.direct.get(resolveOptions);
        if (cachedResolver) {
            return cachedResolver;
        }
        
        // 3.
        const ident = JSON.stringify(resolveOptions);
       
        
        // 4.
         const resolver = typedCaches.stringified.get(ident);
        if (resolver) {
            typedCaches.direct.set(resolveOptions, resolver);
            return resolver;
        }
        
        // 5.
        const newResolver = this._create(type, resolveOptions);
        
        // 6.
        typedCaches.direct.set(resolveOptions, newResolver);
        typedCaches.stringified.set(ident, newResolver);
        
        // 7.
        return newResolver;
    }
}

4.2.1参数

  1. type:要获取的 resolver 类型,在我们的项目中,type 是 loader 或者 normal,分别对应 loader 的路径解析器和资源模块解析器;
  2. resolveOptions:resolve 的选项对象,来自 webpack.config.js,用户可以通过添加配置定制的解析器行为,不传递时为 EMPTY_RESOLVE_OPTIONS;

4.2.2 逻辑

整个逻辑分为了 7 个步骤:

  1. 根据传入的 type 取用对应 type 对应的缓存,保存到 typedCaches 变量; 如果 typedCaches 为 falsy 值,则声明 typedCaches 对象,包含 direct 属性,值类型为 WeakMap,另外包含 stringify 属性,值为 Map 类型;声明结束后,以 type 为 key 更新到 resolver.cache 对象;
  2. 先尝试在 typedCaches.direct 缓存中查找当前传入的 resolveOptions 对应的 resolver,若缓存中有即 cachedResolver 不为 falsy 值,则直接返回该缓存的解析器;
  3. 到这里相当于 typedCaches.direct 缓存中不存在,此时序列化 resolveOptions 得到标识符 ident
  4. 然后以 ident 为 key 查找 typedCaches.stringfiy 缓存是否存在,结果保存在 resolver 常量上,若存在则返回 resolver;
  5. 此时,到这里说明 typedCaches.direct 和 typedCaches.stringify 缓存中都不存在当前 type 及 resolveOption 对应的 reosolver 对象;此时调用 this._create() 并传入 type 和 resolveOption 重新创建 resover,结果保存到 newReoslver 常量;
  6. 新建完成后,更新 typedCaches.direct 和 typedCaches.stringify 缓存对象上,下次再创建相同 type 和 resolveOptions 的解析器时可直接相应缓存内容;
  7. 将 5. 中新建的 newResolver 返回;

4.3 _create 方法

js 复制代码
module.exports = class ResolverFactory {  
    _create(type, resolveOptionsWithDepType) {
        // 1.
        const originalResolveOptions = { ...resolveOptionsWithDepType };
        // 2.
        const resolveOptions = convertToResolveOptions(
            this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType)
        );
        
        // 3.
        const resolver = Factory.createResolver(resolveOptions);
       
        
        // 4.
        resolver.withOptions = options => {};
        
        // 5.
        this.hooks.resolver
                .for(type)
                .call(resolver, resolveOptions, originalResolveOptions);
        // 6.
        return resolver;
    }
}

4.3.1 参数

该方法有两个参数:

  1. type:resolver 类型,这里不过多展开;
  2. resolverOptionsWithType:其实就上前文传入的 resolveOptions 对象;

4.3.2 逻辑

  1. 缓存 resolverOptionsWithType 到 originalResolveOptions 常量;

  2. 触发 resolverFactory.hooks.resolveOptions.for(type) 对应的钩子,获取创建当前 type 对应 resolver 所需的配置项目,最后进行智能合并。不知道你们是否还记得,这些是在哪里配置的?是的前面提到过 WebpackOptionsApply 这个模块中:

js 复制代码
class WebpackOptionsApply {

    process () {
        // 这里做的这个工作
    }
}
  1. 调用 Factory.createResolver 创建 resolver 解析器,Factory 来自 eanhanced-resolve 库,后文我们展开讲这个库的源码;

  2. 声明 resolver.withOptions 方法;

  3. 触发 resolverFactory.hooks.resolver 钩子,该钩子用于修改 resolver 的默认行为,前面提到过有个插件 ResovlerCachePlugin 就利用了这个钩子;

  4. 返回新建的 resolver;

五、总结

本文详细讲述了 webpack 中的 resolver 的由来及作用:

  1. webpack resolver 用于解析webpack 构建过程中的 request 对应的 模块及模块要对应的 loader 的资源路径用的,分为 normal resolver 和 loader resolver 两种类型;

  2. NMF.prototype.getResolver 负责获取 resolver,核心是通过 ResolverFactory.prototype.get 方法实现;

  3. 接着我们讨论了 ResolverFactory 类型的构造函数,重点在于声明钩子:

    • 3.1 resolveOptions: 用于修改定制 resolver 行为的配置对象;
    • 3.2 resolver:用于修改 resolver 默认行为;
  4. 详细讨论了 ResolverFactory.prototype.get 方法,内部会优先取用 typedCaches.direct/stringify 缓存,有则返回,没有则调用它 ResolverFactory.prototype._create 方法创建;

  5. ResolverFactory.prototype._create 方法内部主要是触发在 WebpackOptionsApply 中配置的 对应 type 的 resolver 选项对象,然后调用 ehanced-resolve 库提供的 Factory.create 方法创建 resolver;接着触发 resolverFactory.hooks.resolver 修改 resolver 的行为并且举例 ResolverCachePlugin;

以上是有关 webpack 中的 Resolver 相关的基础知识,后面我们会展开多次提到的 ResolverCachePlugin 插件以及 enhanced-resolve 这个库的源码;

相关推荐
醉の虾12 分钟前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧21 分钟前
TypeScript 的发展与基本语法
前端·javascript·typescript
疯狂的沙粒1 小时前
对 TypeScript 中高级类型的理解?应该在哪些方面可以更好的使用!
前端·javascript·typescript
ZOMI酱2 小时前
【AI系统】GPU 架构与 CUDA 关系
人工智能·架构
旭日猎鹰2 小时前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
J老熊2 小时前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程
猿java3 小时前
什么是 Hystrix?它的工作原理是什么?
java·微服务·面试
一条晒干的咸魚3 小时前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
花海少爷3 小时前
第十章 JavaScript的应用课后习题
开发语言·javascript·ecmascript
sinat_384241093 小时前
在有网络连接的机器上打包 electron 及其依赖项,在没有网络连接的机器上安装这些离线包
javascript·arcgis·electron