面试官: Compilation 实例化的时做了什么?

一、前文回顾

上文对 Compiler.prototype.compile 方法进行了由浅入深的解析,compile 则是 compiler 对象真正处理编译工作的地方,因此 compiler.compile 方法的内部就是整个编译过程的调度图。compiler.compile 方法主要做了以下工作:

  1. 调用 compiler.newCompilationParams() 方法为创建 compilation 实例准参数;
  2. compiler.hooks.beforeCompile 钩子;
  3. 触发 compiler.hooks.compile 钩子,参数同样传入的是第一步获取的 params 对象;
  4. 执行 compiler.newCompilation(params) 创建 compilation 实例;
  5. 传入 compilation 对象触发 compiler.hooks.make 钩子;
  6. 接着介绍了订阅在 compiler.hooks.make 上的 EntryPlugin,同时介绍了 EntryPlugin 为整个构建投料的重要意义。

从这一篇开始,以后相当一段时间的重点将会转移到 compilation 对象上。具体的工作将要展开了,比如模块的编译、执行 loader,创建模块图等等。

今天我们先来了解一下 compilation.addEntry 及 compilation 实例对象的创建过程;

二、compilation 对象

compilation 对象实例化自 Compilation 类型。实例化的调用是由前面的 compiler.newCompilation() 方法完成。

compilation 对象则是 webpack 构建编译具体过程的实施者,说白了就是干活儿的了。

2.1 Compilation 类型

  1. 定义位置:webpack/lib/Compilation.js;

  2. 构造函数参数:

    • 2.1 compiler: Compiler 类型实例,创建当前 compilation 的 compiler 对象;
    • 2.2 params: 创建 compilation 所需的参数对象,当然,这里我们是很清楚的,这里面包含的是 normalModuleFactory 和 contextModuelFactory 这两个类型模块的工厂;
  3. 关键方法:

    • 3.1 addEntry:添加入口
    • 3.2 seal: 优化 module、chunk
    • 3.3 ....

2.2 构造函数

构造函数中的内容很多,初始化了相当多的属性,有很多暂时用不到,这里我们就先忽略它,我们先来看看整体的结构,主要包括了以下内容的初始化:

  1. compilation.hooks,注册 compilation 的 hooks 对象;
  2. 初始化 compilation.mainTemplate 属性;
  3. 初始化 compilation.chunkTemplate 属性;
  4. 初始 compilation.runtimeTemplate 属性;
  5. 初始化 compilation.processDependenciesQueue;
  6. 初始化 compilation.addModuleQueue;
  7. 初始化 compilation.factorizeQueue;
  8. 初始化 compilation.buildQueue;

有关各个属性的作用,我们下面用小标题进行单独讲解!

js 复制代码
class Compilation {
    constructor(compiler, params) {
        // ....
        this.hooks = Object.freeze({ /**/ })
        
        this.mainTemplate = new MainTemplate(this.outputOptions, this);  
        this.chunkTemplate = new ChunkTemplate(this.outputOptions, this);  
        this.runtimeTemplate = new RuntimeTemplate(  
            this,  
            this.outputOptions,  
            this.requestShortener  
        );
        
        this.moduleTemplates = {  
            javascript: new ModuleTemplate(this.runtimeTemplate, this)  
        };
        
        this.processDependenciesQueue = new AsyncQueue({
            name: "processDependencies",
            parallelism: options.parallelism || 100,
            processor: this._processModuleDependencies.bind(this)
        });
        this.addModuleQueue = new AsyncQueue({
            name: "addModule",
            parent: this.processDependenciesQueue,
            getKey: module => module.identifier(),
            processor: this._addModule.bind(this)
        });
        this.factorizeQueue = new AsyncQueue({
            name: "factorize",
            parent: this.addModuleQueue,
            processor: this._factorizeModule.bind(this)
        });
        this.buildQueue = new AsyncQueue({
            name: "build",
            parent: this.factorizeQueue,
            processor: this._buildModule.bind(this)
        });

    }

    getCache(name) {
        return this.compiler.getCache(name);
    }


    addModule(module, callback) {
        this.addModuleQueue.add(module, callback);
    }

    _addModule(module, callback) {}


    buildModule(module, callback) {
       this.buildQueue.add(module, callback);
    }

    _buildModule(module, callback) {}

    processModuleDependencies(module, callback) {}


    handleModuleCreation(
    {
        factory,
        dependencies,
        originModule,
        contextInfo,
        context,
        recursive = true,
        connectOrigin = recursive
    },
    callback
    ) {}

    _handleModuleBuildAndDependencies(originModule, module, recursive, callback) {
        this.processDependenciesQueue.add(module, callback);
    }

    _factorizeModule(
    {
        currentProfile,
        factory,
        dependencies,
        originModule,
        factoryResult,
        contextInfo,
        context
    },
    callback
    ) {}


    addModuleChain(context, dependency, callback) {
        return this.addModuleTree({ context, dependency }, callback);
    }


    addModuleTree({ context, dependency, contextInfo }, callback) {    }


    addEntry(context, entry, optionsOrName, callback) {
        // TODO webpack 6 remove
        const options =
            typeof optionsOrName === "object"
                    ? optionsOrName
                    : { name: optionsOrName };

        this._addEntryItem(context, entry, "dependencies", options, callback);
    }


    _addEntryItem(context, entry, target, options, callback) {}

    finish(callback) {}

    seal(callback) {}


    createModuleHashes() {}

    emitAsset(file, source, assetInfo = {}) {}

}

2.2.1 this.hooks

这里声明的是 compilation 的 hooks 对象,前面我们介绍过 compiler 的钩子注册,当时介绍 compiler介 绍的的很详细。但是这里有个大概的印象就行,compilation 的钩子贯穿了构建的全过程,即从 module 到 chunk 的全过程。

这个 hooks 包含了 addEntry、finishMoudle、optimize、optimizeModule、seal、unseal、 beforeChunk、afterChunk 等钩子;

2.2.2 this.mainTemplate

ompilation.mainTemplate 是用于渲染入口 Chunk 的模板。compilation.mainTemplate 的作用是根据 Chunk 生成最终文件,主要有三个步骤:

  1. 模板 hash 更新;
  2. 模板渲染 chunk
  3. 生成文件;

2.2.3 this.chunkTemplate

ompilation.ChunkTemplate 用于动态引入的非入口 Chunk 的渲染模板。

webpack 构建过程中会将输入的 模块 分割成多个代码块 Chunks。每个 chunk 都是一个独立块,包含了相关的模块代码和运行时代码(或部分)。compilation.ChunkTemplate 提供了一种方式来定义或修改代码块的生成相关逻辑;

2.2.4 this.runtimeTemplate

在 webpack 中,compilation.runtimeTemplate 用于生成运行时模板。运行时模板是用于生成最终输出文件的一部分。

所谓 webpack 运行时就是大家口中的 runtime 代码,就是 webpack 内部实现的模块加载、导出、异步加载等用于组织 webpack 构建所得的bundle 和 chunk 正确执行的代码;以下是部分大家常用的 runtime 代码:

  1. webpack_require 方法 webpack 运行时的主要方法,其作用创建并缓存 module 对象()后,执行这个 module 中的代码;
js 复制代码
function __webpack_require__(moduleId) {}
  1. webpack_require.e 方法 用于加载额外 chunk 的函数,比如按需加载的 chunk,这个里面就会有创建 script 标签然后去加载代码的具体逻辑;
js 复制代码
__webpack_require__.e = function requireEnsure(chunkId) {};
  1. webpack_require.m 属性 暴露这个 runtime 接收到的 modules 对象;
js 复制代码
__webpack_require__.m = modules;
  1. webpack_require.d 态方法 在模块对象(module 上面__webpack_require__ 中创建的 module 对象,下同)的 exports 对象上增加属性,以 getter 的形式定义导出(就是实现你代码中的通过 export 导出一个变量/常量/函数等)
js 复制代码
__webpack_require__.d = function(exports, name, getter) {};
  1. webpack_require.r 静态方法 在模块对象(module) 增加 __esModule 属性,用于标识这个模块是个 ES6 模块
js 复制代码
__webpack_require__.r = function(exports) {};

更多内容可以看我之前写过的一篇文章 webpack 的输出文件竟然这么妙!

2.2.5 this.moduleTemplate

webpack 中,ModuleTemplate 是用于渲染 chunk 中的 module 的模板。它与 MainTemplateChunkTemplate 联用,在实例化 Compilation 对象时被创建,分别对应入口 chunk、动态引入的非入口 chunk 及 chunk 中的 module 的渲染模板。

2.2.5 模块构建四队列

webpack 的模块创建、依赖模块的解析创建及编译工作依靠以下四个队列实现。相较于上一代 webpack ,webpack v5 最大的差别在递归的实现,在 v5 中使用异步队列实现的递归解析。而上一代则使用回调组织的一部递归过程。

下面我们先来看看这些队列的作用:

1)this.processDependenciesQueue

这是 webpack 在构建中使用的一个异步队列,实例化自 AsyncQueue:

js 复制代码
this.processDependenciesQueue = new AsyncQueue({
    name: "processDependencies",
    parallelism: options.parallelism || 100,
    processor: this._processModuleDependencies.bind(this)
});

该队列是 webpack 处理模块创建及依赖解析的四个队列的最顶层队列,也就是说无论后面的 factorizeQueue、addModuleQueue、buildQueue 哪个队列消耗时都会触发 processDependenciesQueue 工作。这一点请你牢记,这是理解这个递归的关键信息!

该队列的 processor 为 this._processModuleDependencies 方法。接收 factorizeQueue 的处理结果,将 factorize 创建的模块加入到队列,随即被 processDependenciesQueue 队里的 processor _processModuleDependencies 消耗,用于处理构建中当前模块的依赖模块的构建。

2) this.addModuleQueue
js 复制代码
this.addModuleQueue = new AsyncQueue({
    name: "addModule",
    parent: this.processDependenciesQueue,
    getKey: module => module.identifier(),
    processor: this._addModule.bind(this)
});

addModuleQueue 的 processor 为 this._addModule() 方法。

该 processor 方法的主要作用是根据 module.identifier 判断并尝试获取缓存,然后将 module 加入到 this.modules 和 this._modules 中。

接着去执行addModuleQueue 时传入的回调函数,建立 originModule.setResolvedModule、moduleGraph.setIssuerIfUnset 及其依赖的关系,也就是 compilation.moduleGraph。

最后将该模块的依赖加入到 this.processDependenciesQueue 中进行依赖解析;

3)this.factorizeQueue

factorizeQueue 工厂队列,其 processor 为 this._factorizeModule 方法,该方法内部调用 factory.create 也就是 NormalModuleFactory.create 方法创建模块实例;

js 复制代码
this.factorizeQueue = new AsyncQueue({
    name: "factorize",
    parent: this.addModuleQueue,
    processor: this._factorizeModule.bind(this)
});

该 processor 包含模块的创建工作全流程。

4)this.buildQueue

buildQueue 用于处理模块构建的队列,该队列的 processor 是 this._buildModule 方法,该方法内部主要用于调用各个 module.build 方法进行模块的构建工作。

并在完成构建后把构建所得内容加入到 this.processDependenciesQueue 队列中,这样也就完成了整个队列的递归过程;

js 复制代码
this.buildQueue = new AsyncQueue({
    name: "build",
    parent: this.factorizeQueue,
    processor: this._buildModule.bind(this)
});

三、总结

本文详述了 compilation 对象的创建过程,期间讨论了以下内容:

  1. Compilation 类型的信息,包括定义文件模块、参数等;
  2. Compilation 的构造函数的主要逻辑:
    • 2.1 compilation.hooks 定义;
    • 2.2 compilation.mainTemplate 的作用------生成主入口 chunk 的模板;
    • 2.3 compilation.chunkTemplate 的作用------生成非入口 chunk 的模板;
    • 2.4 compilation.runteimTemplate 的作用 ------ 生成 webpack 运行时相关代码(runtime)模板;
    • 2.5 compilation.moduleTemplate 的作用 ------ 生成 module 相关代码的模板;
    • 2.6 模块构建的四队列的作用,并简述了各个队列的 processor 函数的作用,另外大致讲述了这四个队列的协同关系;
相关推荐
神夜大侠1 小时前
VUE 实现公告无缝循环滚动
前端·javascript·vue.js
明辉光焱1 小时前
【Electron】Electron Forge如何支持Element plus?
前端·javascript·vue.js·electron·node.js
柯南二号2 小时前
HarmonyOS ArkTS 下拉列表组件
前端·javascript·数据库·harmonyos·arkts
wyy72932 小时前
v-html 富文本中图片使用element-ui image-viewer组件实现预览,并且阻止滚动条
前端·ui·html
前端郭德纲2 小时前
ES6的Iterator 和 for...of 循环
前端·ecmascript·es6
究极无敌暴龙战神X2 小时前
前端学习之ES6+
开发语言·javascript·ecmascript
王解2 小时前
【模块化大作战】Webpack如何搞定CommonJS与ES6混战(3)
前端·webpack·es6
欲游山河十万里2 小时前
(02)ES6教程——Map、Set、Reflect、Proxy、字符串、数值、对象、数组、函数
前端·ecmascript·es6
明辉光焱2 小时前
【ES6】ES6中,如何实现桥接模式?
前端·javascript·es6·桥接模式
PyAIGCMaster2 小时前
python环境中,敏感数据的存储与读取问题解决方案
服务器·前端·python