关于前端:Webpack5源码seal阶段分析三生成代码runtime

4次阅读

共计 56681 个字符,预计需要花费 142 分钟才能阅读完成。

本文内容基于 webpack 5.74.0 版本进行剖析

因为 webpack5 整体代码过于简单,为了缩小复杂度,本文所有剖析将只基于 js 文件类型进行剖析,不会对其它类型(cssimage)进行剖析,所举的例子也都是基于 js 类型
为了减少可读性,会对源码进行删减、调整程序、扭转的操作,文中所有源码均可视作为伪代码

前言

本文是 webpack5 外围流程 解析的最初一篇文章,共有 5 篇,应用流程图的模式从头到尾剖析了webpack5 外围流程

  1. 「Webpack5 源码」make 阶段(流程图)剖析
  2. 「Webpack5 源码」enhanced-resolve 门路解析库源码剖析
  3. 「Webpack5 源码」seal 阶段(流程图)剖析(一)
  4. 「Webpack5 源码」seal 阶段剖析(二)-SplitChunksPlugin 源码
  5. 「Webpack5 源码」seal 阶段剖析(三)- 生成代码 &runtime

在上一篇文章「Webpack5 源码」seal 阶段剖析 (二)-SplitChunksPlugin 源码中,咱们进行了hooks.optimizeChunks() 的相干逻辑剖析,选取了 SplitChunksPlugin 进行具体的剖析

在上篇文章完结剖析 SplitChunksPlugin 之后,咱们将开始 codeGeneration() 的相干逻辑剖析

源码整体概述

在上一篇文章中,咱们曾经剖析了 buildChunkGraph()hooks.optimizeChunks相干逻辑,当初咱们要开始剖析代码生成和文件打包输入相干的内容

如上面代码所示,咱们会执行:

  • codeGeneration():遍历 modules 数组,实现所有 module 代码转化,并将后果存储到 compilation.codeGenerationResults
  • createChunkAssets():合并 runtime 代码(包含立刻执行函数,多种工具函数)、modules代码、其它 chunk 相干的桥接代码,并调用 emitAsset()输入产物
seal() {
    //... 依据 entry 初始化 chunk 和 chunkGroup,关联 chunk 和 chunkGroup
    // ... 遍历 entry 所有的 dependencies,关联 chunk、dependencies、chunkGroup
    // 为 module 设置深度标记
    this.assignDepths(entryModules);
    buildChunkGraph(this, chunkGraphInit);

    this.createModuleHashes();
    this.codeGeneration(err => {this.createChunkAssets(err => {});
    });
}
function codeGeneration() {
    // ...... 解决 runtime 代码
    const jobs = [];
    for (const module of this.modules) {const runtimes = chunkGraph.getModuleRuntimes(module);
        for (const runtime of runtimes) {
            //... 省略 runtimes.size>0 的逻辑
            // 如果有多个 runtime, 则取第一个 runtime 的 hash
            const hash = chunkGraph.getModuleHash(module, runtime);
            jobs.push({module, hash, runtime, runtimes: [runtime] });
        }
    }
    this._runCodeGenerationJobs(jobs, callback);
}
function _runCodeGenerationJobs(jobs, callback) {
    //... 省略十分多的细节代码
    const {dependencyTemplates, ...} = this;
    //... 省略遍历 jobs 的代码,伪代码为:for(const job of jobs)
    const {module} = job;
    const {hash, runtime, runtimes} = job;
    this._codeGenerationModule({module, runtime, runtimes, ... , dependencyTemplates});
}
function createChunkAssets(callback) {
    asyncLib.forEachLimit(
        this.chunks,
        (chunk, callback) => {
            let manifest = this.getRenderManifest({
                chunk,
                ...
            });
            // manifest=this.hooks.renderManifest.call([], options);

            //... 遍历 manifest,调用(manifest[i]=fileManifest)fileManifest.render()
            asyncLib.forEach(
                manifest,
                (fileManifest, callback) => {
                    //....
                    source = fileManifest.render();
                    this.emitAsset(file, source, assetInfo);
                }
            );
        }
    )
}

codeGeneration():module 生成代码

1.1 整体流程图

联合 1.3.3 具体例子 的流程图一起看成果更佳

1.2 codeGeneration()

遍历 this.modules,而后获取对应的运行时runtimes,生成hash,将这三个值都放入到job 数组中,调用this._runCodeGenerationJobs()

_runCodeGenerationJobs() 中,遍历 jobs 数组,一直拿出对应的 job(蕴含 module、hash、runtime) 调用 _codeGenerationModule() 进行模块代码的生成

seal() {
    //... 依据 entry 初始化 chunk 和 chunkGroup,关联 chunk 和 chunkGroup
    // ... 遍历 entry 所有的 dependencies,关联 chunk、dependencies、chunkGroup
    // 为 module 设置深度标记
    this.assignDepths(entryModules);
    buildChunkGraph(this, chunkGraphInit);

    this.createModuleHashes();
    this.codeGeneration(err => {this.createChunkAssets(err => {});
    });
}
function codeGeneration() {
    // ...... 解决 runtime 代码
    const jobs = [];
    for (const module of this.modules) {const runtimes = chunkGraph.getModuleRuntimes(module);
        for (const runtime of runtimes) {
            //... 省略 runtimes.size>0 的逻辑
            // 如果有多个 runtime, 则取第一个 runtime 的 hash
            const hash = chunkGraph.getModuleHash(module, runtime);
            jobs.push({module, hash, runtime, runtimes: [runtime] });
        }
    }
    this._runCodeGenerationJobs(jobs, callback);
}
function _runCodeGenerationJobs(jobs, callback) {
    //... 省略十分多的细节代码
    const {dependencyTemplates, ...} = this;
    //... 省略遍历 jobs 的代码,伪代码为:for(const job of jobs)
    const {module} = job;
    const {hash, runtime, runtimes} = job;
    this._codeGenerationModule({module, runtime, runtimes, ... , dependencyTemplates});
}

1.3 _codeGenerationModule

从上面代码块能够晓得

  • 调用 module.codeGeneration() 进行代码的生成,将生成后果放入到result
  • 还会遍历 runtimes,生成运行时代码,放入到results
function _codeGenerationModule({module, runtime, runtimes, ... , dependencyTemplates}) {
    //... 省略十分多的细节代码
    this.codeGeneratedModules.add(module);
    result = module.codeGeneration({
        chunkGraph,
        moduleGraph,
        dependencyTemplates,
        runtimeTemplate,
        runtime,
        codeGenerationResults: results,
        compilation: this
    });
    for (const runtime of runtimes) {results.add(module, runtime, result);
    }
    callback(null, codeGenerated);
}

1.3.1 NormalModule.codeGeneration

// NormalModule.js
// 上面的办法对应下面的 module.codeGeneration 办法
function codeGeneration(..., dependencyTemplates, codeGenerationResults) {
    //... 省略十分多的细节代码
    const sources = new Map();
    for (const type of sourceTypes || chunkGraph.getModuleSourceTypes(this)) {const source = this.generator.generate(this, { ..., dependencyTemplates, codeGenerationResults});
        if (source) {sources.set(type, new CachedSource(source));
        }
    }
    const resultEntry = {
        sources,
        runtimeRequirements,
        data
    };
    return resultEntry;
}

由下面的代码能够晓得,最终触发的是 this.generator.generate,而这个generator 到底是什么呢?

make 阶段的 NormalModuleFactory.constructor-this.hooks.resolve 的代码能够晓得,之前就曾经生成对应的 parser 解决类以及 generator 解决类

会依据不同的 type,比方javascript/autocss 等不同类型的 NormalModule 进行不同 parsergenerator的初始化

getGenerator实质是触发 this.hooks.createGenerator,实际上是由各个Plugin 注册该 hooks 进行各种 generator 返回,也就是说对于不同的文件内容,会有不同的 genrator 解决类来进行代码的生成

// lib/NormalModuleFactory.js
const continueCallback = () => {
    // normalLoaders = this.resolveRequestArray 进行 resolve.resolve 的后果
    const allLoaders = postLoaders;
    if (matchResourceData === undefined) {for (const loader of loaders) allLoaders.push(loader);
        for (const loader of normalLoaders) allLoaders.push(loader);
    } else {for (const loader of normalLoaders) allLoaders.push(loader);
        for (const loader of loaders) allLoaders.push(loader);
    }
    for (const loader of preLoaders) allLoaders.push(loader);

    Object.assign(data.createData, {
        ...
        loaders: allLoaders,
        ...
        type,
        parser: this.getParser(type, settings.parser),
        parserOptions: settings.parser,
        generator: this.getGenerator(type, settings.generator),
        generatorOptions: settings.generator,
        resolveOptions
    });

    // 为了减少可读性,将外部函数提取到内部,上面的 callback 实际上是 this.hooks.resolve.tapAsync 注册的 callback()
    callback();}
getGenerator(type, generatorOptions = EMPTY_GENERATOR_OPTIONS) {if (generator === undefined) {generator = this.createGenerator(type, generatorOptions);
    cache.set(generatorOptions, generator);
  }

  return generator;
}
createGenerator(type, generatorOptions = {}) {
  const generator = this.hooks.createGenerator
    .for(type)
    .call(generatorOptions);
}

如上面截图所示,hooks.createGenerator会触发多个 Plugin 执行,返回不同的 return new xxxxxGenerator()

其中最常见的是 javascript 类型对应的 genrator 解决类:JavascriptGenerator

1.3.2 示例:JavascriptGenerator.generate

整体流程图

概述

从以下精简代码中能够发现,JavascriptGenerator.generate()执行的程序是:

  • new ReplaceSource(originalSource)初始化源码
  • this.sourceModule()进行依赖的遍历,一直执行对应的 template.apply(),将后果放入到initFragments.push(fragment)
  • 最初调用 InitFragment.addToSource(source, initFragments, generateContext) 合并 source 和 initFragments
generate(module, generateContext) {const originalSource = module.originalSource();
    const source = new ReplaceSource(originalSource);
    const initFragments = [];
    this.sourceModule(module, initFragments, source, generateContext);
    return InitFragment.addToSource(source, initFragments, generateContext);
}
this.sourceModule

遍历 module.dependenciesmodule.presentationalDependencies,而后调用 sourceDependency() 解决依赖文件的代码生成

如果存在异步依赖的模块,则应用 sourceBlock() 解决,实质就是递归一直调用 sourceDependency() 解决依赖文件的代码生成

sourceModule(module, initFragments, source, generateContext) {for (const dependency of module.dependencies) {this.sourceDependency();
    }
    for (const dependency of module.presentationalDependencies) {this.sourceDependency();
    }
    for (const childBlock of module.blocks) {this.sourceBlock()
    }
}
sourceBlock(module, block, initFragments, source, generateContext) {for (const dependency of block.dependencies) {this.sourceDependency();
    }
    for (const childBlock of block.blocks) {this.sourceBlock();
    }
}
sourceDependency(module, dependency, initFragments, source, generateContext) {
    const constructor = dependency.constructor;
    const template = generateContext.dependencyTemplates.get(constructor);
    template.apply(dependency, source, templateContext);
    const fragments = deprecatedGetInitFragments(
        template,
        dependency,
        templateContext
    );
    if (fragments) {for (const fragment of fragments) {initFragments.push(fragment);
        }
    }
}

在下面 sourceDependency() 的代码中,又呈现了依据不同类型实例化的对象 template,接下来咱们将剖析templatedependencyTemplates 到底是什么?

dependencyTemplates 跟 dependency 的提前绑定

通过提前绑定不同类型对应的 template,为 template.apply() 做筹备

在初始化 webpack 时,webpack.js中进行new WebpackOptionsApply().process(options, compiler)

WebpackOptionsApply.js 中,进行了 HarmonyModulesPlugin 的初始化

new HarmonyModulesPlugin({topLevelAwait: options.experiments.topLevelAwait}).apply(compiler);

HarmonyModulesPlugin.jsapply办法中,提前注册了多个 xxxxDependency 对应的 xxxx.Template() 的映射关系,比方上面代码中的

  • HarmonyCompatibilityDependency对应HarmonyCompatibilityDependency.Template()
// HarmonyModulesPlugin.js
apply(compiler) {
    compiler.hooks.compilation.tap(
        "HarmonyModulesPlugin",
        (compilation, { normalModuleFactory}) => {
            // 绑定 Dependency 跟 Dependency.Template 的关系
            compilation.dependencyTemplates.set(
                HarmonyCompatibilityDependency,
                new HarmonyCompatibilityDependency.Template());
            // 绑定 Dependency 跟 NormalModuleFactory 的关系
            compilation.dependencyFactories.set(
                HarmonyImportSideEffectDependency,
                normalModuleFactory
            );
            ...
        }
    );
}

为什么咱们要通过 xxxxDenependency 绑定对应的 xxxx.Template() 映射关系呢?

从上一篇文章「Webpack5 源码」make 整体流程浅析,咱们能够晓得,咱们在 make 阶段 会进行 AST 的解析,比方上一篇文章中呈现的具体例子,咱们会将呈现的 import {getC1} 解析为 HarmonyImportSideEffectDependency 依赖

前面咱们就能够依据这个依赖进行对应 Template 代码的生成

template.apply()

template.apply()的实现也是依据理论类型进行辨别的,比方 template.apply 可能是ConstDependency.Template.apply,也可能是HarmonyImportDependency.Template.apply

次要有两种模式:

  • 间接更改 source 源码
  • 应用 initFragments.push() 减少一些代码片段

上面取一些 xxxxxDependency 进行剖析,它会先拿到对应的Template,而后执行apply()

ConstDependency.Template.apply()

间接操作 source,间接扭转源码

if (typeof dep.range === "number") {source.insert(dep.range, dep.expression);
    return;
}
source.replace(dep.range[0], dep.range[1] - 1, dep.expression);
HarmonyImportDependency.Template.apply()

将代码增加到templateContext.initFragments

最终收集的 initFragments 数组会执行InitFragment.addToSource,在下一个阶段执行

HarmonyImportDependency.Template = class HarmonyImportDependencyTemplate extends (ModuleDependency.Template) {apply(dependency, source, templateContext) {const importStatement = dep.getImportStatement(false, templateContext);
        templateContext.initFragments.push(
            new ConditionalInitFragment(importStatement[0], // 同步
                InitFragment.STAGE_HARMONY_IMPORTS,
                dep.sourceOrder,
                key,
                runtimeCondition
            )
        );
        templateContext.initFragments.push(
            // await
            new AwaitDependenciesInitFragment(new Set([dep.getImportVar(templateContext.moduleGraph)])
            )
        );
        templateContext.initFragments.push(
            new ConditionalInitFragment(importStatement[1], // 异步
                InitFragment.STAGE_ASYNC_HARMONY_IMPORTS,
                dep.sourceOrder,
                key + "compat",
                runtimeCondition
            )
        );
    }
}
InitFragment.addToSource

通过十分繁冗的数据收集后,this.sourceModule()执行结束

回到 JavascriptGenerator.generate() 代码,在 this.sourceModule() 执行结束,会执行 InitFragment.addToSource() 逻辑,而这个 InitFragment 就是下面 template.apply() 所收集的数据

generate(module, generateContext) {const originalSource = module.originalSource();
    const source = new ReplaceSource(originalSource);
    const initFragments = [];
    this.sourceModule(module, initFragments, source, generateContext);
    return InitFragment.addToSource(source, initFragments, generateContext);
}

依照上面程序拼接最终生成代码concatSource

  • header局部代码:fragment.getContent()
  • middle局部代码: module文件理论的内容(可能通过一些革新)
  • bottom局部代码:fragment.getEndContent()

最终返回ConcatSource

static addToSource(source, initFragments, context) {
    // 排序
    const sortedFragments = initFragments
        .map(extractFragmentIndex)
        .sort(sortFragmentWithIndex);
        
    const keyedFragments = new Map();
    //... 省略依据 initFragments 拼接 keyedFragments 数据的逻辑
    const endContents = [];
    for (let fragment of keyedFragments.values()) {
        // add fragment Content
        concatSource.add(fragment.getContent(context));
        const endContent = fragment.getEndContent(context);
        if (endContent) {endContents.push(endContent);
        }
    }
    // add source
    concatSource.add(source);
    for (const content of endContents.reverse()) {
        // add fragment endContent
        concatSource.add(content);
    }
    return concatSource;

}

1.3.3 具体例子

_codeGenerationModule()流程波及的文件较为简单,上面应用一个具体例子进行整个流程再度解说,可能跟下面的内容会有所反复,然而为了可能真正明确整个流程,笔者认为肯定的反复是有必要的

咱们应用一个入口文件entry4.js,如上面代码块所示,有两个同步依赖,以及对应的调用语句,还有一个异步依赖chunkB

import {getG} from "./item/common_____g.js";
import voca from 'voca';
voca.kebabCase('goodbye blue sky'); // => 'goodbye-blue-sky'
import (/*webpackChunkName: "B"*/"./async/async_B.js").then(bModule=> {bModule.default();
});

console.info("getA2E", getG());
1.3.3.1 整体流程图

1.3.3.2 codeGeneration()

codeGeneration()会遍历所有 this.modules,而后通过一系列流程调用module.codeGeneration(),也就是NormalModule.codeGeneration() 生成代码放入 result

function codeGeneration() {
    // ...... 解决 runtime 代码
    const jobs = [];
    for (const module of this.modules) {const runtimes = chunkGraph.getModuleRuntimes(module);
        for (const runtime of runtimes) {
            //... 省略 runtimes.size>0 的逻辑
            // 如果有多个 runtime, 则取第一个 runtime 的 hash
            const hash = chunkGraph.getModuleHash(module, runtime);
            jobs.push({module, hash, runtime, runtimes: [runtime] });
        }
    }
    this._runCodeGenerationJobs(jobs, callback);
}
function _runCodeGenerationJobs(jobs, callback) {
    //... 省略十分多的细节代码
    const {dependencyTemplates, ...} = this;
    //... 省略遍历 jobs 的代码,伪代码为:for(const job of jobs)
    const {module} = job;
    const {hash, runtime, runtimes} = job;
    this._codeGenerationModule({module, runtime, runtimes, ... , dependencyTemplates});
}
function _codeGenerationModule({module, runtime, runtimes, ... , dependencyTemplates}) {
    //... 省略十分多的细节代码
    this.codeGeneratedModules.add(module);
    result = module.codeGeneration({
        chunkGraph,
        moduleGraph,
        dependencyTemplates,
        runtimeTemplate,
        runtime,
        codeGenerationResults: results,
        compilation: this
    });
    for (const runtime of runtimes) {results.add(module, runtime, result);
    }
    callback(null, codeGenerated);
}
1.3.3.3 NormalModule.codeGeneration()

外围代码也就是调用 generator.generate(),咱们的示例都是JS 类型,因而等同于调用JavascriptGenerator.generate()

function codeGeneration(..., dependencyTemplates, codeGenerationResults) {
    //... 省略十分多的细节代码
    const sources = new Map();
    for (const type of sourceTypes || chunkGraph.getModuleSourceTypes(this)) {const source = this.generator.generate(this, { ..., dependencyTemplates, codeGenerationResults});
        if (source) {sources.set(type, new CachedSource(source));
        }
    }
    const resultEntry = {
        sources,
        runtimeRequirements,
        data
    };
    return resultEntry;
}
1.3.3.4 JavascriptGenerator.generate()
generate(module, generateContext) {const originalSource = module.originalSource();
    const source = new ReplaceSource(originalSource);
    const initFragments = [];
    this.sourceModule(module, initFragments, source, generateContext);
    return InitFragment.addToSource(source, initFragments, generateContext);
}

originalSource实质就是 entry4.js 的原始内容

sourceModule()

在下面的剖析中,咱们晓得应用 sourceModule() 就是

  • 遍历所有 module.dependency,调用sourceDependency() 解决
  • 遍历所有 module.presentationalDependencies,调用sourceDependency() 解决

sourceDependency() 就是触发对应 dependencytemplate.apply()进行:

  • 文件内容 source 的革新
  • initFragments.push(fragment)收集,initFragments数组在 InitFragment.addToSource() 流程为 source 插入 header 代码和 bottom 代码

entry4.js 这个 module

module.dependencies=[HarmonyImportSideEffectDependency("./item/common_____g.js"),
    HarmonyImportSideEffectDependency("voca"),
    HarmonyImportSpecifierDependency("./item/common_____g.js"),
    HarmonyImportSpecifierDependency("voca")
]
module.presentationalDependencies=[HarmonyCompatibilityDependency, ConstDependency, ConstDependency]
InitFragment.addToSource()

通过 sourceModule() 的解决,咱们当初就能拿到 originalSource,以及initFragments 数组

generate(module, generateContext) {const originalSource = module.originalSource();
    const source = new ReplaceSource(originalSource);
    const initFragments = [];
    this.sourceModule(module, initFragments, source, generateContext);
    return InitFragment.addToSource(source, initFragments, generateContext);
}

在下面的剖析中,咱们晓得 InitFragment.addToSource() 就是依照上面程序拼接最终生成代码concatSource

  • header局部代码:fragment.getContent()
  • middle局部代码: module文件代码(可能通过一些革新)
  • bottom局部代码:fragment.getEndContent()

最终返回ConcatSource

而对于 entry4.js 来说,sourceModule()会触发什么类型的 template.apply() 进行代码的收集呢?

import {getG} from "./item/common_____g.js";
import voca from 'voca';
voca.kebabCase('goodbye blue sky'); // => 'goodbye-blue-sky'
import (/*webpackChunkName: "B"*/"./async/async_B.js").then(bModule=> {bModule.default();
});

console.info("getA2E", getG());

最终 entry4.js 生成的代码如下所示

__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _item_common_g_js__WEBPACK_IMPORTED_MODULE_1__ = 
  __webpack_require__(/*! ./item/common_____g.js */ \"./src/item/common_____g.js\");
/* harmony import */ var voca__WEBPACK_IMPORTED_MODULE_0__ = 
  __webpack_require__(/*! voca */ \"./node_modules/voca/index.js\");
/* harmony import */ var voca__WEBPACK_IMPORTED_MODULE_0___default = 
  /*#__PURE__*/__webpack_require__.n(voca__WEBPACK_IMPORTED_MODULE_0__);


voca__WEBPACK_IMPORTED_MODULE_0___default().kebabCase('goodbye blue sky'); // => 'goodbye-blue-sky'
__webpack_require__.e(/*! import() | B */ \"B\").then(__webpack_require__.bind(__webpack_require__, /*! ./async/async_B.js */ \"./src/async/async_B.js\")).then(bModule=> {bModule.default();
});

console.info(\"getA2E\", (0,_item_common_g_js__WEBPACK_IMPORTED_MODULE_1__.getG)());

//# sourceURL=webpack://webpack-5-image/./src/entry4.js?

咱们接下来就要剖析 entry4.js 的每一句代码会造成什么 dependency,从而触发的template 的类型是什么,template.apply()又做了什么?

import voca from 'voca';

造成HarmonyImportSideEffectDependency,触发HarmonyImportSideEffectDependencyTemplate.apply()

HarmonyImportSideEffectDependencyTemplate.apply() 中,如代码所示,触发initFragments.push(),其中content=importStatement[0] + importStatement[1]

const importStatement = dep.getImportStatement(false, templateContext);
templateContext.initFragments.push(
    new ConditionalInitFragment(importStatement[0] + importStatement[1],
        InitFragment.STAGE_HARMONY_IMPORTS,
        dep.sourceOrder,
        key,
        runtimeCondition
    )
);

最终在拼接最终生成代码时,ConditionalInitFragment.content也就是 importStatement[0] + importStatement[1] 的内容如下所示

/* harmony import */ var voca__WEBPACK_IMPORTED_MODULE_0__ = 
  __webpack_require__(/*! voca */ "./node_modules/voca/index.js");
/* harmony import */ var voca__WEBPACK_IMPORTED_MODULE_0___default = 
  /*#__PURE__*/__webpack_require__.n(voca__WEBPACK_IMPORTED_MODULE_0__);

问题:咱们晓得,initFragments.push()做的事件是收集插入的代码,不是替换,那么原来的 import voca from 'voca'; 是如何删除的呢?

波及到 HarmonyImportSideEffectDependency 的增加相干逻辑,当 AST 解析失去 HarmonyImportSideEffectDependency 时,如上面代码所示,也会触发对应的 ConstDependency 的增加,即

  • module.addPresentationalDependency(clearDep)
  • module.addDependency(sideEffectDep)
// CompatibilityPlugin.js
parser.hooks.import.tap(
    "HarmonyImportDependencyParserPlugin",
    (statement, source) => {
        parser.state.lastHarmonyImportOrder =
            (parser.state.lastHarmonyImportOrder || 0) + 1;
        const clearDep = new ConstDependency(parser.isAsiPosition(statement.range[0]) ? ";" : "",
            statement.range
        );
        clearDep.loc = statement.loc;
        parser.state.module.addPresentationalDependency(clearDep);
        parser.unsetAsiPosition(statement.range[1]);
        const assertions = getAssertions(statement);
        const sideEffectDep = new HarmonyImportSideEffectDependency(
            source,
            parser.state.lastHarmonyImportOrder,
            assertions
        );
        sideEffectDep.loc = statement.loc;
        parser.state.module.addDependency(sideEffectDep);
        return true;
    }
);

因而在 entry4.js 这个 module 中,生成对应的HarmonyImportSideEffectDependency,也会生成对应的ConstDependency

module.dependencies=[HarmonyImportSideEffectDependency("./item/common_____g.js"),
    HarmonyImportSideEffectDependency("voca"),
    HarmonyImportSpecifierDependency("./item/common_____g.js"),
    HarmonyImportSpecifierDependency("voca")
]
module.presentationalDependencies=[HarmonyCompatibilityDependency, ConstDependency, ConstDependency]

ConstDependency的作用就是记录对应的 import 语句的地位,而后应用 source.replace() 进行替换

source.replace(dep.range[0], dep.range[1] - 1, dep.expression)

在这个示例中,dep.expression="",因而替换后为:

import {getG} from "./item/common_____g.js";    ===>  ""

voca.kebabCase('goodbye blue sky'); // => 'goodbye-blue-sky'

造成HarmonyImportSpecifierDependency,触发HarmonyImportSpecifierDependencyTemplate.apply()

HarmonyImportSpecifierDependencyTemplate.apply() 中,如代码所示,触发initFragments.push(),其中content=importStatement[0] + importStatement[1]

class HarmonyImportSpecifierDependencyTemplate extends (HarmonyImportDependency.Template) {apply(dependency, source, templateContext, module) {const dep = /** @type {HarmonyImportSpecifierDependency} */ (dependency);
        const {moduleGraph, runtime} = templateContext;
        const ids = dep.getIds(moduleGraph);
        const exportExpr = this._getCodeForIds(dep, source, templateContext, ids);
        const range = dep.range;
        if (dep.shorthand) {source.insert(range[1], `: ${exportExpr}`);
        } else {source.replace(range[0], range[1] - 1, exportExpr);
        }
    }
}

source实质就是 ReplaceSourceinsert() 是将要替换的代码的范畴以及内容都放在 _replacements 属性中

insert(pos, newValue, name) {this._replacements.push(new Replacement(pos, pos - 1, newValue, name));
    this._isSorted = false;
}

最终在拼接最终生成代码时,替换的数据为:

voca.kebabCase('goodbye blue sky') ===>  "voca__WEBPACK_IMPORTED_MODULE_0___default().kebabCase"

同理,import {getG} from "./item/common_____g.js";和对应的调用办法的生成代码流程如下面 voca 的剖析一样

import {getG} from "./item/common_____g.js";
                              ↓
                              ↓
                              ↓
/* harmony import */ var _item_common_g_js__WEBPACK_IMPORTED_MODULE_1__ = 
  __webpack_require__(/*! ./item/common_____g.js */ \"./src/item/common_____g.js\");
getG() ===>  "(0,_item_common_g_js__WEBPACK_IMPORTED_MODULE_1__.getG)"

当初咱们曾经处理完毕 module.dependencies 的所有依赖,接下里咱们要解决module.presentationalDependencies

module.dependencies=[HarmonyImportSideEffectDependency("./item/common_____g.js"),
    HarmonyImportSideEffectDependency("voca"),
    HarmonyImportSpecifierDependency("./item/common_____g.js"),
    HarmonyImportSpecifierDependency("voca")
]
module.presentationalDependencies=[HarmonyCompatibilityDependency, ConstDependency, ConstDependency]

HarmonyCompatibilityDependency是一个非凡的依赖,在 AST 解析的过程中,咱们就会判断 ast.body 是否蕴含

  • ImportDeclaration
  • ExportDefaultDeclaration
  • ExportNamedDeclaration
  • ExportAllDeclaration

如果蕴含,则module.addPresentationalDependency(HarmonyCompatibilityDependency)

// JavascriptParser.js
parse(source, state) {if (this.hooks.program.call(ast, comments) === undefined) {this.detectMode(ast.body);
        this.preWalkStatements(ast.body);
        this.prevStatement = undefined;
        this.blockPreWalkStatements(ast.body);
        this.prevStatement = undefined;
        this.walkStatements(ast.body);
    }
    return state;
}
// HarmonyDetectionParserPlugin.js
parser.hooks.program.tap("HarmonyDetectionParserPlugin", ast => {
    const isStrictHarmony = parser.state.module.type === "javascript/esm";
    const isHarmony =
        isStrictHarmony ||
        ast.body.some(
            statement =>
                statement.type === "ImportDeclaration" ||
                statement.type === "ExportDefaultDeclaration" ||
                statement.type === "ExportNamedDeclaration" ||
                statement.type === "ExportAllDeclaration"
        );
    if (isHarmony) {
        const module = parser.state.module;
        const compatDep = new HarmonyCompatibilityDependency();

        module.addPresentationalDependency(compatDep);
    }
});

而在 HarmonyExportDependencyTemplate.apply() 的解决也很简略,就是减少一个 new InitFragment() 数据到initFragments

const exportsInfo = moduleGraph.getExportsInfo(module);
if (exportsInfo.getReadOnlyExportInfo("__esModule").getUsed(runtime) !==
    UsageState.Unused
) {
    const content = runtimeTemplate.defineEsModuleFlagStatement({
        exportsArgument: module.exportsArgument,
        runtimeRequirements
    });
    initFragments.push(
        new InitFragment(
            content,
            InitFragment.STAGE_HARMONY_EXPORTS,
            0,
            "harmony compatibility"
        )
    );
}

最终这个 InitFragment 造成的代码为:

__webpack_require__.r(__webpack_exports__);

sourceModule()解决实现所有 module.dependencymodule.presentationalDependencies之后,开始解决 module.blocks 异步依赖

sourceModule(module, initFragments, source, generateContext) {for (const dependency of module.dependencies) {this.sourceDependency();
    }
    for (const dependency of module.presentationalDependencies) {this.sourceDependency();
    }
    for (const childBlock of module.blocks) {this.sourceBlock()
    }
}
sourceBlock(module, block, initFragments, source, generateContext) {for (const dependency of block.dependencies) {this.sourceDependency();
    }
    for (const childBlock of block.blocks) {this.sourceBlock();
    }
}

entry4.js 的源码中能够晓得,存在着惟一一个异步依赖async_B.js

import {getG} from "./item/common_____g.js";
import voca from 'voca';
voca.kebabCase('goodbye blue sky'); // => 'goodbye-blue-sky'
import (/*webpackChunkName: "B"*/"./async/async_B.js").then(bModule=> {bModule.default();
});

console.info("getA2E", getG());

异步依赖 async_B.js 造成ImportDependency,触发ImportDependencyTemplate.apply()

ImportDependencyTemplate.apply() 中,如代码所示,触发 source.replacesource 实质就是 ReplaceSourcereplace() 是将要替换的代码的范畴以及内容都放在 _replacements 属性中

const dep = /** @type {ImportDependency} */ (dependency);
const block = /** @type {AsyncDependenciesBlock} */ (moduleGraph.getParentBlock(dep)
);
const content = runtimeTemplate.moduleNamespacePromise({
  chunkGraph,
  block: block,
  module: moduleGraph.getModule(dep),
  request: dep.request,
  strict: module.buildMeta.strictHarmonyModule,
  message: "import()",
  runtimeRequirements
});

source.replace(dep.range[0], dep.range[1] - 1, content);

// ReplaceSource.js
replace(start, end, newValue, name) {this._replacements.push(new Replacement(start, end, newValue, name));
  this._isSorted = false;
}

最终在拼接最终生成代码时,替换的数据为:

import (/*webpackChunkName: "B"*/"./async/async_B.js")
                              ↓
                              ↓
                              ↓

__webpack_require__.e(/*! import() | B */ "B").then(__webpack_require__.bind(__webpack_require__, /*! ./async/async_B.js */ "./src/async/async_B.js"))

createChunkAssets: module 代码合并打包成 chunk

2.1 概述

次要执行逻辑程序为:

  • this.hooks.renderManifest.call([], options)触发,获取 render() 对象,此时还没生成代码
  • source = fileManifest.render(),触发 render() 进行代码渲染拼凑
  • this.emitAsset(file, source, assetInfo)将代码写入到文件中
seal() {
    //... 依据 entry 初始化 chunk 和 chunkGroup,关联 chunk 和 chunkGroup
    // ... 遍历 entry 所有的 dependencies,关联 chunk、dependencies、chunkGroup
    // 为 module 设置深度标记
    this.assignDepths(entryModules);
    buildChunkGraph(this, chunkGraphInit);

    this.createModuleHashes();
    this.codeGeneration(err => {this.createChunkAssets(err => {});
    });
}
function createChunkAssets(callback) {
    asyncLib.forEachLimit(
        this.chunks,
        (chunk, callback) => {// this.getRenderManifest=this.hooks.renderManifest.call([], options);
            let manifest = this.getRenderManifest({
                chunk,
                ...
            });
            
            //... 遍历 manifest,调用(manifest[i]=fileManifest)fileManifest.render()
            asyncLib.forEach(
                manifest,
                (fileManifest, callback) => {
                    //....
                    source = fileManifest.render();
                    this.emitAsset(file, source, assetInfo);
                }
            );
        }
    )
}

2.2 manifest=this.getRenderManifest

manifest=this.hooks.renderManifest.call([], options)

parsergeneratordependencyTemplates 相似,这里也采纳 hooks.renderManifest.tap 注册监听,依据传入的 chunk 中不同类型的局部,比方

  • 触发 JavascriptModulesPlugin.js 解决 chunk 中的 js 局部
  • 触发 CssModulePlugins.js 解决 chunk 中的 css 局部

2.3 source = manifest[i].render()

由下面的剖析能够晓得,会依据 chunk 中有多少类型数据而采纳不同的 Plugin 进行解决,其中最常见的就是 JavascriptModulesPlugin 的解决,上面咱们应用 JavascriptModulesPlugin 看下整体的 render() 流程

2.3.1 source = JavascriptModulesPlugin.render()

依据不同的状态,进行 render() 办法内容的调用

  • chunk.hasRuntime()->renderMain()办法
  • chunk.renderChunk()->renderChunk()办法
// JavascriptModulesPlugin
compilation.hooks.renderManifest.tap(
    "JavascriptModulesPlugin",
    (result, options) => {if (hotUpdateChunk) {render = () => this.renderChunk(...);
        } else if (chunk.hasRuntime()) {render = () => this.renderMain(...);
        } else {if (!chunkHasJs(chunk, chunkGraph)) {return result;}
            render = () => this.renderChunk();
        }
        result.push({
            render,
            filenameTemplate,
            pathOptions: {
                hash,
                runtime: chunk.runtime,
                chunk,
                contentHashType: "javascript"
            },
            ...
        });
        return result;
    }
)

2.3.2 renderMain(): 入口 entry 类型 chunk 触发代码生成


问题

  • mode: "development"mode: "production" 两种模式有什么区别?
  • 为什么 renderMain() 的内容是对应 mode: "development" 的?mode:production是有另外的分支解决还是在 renderMain() 根底上进行革新呢?

整体流程图

上面会针对该流程图进行具体的文字描述剖析

打包产物详细分析

从下面所打包生成的代码,咱们能够总结进去打包产物为

名称 解释 示例
webpackBootstrap 最外层包裹的立刻执行函数 (function(){})
webpack_modules 所有 module 的代码,包含
webpack_module_cache 对加载过的 module 进行缓存,如果曾经缓存过,下一次加载就不执行上面 __webpack_require__() 办法 var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) 通过 moduleId 进行模块的加载
__webpack_require__.m 蕴含所有 module 的对象的别名 __webpack_require__.m = __webpack_modules__
runtime 各种工具函数 各种 __webpack_require__.xxx 的办法,提供各种能力,比方 __webpack_require__.O 提供 chunk loaded 的能力
Load entry module 加载入口文件,如果入口文件须要依赖其它chunk,则提早加载入口文件
整体流程图文字描述

依照指定程序合并代码

  1. 合并立刻执行函数的最头部的代码,一个(()=> {
  2. 合并所有 module 的代码
  3. 合并 __webpack_module_cache__ = {};function __webpack_require__(moduleId) 等加载代码
  4. 合并 runtime 模块代码
  5. 合并 "Load entry module and return exports" 等代码,即立刻执行函数中的执行入口文件解析的代码局部,被动触发入口文件的加载
  6. 合并立刻执行函数的最初最初的代码,即})()

因为计算代码和合并代码逻辑揉杂在一起,因而调整了上面代码程序,加强可读性

renderMain(renderContext, hooks, compilation) {let source = new ConcatSource();
    const iife = runtimeTemplate.isIIFE();
    // 1. 计算出 "function __webpack_require__(moduleId)" 等代码
    const bootstrap = this.renderBootstrap(renderContext, hooks);
    // 2. 计算出所有 module 的代码
    const chunkModules = Template.renderChunkModules(
        chunkRenderContext,
        inlinedModules
            ? allModules.filter(m => !inlinedModules.has(m))
            : allModules,
        module => this.renderModule(module, chunkRenderContext, hooks, true),
        prefix
    );
    // 3. 计算出运行时的代码
    const runtimeModules =
        renderContext.chunkGraph.getChunkRuntimeModulesInOrder(chunk);

    // 1. 合并立刻执行函数的最头部的代码,一个(()=> {if (iife) {if (runtimeTemplate.supportsArrowFunction()) {source.add("/******/ (() => {// webpackBootstrap\n");
        } else {source.add("/******/ (function() {// webpackBootstrap\n");
        }
        prefix = "/******/ \t";
    } else {prefix = "/******/";}
    // 2. 合并所有 module 的代码
    if (
        chunkModules ||
        runtimeRequirements.has(RuntimeGlobals.moduleFactories) ||
        runtimeRequirements.has(RuntimeGlobals.moduleFactoriesAddOnly) ||
        runtimeRequirements.has(RuntimeGlobals.require)
    ) {source.add(prefix + "var __webpack_modules__ = (");
        source.add(chunkModules || "{}");
        source.add(");\n");
        source.add("/************************************************************************/\n");
    }
    // 3. 合并 "__webpack_module_cache__ = {};function __webpack_require__(moduleId)" 等加载代码
    if (bootstrap.header.length > 0) {const header = Template.asString(bootstrap.header) + "\n";
        source.add(
            new PrefixSource(
                prefix,
                useSourceMap
                    ? new OriginalSource(header, "webpack/bootstrap")
                    : new RawSource(header)
            )
        );
        source.add("/************************************************************************/\n");
    }
    // 4. 合并 runtime 模块代码
    if (runtimeModules.length > 0) {
        source.add(
            new PrefixSource(
                prefix,
                Template.renderRuntimeModules(runtimeModules, chunkRenderContext)
            )
        );
        source.add("/************************************************************************/\n");
        // runtimeRuntimeModules calls codeGeneration
        for (const module of runtimeModules) {compilation.codeGeneratedModules.add(module);
        }
    }
    // 5. 合并 "Load entry module and return exports" 等代码,即立刻执行函数中的执行入口文件解析的代码局部
    // 被动触发入口文件的加载
    source.add(
        new PrefixSource(
            prefix,
            new ConcatSource(toSource(bootstrap.beforeStartup, "webpack/before-startup"),
                "\n",
                hooks.renderStartup.call(toSource(bootstrap.startup.concat(""),"webpack/startup"),
                    lastEntryModule,
                    {
                        ...renderContext,
                        inlined: false
                    }
                ),
                toSource(bootstrap.afterStartup, "webpack/after-startup"),
                "\n"
            )
        )
    );
    // 6. 合并立刻执行函数的最初最初的代码
    if (iife) {source.add("/******/})()\n");
    }
}
Template.renderChunkModules

renderMain() 的流程中,次要还是调用了 Template.renderChunkModules() 进行该 chunk 波及到 module 的渲染,上面咱们将简略剖析下该办法

传入该 chunk 蕴含的所有 modules 以及对应的 renderModule() 函数,返回所有 module 渲染的source

在不同的 Plugin 中书写 renderModule() 函数,应用 Template.renderChunkModules() 静态方法调用 renderModule() 函数生成所有 modulessource,实质还是利用 codeGeneration() 中生成的 codeGenerationResults 获取对应 modulesource

留神!要渲染的 modules 会依照 identifier 进行排序渲染

2.3.3 renderChunk(): 非入口 entry 类型 chunk 触发代码生成

异步 chunk、合乎 splitChunkPlugin 的 cacheGroup 规定的 chunk

实质 renderChunk() 次要也是调用 Template.renderChunkModules() 进行所有 module 相干代码的生成

Template.renderChunkModules()办法曾经在下面 renderMain() 中剖析,这里不再赘述

而后拼接一些字符串造成最终的bundle

renderChunk(renderContext, hooks) {const { chunk, chunkGraph} = renderContext;
    const modules = chunkGraph.getOrderedChunkModulesIterableBySourceType(
        chunk,
        "javascript",
        compareModulesByIdentifier
    );
    const allModules = modules ? Array.from(modules) : [];
    const chunkRenderContext = {
        ...renderContext,
        chunkInitFragments: [],
        strictMode: allStrict
    };
    const moduleSources =
        Template.renderChunkModules(chunkRenderContext, allModules, module =>
            this.renderModule(module, chunkRenderContext, hooks, true)
        ) || new RawSource("{}");
    let source = tryRunOrWebpackError(() => hooks.renderChunk.call(moduleSources, chunkRenderContext),
        "JavascriptModulesPlugin.getCompilationHooks().renderChunk");
    //... 省略对 source 的优化解决
    chunk.rendered = true;
    return strictHeader
        ? new ConcatSource(strictHeader, source, ";")
        : renderContext.runtimeTemplate.isModule()
            ? source
            : new ConcatSource(source, ";");
}

Compilation.emitAsset(file, source, assetInfo)

// node_modules/webpack/lib/Compilation.js
seal() {
    //... 依据 entry 初始化 chunk 和 chunkGroup,关联 chunk 和 chunkGroup
    // ... 遍历 entry 所有的 dependencies,关联 chunk、dependencies、chunkGroup
    // 为 module 设置深度标记
    this.assignDepths(entryModules);
    buildChunkGraph(this, chunkGraphInit);

    this.createModuleHashes();
    this.codeGeneration(err => {this.createChunkAssets(err => {});
    });
}
function createChunkAssets(callback) {
    asyncLib.forEachLimit(
        this.chunks,
        (chunk, callback) => {
            let manifest = this.getRenderManifest({
                chunk,
                ...
            });
            // manifest=this.hooks.renderManifest.call([], options);

            asyncLib.forEach(
                manifest,
                (fileManifest, callback) => {
                    //....
                    source = fileManifest.render();
                    this.emitAsset(file, source, assetInfo);
                }
            );
        }
    )
}

将生成代码赋值到 assets 对象上,筹备写入文件,而后调用 callback,完结compliation.seal() 流程

// node_modules/webpack/lib/Compilation.js
createChunkAssets(callback) {this.assets[file] = source;
}

Compiler.emitAsset

从上面代码能够晓得,compilation.seal完结时会调用 onCompiled() 办法,从而触发 this.emitAssets() 办法

// node_modules/webpack/lib/Compiler.js
run(callback) {
    //....
    const onCompiled = (err, compilation) => {
        this.emitAssets(compilation, err => {logger.time("emitRecords");
            this.emitRecords(err => {logger.time("done hook");
                const stats = new Stats(compilation);
                this.hooks.done.callAsync(stats, err => {logger.timeEnd("done hook");
                });
            });
        });
    };
    this.compile(onCompiled);
}

compile(callback) {
    compilation.seal(err => {
        this.hooks.afterCompile.callAsync(compilation, err => {return callback(null, compilation);
        });
    });
}

mkdirp()webpack 封装的一个办法,实质还是调用 fs.mkdir(p, err => {}) 异步地创立目录,而后调用 emitFiles() 办法

function emitAssets() {outputPath = compilation.getPath(this.outputPath, {});
    mkdirp(this.outputFileSystem, outputPath, emitFiles);
}

emitFiles() 实质也是遍历 Compliation.emitAsset() 生成的资源文件 this.assets[file],而后从source 失去 binary (Buffer) 内容输入生成文件

const emitFiles = err => {const assets = compilation.getAssets();
    asyncLib.forEachLimit(
        assets,
        15,
        ({name: file, source, info}, callback) => {
            let targetFile = file;
            if (targetFile.match(/\/|\\/)) {
                const fs = this.outputFileSystem;
                const dir = dirname(fs, join(fs, outputPath, targetFile));
                mkdirp(fs, dir, writeOut);
            } else {writeOut();
            }
        }
    )
}
const writeOut = err => {
    const targetPath = join(
        this.outputFileSystem,
        outputPath,
        targetFile
    );
    // 从 source 失去 binary (Buffer)内容
    const getContent = () => {if (typeof source.buffer === "function") {return source.buffer();
        } else {const bufferOrString = source.source();
            if (Buffer.isBuffer(bufferOrString)) {return bufferOrString;} else {return Buffer.from(bufferOrString, "utf8");
            }
        }
    };
    const doWrite = content => {this.outputFileSystem.writeFile(targetPath, content, err => {});
    };
    const processMissingFile = () => {const content = getContent();
        //...
        return doWrite(content);
    };
    processMissingFile();};

其它知识点

6.1 runtime 代码的品种和作用

摘录自 https://webpack.docschina.org/concepts/manifest#runtime

runtime,以及随同的 manifest 数据,次要是指:在浏览器运行过程中,webpack 用来连贯模块化应用程序所需的所有代码。它蕴含:在模块交互时,连贯模块所需的加载和解析逻辑。包含:曾经加载到浏览器中的连贯模块逻辑,以及尚未加载模块的提早加载逻辑。

runtime 蕴含的函数 作用
__webpack_require__.o 工具函数,判断是否有某属性
__webpack_require__.d 对应 exports,用来定义导出变量对象:Object.defineProperty(exports, key, { enumerable: true, get: definition[key] })
__webpack_require__.r 辨别是否是 es 模块,给导出导出变量对象增加 __esModule:true 属性
__webpack_require__.l 工具函数,动态创建 script 标签去加载 js,比方在热更新 HRM 中用来加载main.xxx.hot-update.js
__webpack_require__.O chunk loaded
__webpack_require__.m 蕴含所有 module 的对象的别名,__webpack_require__.m = __webpack_modules__

如果 entry Module 蕴含了异步加载的 import 逻辑,那么合并的 runtime 代码还会生成一些异步的办法

runtime 蕴含的函数 作用
__webpack_require__.e 加载异步 chunk 的入口办法,外部应用__webpack_require__.f 进行 chunk 的加载
__webpack_require__.f 异步加载的对象,下面能够挂载一些异步加载的具体方法
__webpack_require__.f.j 具体的 chunk 的加载办法,封装 promise,检测是否有缓存installedChunkData,而后应用多种办法返回后果,最终应用__webpack_require__.l 加载对应的 js
__webpack_require__.g 获取全局 this
__webpack_require__.p 获取 publicPath,首先尝试从document 中获取 publicPath,如果获取不到,咱们须要在output.publicPath/__webpack_public_path__ 被动申明publicPath
__webpack_require__.u 传入chunkId,返回[chunkId].js,拼接后缀
__webpack_require__.l 工具函数,动态创建 script 标签去加载 js

runtime 蕴含的办法具体的联动逻辑请看上面的剖析

6.2 webpack 如何解决循环依赖

同步 import、异步 import、require、export {xxx}、export default 等 5 种状况剖析

6.2.1 ES6 和 CommonJS 的区别

参考自阮一峰 -Node.js 如何解决 ES6 模块和 ES6 模块和 CommonJS 模块有哪些差别?

CommonJS ES6
语法 应用 require 导入和 module.exports(exports) 导出 应用 import 导入和 export 导出
用法 运行时加载:只有代码遇到 require 指令时,才会去执行模块中的代码,如果曾经 require 一个模块,就要期待它执行结束后能力执行前面的代码 编译时输入接口:编译时就能够确定模块的依赖关系,编译时遇到 import 不会去执行模块,只会生成一个援用,等到真正须要时,才会到模块中进行取值
输入值 CommonJS模块输入的是一个值的复制 CommonJS 输入的是 module.exports={xx} 对象,如果这个对象蕴含根本类型,module外部扭转了这个根本类型,输入的值不会受到影响,因而一个 Object 的根本类型是拷贝的,而不是援用,如果 Object 蕴含援用类型,如Array,则会影响内部援用的值 ES6模块输入的是值的援用,输入的是一个只读援用,等到脚本真正执行时,再依据这个只读援用,到被加载的那个模块外面去取值,因而 ES6 模块外部的任何变动,包含根本类型和援用类型的变动,都会导致内部援用的值发生变化。如果是 export default {xxx},因为导出的是一个对象,变动规定跟 CommonJS 一样,根本类型是值的拷贝,援用类型是内存地址的拷贝

6.2.2 具体例子剖析

加深对打包产物整体运行流程的了解

ES6 模式下 import 的循环援用

如上面代码所示,咱们在入口文件中 import ./es_a.js,而后在es_a.jsimport ./es_b.js,在 es_.js 中又import ./es_a.js

import {a_Value} from "./es_a";

console.error("筹备开始 es_entry1", a_Value);


咱们在下面比拟中说了 ES6 模块输入的是值的援用,输入的是一个只读援用,等到脚本真正执行时,再依据这个只读援用,到被加载的那个模块外面去取值

因而在看 webpack 打包文件时,咱们就能够直接判断出应该打印出的程序为:

  • 一开始 import 会晋升到顶部,进行动态剖析
  • es_a.js 中,咱们最开始调用 b_Value,因而这个时候咱们会去找es_b 模块是否曾经加载,如果没有加载则创立对应的模块,而后进入 es_b 模块,试图去寻找 export 语句
  • es_b.js 中,咱们最开始调用 a_Value,咱们会寻找es_a 模块是否曾经加载,目前曾经加载了,然而因为还没执行结束 es_a.js 的全部内容就进入 es_b 模块,因而此时 a_Value=undefined,而后export var b_Value,继续执行结束es_b 模块剩下的内容
  • 此时从 es_b.js->es_a.js,咱们拿到了b_Value,将b_Value 打印进去,而后再export var a_Value
  • 500 毫秒后,es_b.js定时器触发,去 es_a 模块寻找对应的 a_Value,此时a_Value="我是一开始的 a.js",并且扭转了b_Value 的值
  • 1000 毫秒后,es_a.js定时器触发,去 es_b 模块寻找对应的b_Value,此时a_Value="我是完结后扭转的 b.js"

咱们再依据 webpack 打包的后果查看打印的信息(上面的截图),跟咱们的推断一样


webpack 是如何产生这种成果的呢?

咱们间接查看 webpack 打包的产物,咱们能够发现

  • __webpack_require__.r:标记为ESModule
  • __webpack_require__.d:将 export 晋升到最顶部,而后申明对应的 key 以及对应的function
  • __webpack_require__(xxxx):这个时候才进行 import 的解决
  • 残余代码:文件内容的代码逻辑

从上面两个代码块,咱们就能够很好解释下面打印信息的前后程序了,一开始

  • "./src/es_a.js":申明了对应的 harmony exportkey,而后 __webpack_require__("./src/es_b.js") 调用 es_b 模块,进入"./src/es_b.js"
  • "./src/es_b.js":申明了对应的 harmony exportkey,而后 __webpack_require__("./src/es_a.js"),此时_es_a_js__WEBPACK_IMPORTED_MODULE_0__ 存在!然而 a_Value 还没赋值,因为 var a_Value 会进行变量晋升,因而拿到的 a_Value=undefined,执行结束es_b.js 剩下的代码,此时 b_Value 曾经赋值
  • es_b.js->es_a.js,打印出对应的_es_b_js__WEBPACK_IMPORTED_MODULE_0__.b_Value,而后赋值a_Value
  • 在接下来的两个 setTimeout() 中,因为都是去 __webpack_require__.d 获取对应 keyfunction(),因而能够实时拿到变动的值
"./src/es_a.js":
(function (__unused_webpack_module, __webpack_exports__, __webpack_require__) {__webpack_require__.r(__webpack_exports__);
    /* harmony export */
    __webpack_require__.d(__webpack_exports__, {/* harmony export */   "a_Value": function () {return /* binding */ a_Value;}
        /* harmony export */
    });
    /* harmony import */
    var _es_b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./es_b.js */ "./src/es_b.js");

    console.info("a.js 开始运行");
    console.warn("在 a.js 中 import 失去 b", _es_b_js__WEBPACK_IMPORTED_MODULE_0__.b_Value);

    var a_Value = "我是一开始的 a.js";
    console.info("a.js 完结运行");

    setTimeout(() => {console.warn("在 a.js 完结运行后 (b.js 那边曾经在 500 毫秒前扭转值) 再次 import 失去 b",
            _es_b_js__WEBPACK_IMPORTED_MODULE_0__.b_Value);
    }, 1000);
})
"./src/es_b.js":
(function (__unused_webpack_module, __webpack_exports__, __webpack_require__) {__webpack_require__.r(__webpack_exports__);
  /* harmony export */
  __webpack_require__.d(__webpack_exports__, {/* harmony export */   "b_Value": function () {return /* binding */ b_Value;}
    /* harmony export */
  });
  /* harmony import */
  var _es_a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./es_a.js */ "./src/es_a.js");
  console.info("bbbbbbb.js 开始运行");
  console.warn("在 bbbbbbb.js 中 import 失去 a", _es_a_js__WEBPACK_IMPORTED_MODULE_0__.a_Value);
  var b_Value = "我是一开始的 b.js";
  console.info("b.js 完结运行");
  setTimeout(() => {
    b_Value = "我是完结后扭转的 b.js";
    console.warn("在 b.js 中提早后再次 import 失去 a", _es_a_js__WEBPACK_IMPORTED_MODULE_0__.a_Value);
  }, 500);
})
CommonJS 模式下 require 的循环援用
console.error("筹备开始 entry");
const moduleA = require("./a.js");
const moduleB = require("./b.js");

console.error("entry 最初拿到的值是", moduleA.a_Value);
console.error("entry 最初拿到的值是", moduleB.b_Value);

具体的示例代码如上所示,它所生成的 webpack 代码如下所示,整体流程是比较简单的,会应用 module.exports 存储要裸露进来的数据,如果加载过一次,则会存入缓存中,因而下面示例代码整体步骤为

  • entry2.js:触发 require("./a.js"),这个时候因为a 模块还没创立,因而会进入到 a 模块中
  • a.js:先裸露了一个 exports.a_Value 数据,而后触发 require("./b.js"),这个时候因为b 模块还没创立,因而会进入到 b 模块中
  • b.js:进入 b.js 后,会遇到 require("./a.js"),这个时候a 模块曾经创立,存入到 __webpack_module_cache__["a"] 中,因而会间接从缓存中读取数据,此时读到了它的 module.exports.a_Value,显示进去后,又扭转了b 模块的 module.exports.b_Value 数据
  • a.js:回到 a.js 后,将失去的 b 模块的 module.exports.b_Value 打印进去,而后扭转本身的一个变量 module.exports.a_Value 数据
  • entry2.jsa.js->b.js->a.js后回到 entry2.js,将a 模块的一个变量 module.exports.a_Valueb模块的一个变量 module.exports.b_Value 打印进去
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {return cachedModule.exports;}
    var module = __webpack_module_cache__[moduleId] = {exports: {}
    };
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    return module.exports;
}
var __webpack_modules__ = ({
    /***/ "./src/CommonJS/a.js":
    /***/ (function (__unused_webpack_module, exports, __webpack_require__) {console.info("a.js 开始运行");
            var a_Value = "我是一开始的 a.js";
            exports.a_Value = a_Value;
            const bModule = __webpack_require__(/*! ./b.js */ "./src/CommonJS/b.js");
            console.warn("在 a.js 中 require 失去 bModule",
                bModule.b_Value);
            a_Value = "我是后扭转的 a.js!!!!!";
            exports.a_Value = a_Value;
            console.info("a.js 完结运行");
            /***/
        }),
    /***/ "./src/CommonJS/b.js":
    /***/ (function (__unused_webpack_module, exports, __webpack_require__) {console.info("bbbbbbb.js 开始运行");
            var b_Value = "我是一开始的 b.js";
            exports.b_Value = b_Value;
            const aModule = __webpack_require__(/*! ./a.js */ "./src/CommonJS/a.js");
            console.warn("在 bbbbbbb.js 中 require 失去 a",
                aModule.a_Value);
            b_Value = "我是后扭转的 b.js";
            exports.b_Value = b_Value;
            console.info("b.js 完结运行");
            /***/
        })
    /******/
});

下面流程运行后果如下所示,因为整体流程都围绕模块的 module.exports.xxx 变量开展,比较简单,这里不再着重剖析

6.3 chunk 是如何实现与其它 chunk 的联动

6.3.1 入口文件有其它 chunk 的同步依赖

须要先加载其它 chunk 再进行入口文件其它代码的执行,常见于应用 node_modules 的第三方库,触发 splitChunks 的默认 cacheGroups 打包造成一个chunk

在下面 renderMain() 的示例代码中,咱们的示例代码如下所示

import getE from "./item/entry1_a.js";
import {getG} from "./item/common_____g.js";
import _ from "loadsh";
console.info(_.add(13, 24));

var testvalue = getE() + getG();

import (/*webpackChunkName: "B"*/"./async/async_B.js").then(bModule => {bModule.default();
});


setTimeout(() => {const requireA = require("./require/require_A.js");
    console.info("testvalue", testvalue + requireA.getRequireA());
}, 4000);

最终生成的 chunk

app4.js:入口造成的Chunk

B.js:异步 import 造成的Chunk

C.js:异步 import 造成的Chunk

vendors-node_modules_loadsh_lodash_js.js:命中 node_modules 而造成的Chunk

从上图咱们能够晓得,咱们在加载入口文件时,咱们应该须要先加载结束vendors-node_modules_loadsh_lodash_js.js,因为在源码中,咱们是同步的,得先有loadsh,而后执行入口文件的其它内容


而从生成的代码中(如下所示),咱们也能够发现的确如此,从生成的正文能够晓得,如果咱们的 entry module 依赖其它 chunk,那么我就得应用__webpack_require__.O 提早加载入口文件./src/entry4.js

// This entry module depends on other loaded chunks and execution need to be delayed
var __webpack_exports__ = __webpack_require__.O(undefined, 
    ["vendors-node_modules_loadsh_lodash_js"], 
    function () { return __webpack_require__("./src/entry4.js"); })
__webpack_exports__ = __webpack_require__.O(__webpack_exports__);

__webpack_require__.O 是如何判断的呢?

在调用第一次 __webpack_require__.O 时,会传入 chunkIds,而后会将前置加载的chunk 数组存储到 deferred

!function () {var deferred = [];
    __webpack_require__.O = function (result, chunkIds, fn, priority) {if (chunkIds) {
            priority = priority || 0;
            for (var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
            deferred[i] = [chunkIds, fn, priority];
            return;
        }
        //...
        return result;
    };
}();

等到第二次调用__webpack_exports__ = __webpack_require__.O(__webpack_exports__),会触发一个遍历逻辑,也就是上面代码块所正文的那一行代码:

  • return __webpack_require__.O[key](chunkIds[j])
  • ==>
  • return __webpack_require__.O.j(chunkIds[j])
  • ==>
  • ƒ (chunkId) {return installedChunks[chunkId] === 0; }

理论就是检测是否曾经加载 deferred 存储的 chunks,如果加载了,能力触发fn(),也就是"./src/entry4.js" 入口文件的加载

!function () {var deferred = [];
    __webpack_require__.O = function (result, chunkIds, fn, priority) {if (chunkIds) {
           //...
            return;
        }
        //...
        for (var i = 0; i < deferred.length; i++) {var chunkIds = deferred[i][0];
            var fn = deferred[i][1];
            //...
            var fulfilled = true;
            for (var j = 0; j < chunkIds.length; j++) {//Object.keys(__webpack_require__.O).every(function (key) {return __webpack_require__.O[key](chunkIds[j]); })
                if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every(function (key) {return __webpack_require__.O[key](chunkIds[j]); })) {chunkIds.splice(j--, 1);
                } else {
                    fulfilled = false;
                    if (priority < notFulfilled) notFulfilled = priority;
                }
            }
            if (fulfilled) {deferred.splice(i--, 1)
                var r = fn();
                if (r !== undefined) result = r;
            }
        }
        return result;
    };
}();

因而,如果有申明 index.htmlwebpack 会帮咱们造成上面的 html 文件,先加载同步依赖的js,再加载对应的入口文件js

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
  </head>
  <body>
    <div id="box1"></div>
    <div id="box2"></div>
    <div id="box3"></div>
    <script src="./build-dev/vendors-node_modules_loadsh_lodash_js.js"></script>
    <script src="./build-dev/app4.js"></script>
  </body>
</html>

6.3.2 入口文件有其它 chunk 的异步依赖

异步 chunk 会触发 __webpack_require__.e 进行解决,外部也是调用 __webpack_require__.f.j->__webpack_require__.l 加载对应的[chunk].js

__webpack_require__.e = function (chunkId) {return Promise.all(Object.keys(__webpack_require__.f).reduce(function (promises, key) {__webpack_require__.f[key](chunkId, promises);
        return promises;
    }, []));
};
__webpack_require__.e(/*! import() | B */ "B").then(__webpack_require__.bind(__webpack_require__, /*! ./async/async_B.js */ "./src/async/async_B.js")
).then(bModule => {bModule.default();
});

从上面代码能够晓得,异步 ChunkB 返回的是一个立刻执行函数,会执行 self["webpackChunkwebpack_5_image"].push,其实就是chunkLoadingGlobal.push,会触发对应的__webpack_require__.m[moduleId] 实例化对应的数据

// B.js
(self["webpackChunkwebpack_5_image"] = self["webpackChunkwebpack_5_image"] || []).push([["B"], {
    "./src/async/async_B.js":
        (function (__unused_webpack_module, __webpack_exports__, __webpack_require__) {eval("...")
        })
}]);

// app4.js
var chunkLoadingGlobal = self["webpackChunkwebpack_5_image"] = self["webpackChunkwebpack_5_image"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));// 在异步 chunk 在 chunkLoadingGlobal 初始化之前曾经塞入数据
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));

var webpackJsonpCallback = function (parentChunkLoadingFunction, data) {
    //...
    var moreModules = data[1];
    //...
    if (chunkIds.some(function (id) {return installedChunks[id] !== 0; })) {for (moduleId in moreModules) {if (__webpack_require__.o(moreModules, moduleId)) {__webpack_require__.m[moduleId] = moreModules[moduleId];
            }
        }
    }
    for (; i < chunkIds.length; i++) {chunkId = chunkIds[i];
        if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {installedChunks[chunkId][0]();}
        installedChunks[chunkId] = 0;
    }
    //...
}

chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal))传入的第二个参数会作为初始参数跟起初调用的参数进行合并,因而 parentChunkLoadingFunction=chunkLoadingGlobal.push.bind(chunkLoadingGlobal),而data 就是 B.jspush的数据

webpackJsonpCallback()加载实现对应的 chunk.js 后,会将 chunk.js 的内容存储到对应的 __webpack_require__.m[moduleId] 中,而后调用installedChunks[chunkId][0]()


installedChunks[chunkId][0]()是什么呢?在之前的 __webpack_require__.f.j() 中,咱们注册了对应的installedChunks[chunkId] = [resolve, reject];

__webpack_require__.f.j = function (chunkId, promises) {
    // ....
    var promise = new Promise(function (resolve, reject) {installedChunkData = installedChunks[chunkId] = [resolve, reject]; 
    });
    promises.push(installedChunkData[2] = promise);
    __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
};

这个时候咱们又能够回到一开始的 __webpack_require__.e__webpack_require__.f 加载对应的 chunk.js 后,会扭转 promises 的状态,从而触发 __webpack_require__.bind(__webpack_require__, "./src/async/async_B.js") 的执行,而此时的 "./src/async/async_B.js" 曾经在之前的 jsonp 申请中存入到 __webpack_require__.m[moduleId],因而这里就能够间接获取对应的内容实现异步chunk 的调用

__webpack_require__.e = function (chunkId) {return Promise.all(Object.keys(__webpack_require__.f).reduce(function (promises, key) {__webpack_require__.f[key](chunkId, promises);
        return promises;
    }, []));
};
__webpack_require__.e(/*! import() | B */ "B").then(__webpack_require__.bind(__webpack_require__, /*! ./async/async_B.js */ "./src/async/async_B.js")
).then(bModule => {bModule.default();
});

6.3.3 其它细小知识点

而在 webpack 官网中,咱们从 https://webpack.docschina.org/concepts/manifest/ 能够晓得

一旦你的利用在浏览器中以 index.html 文件的模式被关上,一些 bundle 和利用须要的各种资源都须要用某种形式被加载与链接起来。在通过打包、压缩、为提早加载而拆分为细小的 chunk 这些 webpack 优化 之后,你精心安排的 /src 目录的文件构造都曾经不再存在。所以 webpack 如何治理所有所需模块之间的交互呢?

compiler 开始执行、解析和映射应用程序时,它会保留所有模块的具体要点。这个数据汇合称为 "manifest"

当实现打包并发送到浏览器时,runtime 会通过 manifest 来解析和加载模块,也就是说在浏览器运行时,runtimewebpack 用来连贯模块化的应用程序的所有代码

runtime蕴含:在模块交互时,连贯模块所需的加载和解析逻辑

runtime包含:浏览器中的已加载模块的连贯,以及懒加载模块的执行逻辑

6.4 runtime 代码与 module、chunk 的关联

6.4.1 计算:runtimeRequirements的初始化

runtime蕴含很多工具办法,一个 Chunk 怎么晓得它须要什么工具办法?比方一个 Chunk 只有同步,没有异步,天然不会生成异步 runtime 的代码

在下面 HarmonyImportDependency.Template 的剖析咱们能够晓得,生成代码时,会触发dep.getImportStatement(),理论就是RuntimeTemplate.importStatement()

//node_modules/webpack/lib/dependencies/HarmonyImportDependency.js
HarmonyImportDependency.Template = class HarmonyImportDependencyTemplate extends (ModuleDependency.Template) {apply(dependency, source, templateContext) {const importStatement = dep.getImportStatement(false, templateContext);
        //...
    }
}
getImportStatement() {
    return runtimeTemplate.importStatement({
        update,
        module: moduleGraph.getModule(this),
        chunkGraph,
        importVar: this.getImportVar(moduleGraph),
        request: this.request,
        originModule: module,
        runtimeRequirements
    });
}

RuntimeTemplate.importStatement() 中,会生成理论的代码,比方 /* harmony import */__webpack_require__(xxxx) 等等

// node_modules/webpack/lib/RuntimeTemplate.js
importStatement() {
    const optDeclaration = update ? "":"var ";

    const exportsType = module.getExportsType(
        chunkGraph.moduleGraph,
        originModule.buildMeta.strictHarmonyModule
    );
    runtimeRequirements.add(RuntimeGlobals.require);
    const importContent = `/* harmony import */ ${optDeclaration}${importVar} = __webpack_require__(${moduleId});\n`;

    if (exportsType === "dynamic") {runtimeRequirements.add(RuntimeGlobals.compatGetDefaultExport);
        return [
            importContent,
            `/* harmony import */ ${optDeclaration}${importVar}_default = /*#__PURE__*/${RuntimeGlobals.compatGetDefaultExport}(${importVar});\n`
        ];
    }
    return [importContent, ""];
}

下面代码对应的实质就是 webpack 生成具体 module 代码时,entry4.js中外部 import 的替换语句

在下面转化语句的流程中,咱们也将对应的 runtime 模块须要的代码放入到 runtimeRequirements 中,比方

runtimeRequirements=["__webpack_require__", "__webpack_require__.n"]

下面的剖析是同步的 /* harmony import */ 流程剖析,那如果是异步的 import 呢?

这里咱们能够应用倒推法,咱们能够发现,实质 runtimeRequirements.push() 都是 RuntimeGlobals 这个变量的值,所以咱们能够从下面的打包产物中,找到异步所须要的 runtime 办法:__webpack_require__.e,就能够轻易找到对应的变量为RuntimeGlobals.ensureChunk,而后就能够轻易找到对应的代码所在位置,进行剖析

因而咱们能够很轻松明确整个异步 import 计算 runtime 依赖的流程,通过 sourceBlock()->sourceDependency()-> 触发对应的ImportDependency 对应的ImportDependency.Template

从而触发 runtimeTemplate.moduleNamespacePromise() 实现代码的转化以及 runtimeRequirements 的计算

6.4.2 依赖收集:runtimeRequirements 和 module

在经验 module.codeGeneration 生成模块代码,并且顺便创立 runtimeRequirements 的流程后,会调用 processRuntimeRequirements() 进行 runtimeRequirements 的解决,将数据放入到 chunkGraph

this.codeGeneration(err => {
    //...module.codeGeneration
    this.processRuntimeRequirements();}
processRuntimeRequirements() {for(const module of modules) {for (const runtime of chunkGraph.getModuleRuntimes(module)) {
            const runtimeRequirements =
                codeGenerationResults.getRuntimeRequirements(module, runtime);
            if (runtimeRequirements && runtimeRequirements.size > 0) {set = new Set(runtimeRequirements);
            }
            chunkGraph.addModuleRuntimeRequirements(module, runtime, set);
        }
    }
    //...
}

6.4.3 依赖收集:runtimeRequirements 和 chunk

chunk 为单位,遍历所有该 chunk 中蕴含的 module 所依赖的 runtimeRequirements,而后应用const set=new Set() 进行去重,最终将 chunkruntimeRequirements的关系放入到 chunkGraph

this.hooks.additionalChunkRuntimeRequirements.call(chunk, set, context)触发的逻辑:判断是否须要为 set 汇合数据增加对应的item

processRuntimeRequirements() {
    //... 解决 module 和 runtimeRequirements

    for (const chunk of chunks) {const set = new Set();
        for (const module of chunkGraph.getChunkModulesIterable(chunk)) {
            const runtimeRequirements = chunkGraph.getModuleRuntimeRequirements(
                module,
                chunk.runtime
            );
            for (const r of runtimeRequirements) set.add(r);
        }
        this.hooks.additionalChunkRuntimeRequirements.call(chunk, set, context);

        for (const r of set) {this.hooks.runtimeRequirementInChunk.for(r).call(chunk, set, context);
        }

        chunkGraph.addChunkRuntimeRequirements(chunk, set);
    }
}

6.4.4 依赖收集:runtimeRequirementschunkGraphEntries

processRuntimeRequirements() {
    //... 解决 module 和 runtimeRequirements
    //... 解决 chunk 和 runtimeRequirements
    
    for (const treeEntry of chunkGraphEntries) {const set = new Set();
        for (const chunk of treeEntry.getAllReferencedChunks()) {
            const runtimeRequirements =
                chunkGraph.getChunkRuntimeRequirements(chunk);
            for (const r of runtimeRequirements) set.add(r);
        }
        this.hooks.additionalTreeRuntimeRequirements.call(
            treeEntry,
            set,
            context
        );
        for (const r of set) {
            this.hooks.runtimeRequirementInTree
                .for(r)
                .call(treeEntry, set, context);
        }
        chunkGraph.addTreeRuntimeRequirements(treeEntry, set);
    }
}

chunkGraphEntries是什么?

chunkGraphEntries = this._getChunkGraphEntries()

获取所有同步 entry 和异步 entry 所具备的 runtimeChunk,而后都放入到treeEntries 这个汇合中去

webpack5容许将 runtime 代码剥离进去造成 runtimeChunk,而后提供给多个chunk 一起应用

_getChunkGraphEntries() {/** @type {Set<Chunk>} */
    const treeEntries = new Set();
    for (const ep of this.entrypoints.values()) {const chunk = ep.getRuntimeChunk();
        if (chunk) treeEntries.add(chunk);
    }
    for (const ep of this.asyncEntrypoints) {const chunk = ep.getRuntimeChunk();
        if (chunk) treeEntries.add(chunk);
    }
    return treeEntries;
}
hooks.additionalTreeRuntimeRequirements.call

触发多个 Plugin 的监听,为 set 汇合减少item

hooks.runtimeRequirementInTree.call
for (const r of set) {
    this.hooks.runtimeRequirementInTree
        .for(r)
        .call(treeEntry, set, context);
}

依据目前的 runtimeRequirements,即RuntimeGlobals.ensureChunk = "__webpack_require__.e" 触发对应的逻辑解决

比方上图所示,咱们在 module.codeGeneration() 能够替换对应的代码为 "__webpack_require__.e",在下面的runtime 代码的品种和作用 中,咱们晓得这是代表异步申请的办法,然而咱们只替换了源码,如下代码块所示

__webpack_require__.e(/*! import() | B */ "B").then(__webpack_require__.bind(__webpack_require__, /*! ./async/async_B.js */ "./src/async/async_B.js")
).then(bModule => {bModule.default();
});

咱们还须要生成 _webpack_require__.e 办法的具体内容,即对应的 runtime 工具办法须要生成,如下所示

__webpack_require__.e = function (chunkId) {return Promise.all(Object.keys(__webpack_require__.f).reduce(function (promises, key) {__webpack_require__.f[key](chunkId, promises);
        return promises;
    }, []));
};

因而咱们通过 this.hooks.runtimeRequirementInTree 解决这种状况,如下面例子所示,咱们最终应用 compilation.addRuntimeModule() 减少了一个代码模块,在前面的代码生成中,咱们就能够触发 EnsureChunkRuntimeModule.jsgenerate()进行对应代码的生成,如下图所示

它所对应的就是咱们打包后 webpack/runtime/ensure chunk 的内容

chunkGraph.addTreeRuntimeRequirements
addTreeRuntimeRequirements(chunk, items) {const cgc = this._getChunkGraphChunk(chunk);
    const runtimeRequirements = cgc.runtimeRequirementsInTree;
    for (const item of items) runtimeRequirements.add(item);
}

6.4.5 代码生成:合并到其它 chunk

在下面的 this.hooks.runtimeRequirementInTree.call 的剖析中,咱们能够晓得,会触发compilation.addRuntimeModule

//node_modules/webpack/lib/RuntimePlugin.js
compilation.hooks.runtimeRequirementInTree
    //__webpack_require__.e
    .for(RuntimeGlobals.ensureChunk)
    .tap("RuntimePlugin", (chunk, set) => {const hasAsyncChunks = chunk.hasAsyncChunks();
        if (hasAsyncChunks) {
            //__webpack_require__.f
            set.add(RuntimeGlobals.ensureChunkHandlers);
        }
        compilation.addRuntimeModule(
            chunk,
            new EnsureChunkRuntimeModule(set)
        );
        return true;
    });

实质上就是把某一个 runtime 的办法当作 ModuleChunk进行关联,而后在 chunk 生成代码时,会将 chunk 蕴含的 modules,包含这些runtimemodules进行代码的生成

// node_modules/webpack/lib/Compilation.js
addRuntimeModule(chunk, module, chunkGraph = this.chunkGraph) {
    // Deprecated ModuleGraph association
    if (this._backCompat)
        ModuleGraph.setModuleGraphForModule(module, this.moduleGraph);

    // add it to the list
    this.modules.add(module);
    this._modules.set(module.identifier(), module);

    // connect to the chunk graph
    chunkGraph.connectChunkAndModule(chunk, module);
    chunkGraph.connectChunkAndRuntimeModule(chunk, module);
    if (module.fullHash) {chunkGraph.addFullHashModuleToChunk(chunk, module);
    } else if (module.dependentHash) {chunkGraph.addDependentHashModuleToChunk(chunk, module);
    }
    //.......
}

而真正生成 runtime 的中央是在生成代码时获取对应 chunkruntimeModule进行代码生成,比方下图的 renderMain() 中,咱们能够拿到一个 runtimeModules,仔细观察其实每一个Module 就是一个工具办法,每一个 Module 生成的 PrefixSource 就是理论工具办法的内容


chunk对应的 modules 代码生成如下所示

chunk对应的 runtime Modules 生成的代码就是下面剖析的各种工具办法的代码

如果配置了 runtime 造成独立的 chunk,实质也是应用 chunk 对应的 runtime Modules 生成代码

6.4.6 代码生成:runtime 造成独立的 chunk

webpack.config.js 配置和 entry 打包内容展现

webpack5容许在 optimization 配置对应的参数,比方上面代码块配置 name: 'runtime',能够生成一个runtimeChunk,多个Chunk 就能够共用生成的runtime.js

webpack5也容许依据不同的 Chunk 抽离出对应的runtimeChunk

module.exports = {
  optimization: {
    runtimeChunk: {name: 'runtime',},
    chunkIds: "named",
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: 10,
      maxAsyncRequests: 10,
      cacheGroups: {
        test3: {
          chunks: 'all',
          minChunks: 3,
          name: "test3",
          priority: 3
        },
        test2: {
          chunks: 'all',
          minChunks: 2,
          name: "test2",
          priority: 2,
          maxSize: 50
        }
      }
    }
  }
}

当配置生成 runtimeChunk 时,入口类型 Chunk 的代码渲染办法会从 renderMain() 变为 renderChunk(),如上面截图所示,renderChunk() 最显著的特点是整个文件会应用全局的变量进行 push 操作

renderChunk()能够参考下面的剖析,逻辑也比较简单,这里不再赘述

造成新的Chunk

简略剖析是如何造成新的runtimeChunk

初始化时会判断是否有配置 options.optimization.runtimeChunk,而后确定runtime 的名称

//node_modules/webpack/lib/WebpackOptionsApply.js
if (options.optimization.runtimeChunk) {const RuntimeChunkPlugin = require("./optimize/RuntimeChunkPlugin");
    new RuntimeChunkPlugin(options.optimization.runtimeChunk).apply(compiler);
}

//node_modules/webpack/lib/optimize/RuntimeChunkPlugin.js
apply(compiler) {
    compiler.hooks.thisCompilation.tap("RuntimeChunkPlugin", compilation => {
        compilation.hooks.addEntry.tap(
            "RuntimeChunkPlugin",
            (_, { name: entryName}) => {if (entryName === undefined) return;
                const data = compilation.entries.get(entryName);
                if (data.options.runtime === undefined && !data.options.dependOn) {
                    // Determine runtime chunk name
                    let name = this.options.name;
                    if (typeof name === "function") {name = name({ name: entryName});
                    }
                    data.options.runtime = name;
                }
            }
        );
    });
}

seal 阶段的初始化阶段,只有配置 options.optimization.runtimeChunk 才会触发上面增加 this.addChunk(runtime) 的逻辑

同步入口和异步依赖都会调用 this.addChunk() 办法

seal(callback) {for (const [name, { dependencies, includeDependencies, options}] of this
        .entries) {const chunk = this.addChunk(name);
        //... 解决失常入口文件为 chunk
    }

    outer: for (const [
        name,
        {options: { dependOn, runtime}
        }
    ] of this.entries) {if (dependOn) {//....} else if (runtime) {const entry = this.entrypoints.get(name);
            let chunk = this.namedChunks.get(runtime);
            if (chunk) {//...} else {chunk = this.addChunk(runtime);
                chunk.preventIntegration = true;
                runtimeChunks.add(chunk);
            }
            entry.unshiftChunk(chunk);
            chunk.addGroup(entry);
            entry.setRuntimeChunk(chunk);
        }
    }
    buildChunkGraph(this, chunkGraphInit);
}
runtimeChunk 生成代码流程

正如下面依赖收集:runtimeRequirementschunkGraphEntries 的剖析一样,如果有配置 options.optimization.runtimeChunk,则chunkGraphEntries 为配置的runtimeChunk

如果没有配置,那么 chunkGraphEntries 就是入口文件自身,比方 entry4.js 造成 app4 这个 Chunk,那么此时chunkGraphEntries 就是 app4 这个Chunk

最终都会触发 hooks.runtimeRequirementInTree 进行 compilation.addRuntimeModule(chunk, module) 减少了一个代码模块,如果有配置 runtimeChunk,那么此时chunk 就是runtimeChunk,否则就是app4 Chunk

processRuntimeRequirements() {
    //... 解决 module 和 runtimeRequirements
    //... 解决 chunk 和 runtimeRequirements
    
    for (const treeEntry of chunkGraphEntries) {const set = new Set();
        for (const chunk of treeEntry.getAllReferencedChunks()) {
            const runtimeRequirements =
                chunkGraph.getChunkRuntimeRequirements(chunk);
            for (const r of runtimeRequirements) set.add(r);
        }
        this.hooks.additionalTreeRuntimeRequirements.call(
            treeEntry,
            set,
            context
        );
        for (const r of set) {
            this.hooks.runtimeRequirementInTree
                .for(r)
                .call(treeEntry, set, context);
        }
        chunkGraph.addTreeRuntimeRequirements(treeEntry, set);
    }
}

因而无论有无配置 options.optimization.runtimeChunk,最终代码runtime 代码始终都是上面的代码块,即 runtimeChunk 实质触发的是 renderMain() 办法生成代码

renderMain() {
    //...
    const runtimeModules =
        renderContext.chunkGraph.getChunkRuntimeModulesInOrder(chunk);

    if (runtimeModules.length > 0) {
        source.add(
            new PrefixSource(
                prefix,
                Template.renderRuntimeModules(runtimeModules, chunkRenderContext)
            )
        );
    }
    //...
}

参考

  1. 「万字进阶」深入浅出 Commonjs 和 Es Module
  2. ES6 模块和 CommonJS 模块有哪些差别?
  3. 阮一峰 -Node.js 如何解决 ES6 模块
  4. webpack 打包后运行时文件剖析
  5. 精通 Webpack 外围原理专栏
  6. webpack@4.46.0 源码剖析 专栏

其它工程化文章

  1. 「Webpack5 源码」热更新 HRM 流程浅析
  2. 「Webpack5 源码」make 阶段(流程图)剖析
  3. 「Webpack5 源码」enhanced-resolve 门路解析库源码剖析
  4. 「Webpack5 源码」seal 阶段(流程图)剖析(一)
  5. 「Webpack5 源码」seal 阶段剖析(二)-SplitChunksPlugin 源码
  6. 「vite4 源码」dev 模式整体流程浅析(一)
  7. 「vite4 源码」dev 模式整体流程浅析(二)
正文完
 0