本文内容基于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.jsconst 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.jsapply(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.jsparser.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.jsparse(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.jsparser.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.jsreplace(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()办法
// JavascriptModulesPlugincompilation.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.jsseal() {    //...依据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.jscreateChunkAssets(callback) {  this.assets[file] = source;}

Compiler.emitAsset

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

// node_modules/webpack/lib/Compiler.jsrun(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__.Ochunk 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模块有哪些差别?
CommonJSES6
语法应用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 delayedvar __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.jsvar 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.jsHarmonyImportDependency.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.jsimportStatement() {    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.jscompilation.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.jsaddRuntimeModule(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.jsif (options.optimization.runtimeChunk) {    const RuntimeChunkPlugin = require("./optimize/RuntimeChunkPlugin");    new RuntimeChunkPlugin(options.optimization.runtimeChunk).apply(compiler);}//node_modules/webpack/lib/optimize/RuntimeChunkPlugin.jsapply(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模式整体流程浅析(二)