本文内容基于
webpack 5.74.0
版本进行剖析因为
webpack5
整体代码过于简单,为了缩小复杂度,本文所有剖析将只基于js
文件类型进行剖析,不会对其它类型(css
、image
)进行剖析,所举的例子也都是基于js
类型
为了减少可读性,会对源码进行删减、调整程序、扭转的操作,文中所有源码均可视作为伪代码
前言
本文是webpack5外围流程
解析的最初一篇文章,共有5篇,应用流程图的模式从头到尾剖析了webpack5外围流程
:
- 「Webpack5源码」make阶段(流程图)剖析
- 「Webpack5源码」enhanced-resolve门路解析库源码剖析
- 「Webpack5源码」seal阶段(流程图)剖析(一)
- 「Webpack5源码」seal阶段剖析(二)-SplitChunksPlugin源码
- 「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/auto
、css
等不同类型的NormalModule
进行不同parser
和generator
的初始化
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.dependencies
和module.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
,接下来咱们将剖析template
、dependencyTemplates
到底是什么?
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.js
的apply
办法中,提前注册了多个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()
就是触发对应dependency
的template.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
实质就是ReplaceSource
,insert()
是将要替换的代码的范畴以及内容都放在_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.dependency
和module.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.replace
,source
实质就是ReplaceSource
,replace()
是将要替换的代码的范畴以及内容都放在_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)
跟parser
、generator
、dependencyTemplates
相似,这里也采纳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 ,则提早加载入口文件 | 无 |
整体流程图文字描述
依照指定程序合并代码
- 合并立刻执行函数的最头部的代码,一个
(()=> {
- 合并所有
module
的代码 - 合并
__webpack_module_cache__ = {};function __webpack_require__(moduleId)
等加载代码 - 合并
runtime
模块代码 - 合并
"Load entry module and return exports"
等代码,即立刻执行函数中的执行入口文件解析的代码局部,被动触发入口文件的加载 - 合并立刻执行函数的最初最初的代码,即
})()
因为计算代码和合并代码逻辑揉杂在一起,因而调整了上面代码程序,加强可读性
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()
函数生成所有modules
的source
,实质还是利用codeGeneration()
中生成的codeGenerationResults
获取对应module
的source
留神!要渲染的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__.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.js
中import ./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 export
的key
,而后__webpack_require__("./src/es_b.js")
调用es_b
模块,进入"./src/es_b.js"
"./src/es_b.js"
:申明了对应的harmony export
的key
,而后__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
获取对应key
的function()
,因而能够实时拿到变动的值
"./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.js
:a.js
->b.js
->a.js
后回到entry2.js
,将a
模块的一个变量module.exports.a_Value
和b
模块的一个变量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.html
,webpack
会帮咱们造成上面的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.js
中push
的数据
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
来解析和加载模块,也就是说在浏览器运行时,runtime
是webpack
用来连贯模块化的应用程序的所有代码
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()
进行去重,最终将chunk
和runtimeRequirements
的关系放入到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 依赖收集:runtimeRequirements
和chunkGraphEntries
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.js
的generate()
进行对应代码的生成,如下图所示
它所对应的就是咱们打包后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
的办法当作Module
与Chunk
进行关联,而后在chunk
生成代码时,会将chunk
蕴含的modules
,包含这些runtime
的modules
进行代码的生成
// 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
的中央是在生成代码时获取对应chunk
的runtimeModule
进行代码生成,比方下图的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生成代码流程
正如下面依赖收集:runtimeRequirements
和chunkGraphEntries
的剖析一样,如果有配置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) ) ); } //...}
参考
- 「万字进阶」深入浅出 Commonjs 和 Es Module
- ES6模块和CommonJS模块有哪些差别?
- 阮一峰-Node.js 如何解决 ES6 模块
- webpack打包后运行时文件剖析
- 精通 Webpack 外围原理专栏
- webpack@4.46.0 源码剖析 专栏
其它工程化文章
- 「Webpack5源码」热更新HRM流程浅析
- 「Webpack5源码」make阶段(流程图)剖析
- 「Webpack5源码」enhanced-resolve门路解析库源码剖析
- 「Webpack5源码」seal阶段(流程图)剖析(一)
- 「Webpack5源码」seal阶段剖析(二)-SplitChunksPlugin源码
- 「vite4源码」dev模式整体流程浅析(一)
- 「vite4源码」dev模式整体流程浅析(二)