webpac5 模块创建基类 NormalModuleFactory,拿走不谢

一、 前文回顾

webpack 通过 AsyncQueue 实现异步并发队列,整体核心有以下几点:

  1. 创建队列实例时通过 parent 构建亲代关系队列;
  2. 当向任一队列中添加成员时都会触发 _root._ensureProcessing 方法从祖先队列开始并发消耗队列;
  3. 启用消耗之后,各个队列的 _processor 方法就会处理队列成员数据;
  4. _processor 处理队列成员数据得到结果后传递给 _handleResult 方法,该方法把结果传给 callback,同时维护并发数量递减及恢复并发消耗队列的工作;

前面几篇有关模块创建的过程都是宏观层面的,今天我们正式进入模块创建具体实现过程。

二、模块工厂(factory)

在 webpack 中模块是由对应的 ModuleFactory 即模块工厂创建所得,在前面创建 Compilation 实例的过程中提到过这个概念,当时有两个 ModuleFactory:

  1. NormalModuleFactory:常规的 JS 模块工厂
  2. ContextModuleFactory:上下文模块

这里以我们的示例项目中的代码模块为例,普通的 js 对应的则是 NormalModuleFactory;

2.1 factory.create() 调用

结合前面的文章,我们模块调用逻辑至于 compilation.factorizeQueue 的 processor 函数中:

js 复制代码
class Compilation {
    _factorizeModule({ factory, /* .... */ }, callback) { 
        // 调用 factory.create 方法
        factory.create(  
            {  
                contextInfo: {  
                issuer: originModule ? originModule.nameForCondition() : "",  
                issuerLayer: originModule ? originModule.layer : null,  
                compiler: this.compiler.name,  
                ...contextInfo  
            },  
            resolveOptions: originModule ? originModule.resolveOptions : undefined,  
            context: context  
                ? context  
                : originModule  
                    ? originModule.context  
                    : this.compiler.context,  
            dependencies: dependencies  
       },  
        (err, result) => { /* 处理模块创建的结果 */ });  
    }
}

我们已经知道了上面的 factory 是 NormalModuleFactory 的实例,所以 factory.create 自然就是 NormalModuleFactory.prototype.create 方法

2.2 NormalModuleFactory.prototype.create

该方法位于 NormalModuleFactory.js 模块,该模块用于创建模块工厂实例,注意不是模块实例,是模块工厂(下称 NMF)!

NMF 的实例是在创建 Compilation 实例之前实例化的,这个过程不在赘述,如果没有印象的话,建议复习前面的章节。

文件位置:webpack/lib/NormalModuleFactory.js

现在我们看看 NMF.prototype.create 方法的大致结构,经过简化后的代码如下:

js 复制代码
class NormalModuleFactory extends ModuleFactory {
    create(data, callback) {
        // 1.
        const resolveData = {
            contextInfo,
            resolveOptions,
            context,
            request,
            assertions,
            dependencies,
            dependencyType,
            fileDependencies,
            missingDependencies,
            contextDependencies,
            createData: {},
            cacheable: true
        };
        
        // 2.
        this.hooks.beforeResolve.callAsync(resolveData, (err, result) => {

            // 3.
            this.hooks.factorize.callAsync(resolveData, (err, module) => {
                
                // 4.
                const factoryResult = {
                    module,
                    fileDependencies,
                    missingDependencies,
                    contextDependencies,
                    cacheable: resolveData.cacheable
                };

                // 5.
                callback(null, factoryResult);
            });
        });
	}
}

2.2.1 参数

  1. data:创建模块所需的信息,包括 contextInfo、dependencies及 resolverOptions(创建解析器 Resolver 需要) 等信息,当然这些数据都是前面的环节推入到 factorizeQueue 队列中的;
js 复制代码
factory.creat({  
    contextInfo: {  
        issuer: originModule ? originModule.nameForCondition() : "",  
        issuerLayer: originModule ? originModule.layer : null,  
        compiler: this.compiler.name,  
        ...contextInfo  
    },  
    resolveOptions: originModule ? originModule.resolveOptions : undefined,  
    context: context  
        ? context  
        : originModule  
            ? originModule.context  
            : this.compiler.context,  
    dependencies: dependencies  
},
  1. callback:受理模块创建后事宜的回调函数,这里暂时不展开这个部分

2.2.2 逻辑

create 方法主要做了以下工作:

  1. 组织 resolveData 对象,这其中包含了 context、resolveOptions及待解析的 request 等信息;
  2. 触发 nmf.hooks.beforeResolve 钩子并传入 resolveData 对象,这个钩子可以用于修改 resolveData 中的信息;
  3. 在 beforeResolve 的回调中触发 nmf.hooks.factorize 钩子,在创建 nmf 实例的时候该钩子订阅了回调, 该钩子就是创建模块的信号;
  4. 在 nmf.hooks.factorize 钩子的回调中,组织 factoryResult 对象,其中包含了新创建的 module 及 fileDependencies、 missingDependencies、 contextDependencies 收集到的三种依赖;
  5. 调用 callback 并传入 factoryResult 对象,结束当前模块的创建流程;

从上面的流程可知,其中重要的环节在 nmf.hooks.factorize 这个钩子,现在我们看看这个钩子从哪里注册的,以及这个钩子中都干了什么;

三、NormalModuleFactory 构造函数

NormalModuleFactory 用于创建模块工厂实例,而模块工厂用于创建模块;以下为精简后的代码,我把重要的环节大致分为 6 个步骤:

js 复制代码
class NormalModuleFactory extends ModuleFactory {

    constructor({
        context,
        fs,
        resolverFactory,
        options,
        associatedObjectForCache,
        layers = false
    }) {
        super();
        
        // 1. 
        this.hooks = Object.freeze({
         
            resolve: new AsyncSeriesBailHook(["resolveData"]),

            factorize: new AsyncSeriesBailHook(["resolveData"]),

            beforeResolve: new AsyncSeriesBailHook(["resolveData"]),
            
            // ....

        });
        
        // 2.
        this.resolverFactory = resolverFactory;
        
        // 3.
        this.ruleSet = ruleSetCompiler.compile([
            {
                rules: options.defaultRules
            },
            {
                rules: options.rules
            }
        ]);
     
        // 4.
        this.hooks.factorize.tapAsync(
            {
                    name: "NormalModuleFactory",
                    stage: 100
            },
            (resolveData, callback) => {
               
            }
        );

        // 6.
        this.hooks.resolve.tapAsync(
            {
                name: "NormalModuleFactory",
                stage: 100
            },
            (data, callback) => {
              
            }
        );
    }
}
  1. 声明 nmf 的 hooks 对象,这其中包含了上面我们提到的 beforeResolve、factorize、resolve 等钩子;
  2. 缓存 resolverFactory,resolverFactory 是创建 resolver (路径解析器)的工厂,后面将会有相当的篇幅介绍 resolver 相关内容;
  3. 初始化 nmf.ruleSet,ruleSet 是一个新概念,但是他的前身大家都熟悉------loader 和 应用 loader 的规则,即告知当前模块要使用哪些loader;ruleSet 是 经由 webpack 格式化之后的标准对象,后面模块构建(module.build)的过程我们再展开;
  4. 订阅 nmf.hook.factorize 钩子,并注册回调,回调的逻辑我们下面单独开一个标题展开讲;
  5. 订阅 nmf.hooks.resolve 钩子,并注册回调,这个回调我们同样开一个小主题讲;

3.1 nmf.hook.factorize 回调

以下是 nfm 实例化过程中订阅 nmf.hooks.factorize 的回调函数,可以直接说所谓 NormalModuleFactory 的 factory(工厂)就是这个钩子的回调函数了,这个回调函数就是创建模块的工厂;

以下是经过简化的代码:

js 复制代码
(resolveData, callback) => {
    // 1.
    this.hooks.resolve.callAsync(resolveData, (err, result) => {
        // 2.
        this.hooks.afterResolve.callAsync(resolveData, (err, result) => {
            
            // 3.
            const createData = resolveData.createData;
            this.hooks.createModule.callAsync(
                createData,
                resolveData,
                (err, createdModule) => {
                    if (!createdModule) {
                        // 4.
                        createdModule = new NormalModule(createData));
                    }
                    
                    // 5.
                    createdModule = this.hooks.module.call(
                        createdModule,
                        createData,
                        resolveData
                    );
                    
                    // 6. 
                    return callback(null, createdModule);
                }
            );
        });
    });
}

我们将这个回到分成 6 个步骤:

  1. 触发 nmf.hooks.resolve 钩子,传入 resolveData 作为参数,该参数用于解析 request 获取 request 对应的路径相关信息;
  2. 触发 nmf.hooks.afterResolve 钩子,传入 resolveData;
  3. 触发 nmf.hooks.createModule 钩子,传入 createData 和 resolveData;
  4. 创建 NormalModule 实例得到模块对象,这个就是我们心心念念的 模块(module) 对象;
  5. 触发 nmf.hooks.module 钩子,传入刚刚创建的模块对象;
  6. 调用 callback,将新建模块传递给模块的受理函数继续后续流程;

3.2 nmf.hooks.resolve 回调

注册在 nmf.hooks.resolve 的回调主要用于解析当前 request 对应的模块以及该模块需要应用的 loader 的路径。

以下是经过简化后的代码:

js 复制代码
(data, callback) => {
   
    // 1.
    const loaderResolver = this.getResolver("loader");

    const resolveContext = {
        fileDependencies,
        missingDependencies,
        contextDependencies
    };
    
    // 2.
    this.resolveRequestArray(
        contextInfo,
        this.context,
        useLoadersPost,
        loaderResolver,
        resolveContext,
        (err, result) => {
            postLoaders = result;
            continueCallback(err);
        }
    );
    
    // 3. 
    this.resolveRequestArray(
        contextInfo,
        this.context,
        useLoaders,
        loaderResolver,
        resolveContext,
        (err, result) => {
            normalLoaders = result;
            continueCallback(err);
        }
    );
    
    // 4.
    this.resolveRequestArray(
        contextInfo,
        this.context,
        useLoadersPre,
        loaderResolver,
        resolveContext,
        (err, result) => {
            preLoaders = result;
            continueCallback(err);
        });
    });
    
    
    // 5.
    const defaultResolve = context => {
        // 7.
        const normalResolver = this.getResolver(
            "normal",
            dependencyType
                ? cachedSetProperty(
                    resolveOptions || EMPTY_RESOLVE_OPTIONS,
                    "dependencyType",
                    dependencyType
                )
                : resolveOptions
        );
        
        // 8.
        this.resolveResource(
            contextInfo,
            context,
            unresolvedResource,
            normalResolver,
            resolveContext,
            (err, resolvedResource, resolvedResourceResolveData) => {
                //
            }
        );        
    };
    
    // 6.
    defaultResolve(context);
}

我们把这个过程分为了 9 个步骤:

  1. 调用 nmf.getResolver 获取 loader Resolver;
  2. 调用 nmf.resolveRequestArray 解析 postLoader;
  3. 调用 nmf.resolveRequestArray 解析 normalLoader;
  4. 调用 nmf.resolveRequestArray 解析 preLoader;
  5. 声明 defaultResolve 方法,该方法用于解析资源路径;
  6. 调用 defaultResolve 方法,启动 request 对应的资源路径解析
  7. 调用 nmf.getResolver 方法创建 normal resolver;
  8. 调用 nmf.resolveResource 解析资源路径;

四、总结

本文主要讨论了 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 当中的调用;
相关推荐
Dread_lxy2 分钟前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
用户3157476081351 小时前
成为程序员的必经之路” Git “,你学会了吗?
面试·github·全栈
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与1 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
前端郭德纲1 小时前
浏览器是加载ES6模块的?
javascript·算法
JerryXZR1 小时前
JavaScript核心编程 - 原型链 作用域 与 执行上下文
开发语言·javascript·原型模式
帅帅哥的兜兜1 小时前
CSS:导航栏三角箭头
javascript·css3
渗透测试老鸟-九青2 小时前
通过投毒Bingbot索引挖掘必应中的存储型XSS
服务器·前端·javascript·安全·web安全·缓存·xss
布川ku子2 小时前
[2024最新] java八股文实用版(附带原理)---Mysql篇
java·mysql·面试
龙猫蓝图2 小时前
vue el-date-picker 日期选择器禁用失效问题
前端·javascript·vue.js