通过一个demo带你深入进入webpack@4.46.0
源码的世界,分析构建原理,专栏地址,共有十篇。
- 1. 从构建前后产物对比分析webpack做了些什么?
- 2. webpack构建的基石: tapable@1.1.3源码分析
- 3. webpack构建整体流程的组织:webpack -> Compiler -> Compilation
- 4. 创建模块实例,为模块解析准备
- 5. 路径解析:enhanced-resolve@4.5.0源码分析
- 6. 模块构建之loader执行:loader-runner@2.4.0源码分析
- 7. 模块构建之解析_source获取dependencies
- 8. 从dependency graph 到 chunk graph
- 9. 从chunk到最终的文件内容到最后的文件输出?
- 10. webpack中涉及了哪些设计模式呢?
compilation.seal
中最终的两件大事除了上面两个小结说的dependency graph -> chunk graph
,另外就是文件内容的生成。文件信息(内容和大小等)存储在compilation.assets
上,在compilation.emitAsset
方法就是用来将文件信息(名称和内容等)缓存到compilation.assets
属性上。
// Compilation.js
emitAsset(file, source, assetInfo = {}) {
//...
this.assets[file] = source;
this.assetsInfo.set(file, assetInfo);
}
在compilation.seal
中有两个和文件内容生成相关的方法:createModuleAssets
和createChunkAssets
createModuleAssets
处理单个模块在构建过程中由额外生成的文件。在normalModule.doBuild调用runLoaders方法之前会先调用createLoaderContext
创建的上下文,该上下文对象包含emitFile
方法,在loader执行阶段时可以调用该方法来输出文件内容(此时只是缓存到module.buildInfo.assets/assetsInfo
属性上),比如file-loader就会使用该方法来输出文件。
// NormalModule.js
// doBuild -> createLoaderContext -> runLoaders
createLoaderContext(resolver, options, compilation, fs) {
const loaderContext = {
//...
emitFile: (name, content, sourceMap, assetInfo) => {
//...
this.buildInfo.assets[name] = this.createSourceForAsset(...);
this.buildInfo.assetsInfo.set(name, assetInfo);
},
}
return loaderContext;
}
而createModuleAssets
就是用来处理module.buildInfo.assets
的,将模块上缓存的文件信息迁移通过调用compilation.assets
上。
// Compilation.js
createModuleAssets() {
for (let i = 0; i < this.modules.length; i ) {
const module = this.modules[i];
if (module.buildInfo.assets) {
const assetsInfo = module.buildInfo.assetsInfo;
for (const assetName of Object.keys(module.buildInfo.assets)) {
//...
this.emitAsset(...);
}
}
}
}
小结
- hooks.make阶段:normalModule.doBuild -> runLoaders:loader函数可能会调用emitFile将文件信息存储到module.buildInfo.assets上
- compilation.seal阶段:createModuleAssets -> emitAsset:将module.buildInfo.assets转移到compilation.assets上
createChunkAssets
来到Compilation中的createChunkAssets
方法,该方法中看到两个核心属性:mainTemplate
和chunkTemplate
,chunkTemplate
根据chunk中包含的模块信息来生成最终该chunk对应输出js文件的内容,而mainTemplate
具有chunkTemplate
能力之外还具有生成运行时runtime代码的能力。
而mainTemplate
和chunkTemplate
是在Compilation构造函数中赋值的,分别对应了MainTemplate和ChunkTemplate.
Compilation.js
代码语言:javascript复制// constuctor 构造函数
this.mainTemplate = new MainTemplate(this.outputOptions);
this.chunkTemplate = new ChunkTemplate(this.outputOptions);
// createChunkAssets方法
createChunkAssets(){
//...
for (let i = 0; i < this.chunks.length; i ) {
const chunk = this.chunks[i];
chunk.files = [];
//...
try {
const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate;
// manifest结构:[{ render(), filenameTemplate, pathOptions, identifier, hash }]
const manifest = template.getRenderManifest(...);
for (const fileManifest of manifest) {
//... 参数准备、缓存相关逻辑(alreadyWrittenFiles、this.cache等处理)
source = fileManifest.render();
this.emitAsset(file, source, assetInfo);
chunk.files.push(file);
//...
}
} catch (err) //...
}
}
// MainTemplate、ChunkTemplate
getRenderManifest(options) {
const result = [];
this.hooks.renderManifest.call(result, options);
return result;
}
进入createChunkAssets
看到首先是遍历所有的chunks生成每个chunk的最终内容,对每一个chunk都会调用emitAsset()
将内容缓存到compilation.assets
上(这里会调用getPathWithInfo
根据options.out
的配置来生成文件路径,如main.js
在这里返回chunkMain.js
,这里就不深入介绍了)
首先根据当前chunk是否包含运行时来获取相应的template,如果hasRuntime()
返回true说明需要给该chunk生成运行时代码此时使用mainTemplate
,否则使用chunkTemplate
。调用template的getRenderManifest方法实际是调用hooks.renderManifest.call
来获取代码生成方法和信息。在JavascriptModulesPlugin
注册了该钩子,关注render
方法
// JavascriptModulesPlugin.js
compilation.mainTemplate.hooks.renderManifest.tap("JavascriptModulesPlugin", (result, options) => {
//...
result.push({ render: () => compilation.mainTemplate.render(...), ... });
return result;
}
)
compilation.chunkTemplate.hooks.renderManifest.tap("JavascriptModulesPlugin", (result, options) => {
//...
result.push({ render: () => this.renderJavascript(...), ... });
return result;
}
)
hasRuntime()
取决于当前chunk所在chunkGroup是否是EntryPoint
,并且该EntryPoint
中的runtimeChunk
属性是否指向当前chunk。比如在compilation.seal开始部分的for循环构造EntryPoint
逻辑时生成的初始chunk就是runtimeChunk
,此时的含义是该chunk最终生成文件中需要包含运行时代码。如果webpack.config.simple.js中配置了optimization.runtimeChunk
则会注册RuntimeChunkPlugin
,该插件会新生成一个chunk用来单独存储runtime的代码并给entryPoint设置新的runtimeChunk指向到该新chunk(entrypoint.setRuntimeChunk(newChunk);
),而原先的chunk则不会包含runtime代码;并且此时也会建立这个新chunk和entryPoint的关系(初始情况下一个chunkGroup只会包含一个chunk,但这里的entryPoint会包含两个,多出的实际是从原先的chunk拆分出来的)。
获取代码生成的方法和信息后,调用fileManifest.render();
生成chunk最终的输出内容,生成完内容后调用compilation.emitAsset
将内容缓存到compilation.assets
中。
由于mainTemplate.render
逻辑中会包含chunkTemplate
中的逻辑,下面仅分析mainTemplate.render
的执行过程
下面会用到的几个插件的注册:JsonpMainTemplatePlugin、JsonpChunkTemplatePlugin、FunctionModuleTemplatePlugin
代码语言:javascript复制// WebpackOptionsApply.js
new JsonpTemplatePlugin().apply(compiler);
// JsonpTemplatePlugin.js
// JsonpTemplatePlugin.apply
compiler.hooks.thisCompilation.tap("JsonpTemplatePlugin", compilation => {
new JsonpMainTemplatePlugin().apply(compilation.mainTemplate);
new JsonpChunkTemplatePlugin().apply(compilation.chunkTemplate);
//...
});
// FunctionModulePlugin.js
compiler.hooks.compilation.tap("FunctionModulePlugin", compilation => {
new FunctionModuleTemplatePlugin().apply(...);
});
下面看下Chunk(name = 'chunkMain')生成内容(对应产物chunkMain.js
)的过程,先看下大致的逻辑。
看到这里一共有五种颜色区分,主要分为三大块
- 运行代码的生成:这部分逻辑在
mainTemplate.render
方法中,该方法包含两个部分:
- `mainTemplate.renderBootstrap`:生成了运行时代码(右侧灰色部分的代码);
- `mainTemplate.hooks.render.call`:给运行时代码套一层iife的壳子(右侧粉色部分的代码):(function(modules){ ... }())经过buildChunkGraph的努力,Chunk(name = 'chunkMain')包含了三个模块,分别是main.js、a.js、c.js。每个模块的代码生成是在
代码语言:txt复制- `module.source()`(如normalModule.source()):生成单个模块的代码
- `hooks.render.call` => `FunctionModuleTemplatePlugin`订阅了该钩子,作用是套一层`function`壳子,配合上面的运行时用于该模块的注册,可以认为是当前模块的定义
mainTemplate.render
代码语言:javascript复制// MainTemplate.js
render(hash, chunk, moduleTemplate, dependencyTemplates) {
const buf = this.renderBootstrap(...);
let source = this.hooks.render.call(
new OriginalSource(
Template.prefix(buf, " t") "n",
"webpack/bootstrap"
), ...);
//...
chunk.rendered = true;
return new ConcatSource(source, ";");
}
// constructor
this.hooks.render.tap("MainTemplate", (...) => {
const source = new ConcatSource();
source.add("/******/ (function(modules) { // webpackBootstrapn");
source.add(new PrefixSource("/******/", bootstrapSource));
source.add("/******/ })n");
source.add(
"/************************************************************************/n"
);
source.add("/******/ (");
source.add(this.hooks.modules.call(...));
source.add(")");
return source;
}
);
// JavascriptModulesPlugin.js
compilation.mainTemplate.hooks.modules.tap("JavascriptModulesPlugin", (...) => {
return Template.renderChunkModules(chunk, ...);
}
);
mainTemplate.render
方法的主要过程如下
- 调用
renderBootstrap
方法生成运行时(runtime)代码(不深入分析了,主要是运行时代码的连接,涉及的核心插件是;JsonpMainTemplatePlugin) - 然后通过钩子
hooks.render.call -> hooks.modules.call -> Template.renderChunkModules
生成该chunk中所有模块的定义,这里的hooks.render.tap
回调将运行时代码和模块代码进行组合。
Template.renderChunkModules 模块代码生成的入口
下面分析Template.renderChunkModules
方法
// Template.js
static renderChunkModules(...) {
const source = new ConcatSource();
const modules = chunk.getModules().filter(filterFn);
//... removedModules 逻辑
const allModules = modules.map(module => {
return {
id: module.id,
source: moduleTemplate.render(module, dependencyTemplates, { chunk })
};
});
//... removedModules 逻辑
const bounds = Template.getModulesArrayBounds(allModules);
if (bounds) {
// Render a spare array
const minId = bounds[0];
const maxId = bounds[1];
//...
source.add("[n");
const modules = new Map();
for (const module of allModules) {
modules.set(module.id, module);
}
for (let idx = minId; idx <= maxId; idx ) {
const module = modules.get(idx);
//...
source.add(`/* ${idx} */`);
if (module) {
source.add("n");
source.add(module.source);
}
}
source.add("n" prefix "]");
//...
} else {
// Render an object
source.add("{n");
allModules.sort(stringifyIdSortPredicate).forEach((module, idx) => {
if (idx !== 0) {
source.add(",n");
}
source.add(`n/***/ ${JSON.stringify(module.id)}:n`);
source.add(module.source);
});
source.add(`nn${prefix}}`);
}
return source;
}
步骤如下
获取有实际内容的所有模块modules
,filterFn: m => typeof m.source === "function"
,因为会通过·module.source()来获取模块的内容,父类Module
没有实现,子类如NormalModule
实现了该方法,因此需要判断模块实例是否实现了该方法。
遍历modules,调用moduleTemplate.render
获取单个模块的代码,得到allModules
将allModules
中的信息串起来,首先调用getModulesArrayBounds
方法获取allModules
中moduleId的边界(上边界,下边界,如0,100),有可能不产生边界比如有可能moduleId不是number
类型。边界的目的是为了构造一个稀疏数组,moduleId表示数组索引,对应的值则是模块的定义;而对于没有边界的情况,如果没有边界则通过一个对象来装载。上面的if-else就是区分这两种情况的。比如当前案例中的chunk(name = 'chunkMain')在这里的效果如下:
[/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
// 模块main.js的构建后的内容
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
// 模块a.js的构建后的内容
/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
// 模块c.js的构建后的内容
/***/ })
/******/ ]
ModuleTemplate.render 单个模块内容生成
下面看下ModuleTemplate.render如何生成单个模块的内容的。
代码语言:javascript复制render(module, dependencyTemplates, options) {
const moduleSource = module.source(...;
// hooks.content、hooks.module
const moduleSourcePostRender = this.hooks.render.call(...);
return this.hooks.package.call(...);
}
首先是调用module.source()
方法获取原始文件经过转换后的内容。另外这里添加一些hooks让再次修改模块的内容提供时机。这里关注hooks.render,注意截止这里已经出现多次hooks.render,但是挂载的实例是不同的,比如这里是moduleTemplate
,FunctionModuleTemplatePlugin
中监听了该钩子,目的是给原始模块套一层function
的壳子,以配合webpack自己的模块化(runtime)机制来保证模块正常加载。
// FunctionModuleTemplatePlugin.js
apply(moduleTemplate) {
moduleTemplate.hooks.render.tap("FunctionModuleTemplatePlugin", (moduleSource, module) => {
// ...
source.add("/***/ (function(" args.join(", ") ") {nn");
// ....
source.add(moduleSource);
source.add("nn/***/ })");
}
)
实际效果可能如下:
代码语言:javascript复制/***/ (function(module, __webpack_exports__, __webpack_require__) {
// 原始模块转换后的代码: moduleSource
/***/ }),
normalModule.source() 获取原始文件转换后的内容
代码语言:javascript复制// NormalModule.js
source(dependencyTemplates, runtimeTemplate, type = "javascript") {
//... 缓存逻辑,如果有缓存并且hashDigest未变化则直接返回
const source = this.generator.generate(...);
//...
return cachedSource; // new CachedSource
}
在normalModueFactory.create创建normalModule实例时有this.generator = new JavascriptGenerator()
// JavascriptGenerator.js
generate(module, dependencyTemplates, runtimeTemplate) {
const originalSource = module.originalSource();
const source = new ReplaceSource(originalSource);
this.sourceBlock(...);
return source;
}
normalModule.originalSource()
返回_source
,即在normalModule.doBuild
构建模式时调用runLoaders
返回的容;注意这里通过ReplaceSource
对originalSource
进行了包装。
// JavascriptGenerator.js
sourceBlock(module, block, ... ) {
for (const dependency of block.dependencies) {
this.sourceDependency(...);
}
// ... 变量注入(block.variables相关逻辑)
for (const childBlock of block.blocks) {
this.sourceBlock(module, childBlock, ...);
}
}
sourceDependency(dependency, dependencyTemplates, source, runtimeTemplate) {
const template = dependencyTemplates.get(dependency.constructor);
//... 异常处理
template.apply(dependency, source, runtimeTemplate, dependencyTemplates);
}
三个部分,分别对应DependeciesBlock
的三个属性的处理(dependencies
、variables
、blocks
),比如本文案例中的main.js
的dependencies
,blocks
都是有值的。
遍历dependencies
并调用sourceDependency() -> template.apply()
应用依赖的模板修改模块的原始内容,后面会分析main.js
中dependencies
是如何修改原始内容。收集的XxxDependency
比如HarmonyImportSpecifierDependency
都会有一个与之对应的Template
类,该类提供了apply
方法会被sourceDependency
调用。看到sourceDependency
中有使用dependencyTemplates
,通过该属性来获取依赖关联的模板,和dependencyFactories
使用方式类似,下面举个具体的例子来说明XxxDependency.Template
。dependencyTemplates
用来存储XxxDependency
关联的template
,前面在介绍parser.parse()
部分提过依赖收集的相关的插件如HarmonyModulesPlugin
,在类似插件的构造函数中会设置依赖到模板的映射,如下例
// HarmonyModulesPlugin.js
// constructor
compilation.dependencyFactories.set( HarmonyImportSpecifierDependency, normalModuleFactory);
compilation.dependencyTemplates.set(HarmonyImportSpecifierDependency, new HarmonyImportSpecifierDependency.Template() );
// --------------------------------------------------------
// HarmonyImportSpecifierDependency.js
class HarmonyImportSpecifierDependency extends HarmonyImportDependency {
//...
}
HarmonyImportSpecifierDependency.Template = class HarmonyImportSpecifierDependencyTemplate extends HarmonyImportDependency.Template {
apply(dep, source, runtime) {
//...
}
}
variables
在前面章节提到过,场景较少,这里不细介绍。
遍历blocks
并递归调用 sourceBlock()
。比如main.js
的blocks
中的ImportDependenciesBlock
插播
这里为什么要将模板实例保存到dependencyTemplates
中,而不是直接通过一个类似templateInstancesMap
来直接保存所有的模板实例❓
因为这里的设置是动态的,在其他场景下可能会将XxxTemplate
对应的模板实例设置为null
,比如在 ConcatenatedModule
中的source()方法中重新设置了部分依赖的模板,使得获取的template实例可以动态变更。
思考:什么时候会用到ConcatenatedModule
,作用是什么?
代码语言:javascript复制提示:options.optimization.concatenateModules
// ConcatenatedModule.js
source(...) {
//...
innerDependencyTemplates.set(HarmonyImportSpecifierDependency,
new HarmonyImportSpecifierDependencyConcatenatedTemplate(...)
);
//...
}
template.apply(): dependencies模板的应用
下面看下main.js中的依赖在这里的处理
以HarmonyCompatibilityDependency为例看下是如何修改原始内容(loaders执行后的结果_source )
在parser.parse()
部分介绍了遍历AST
然后在关键节点处发布相关hooks,实际上第一个发布的事件就是hooks.program.call(...)
,在HarmonyDetectionParserPlugin
插件中有注册该钩子,该钩子会判断当前文件是否是ESM
,如果是则会添加HarmonyCompatibilityDependency
和HarmonyInitDependency
依赖
在添加该依赖的过程中,会设置module.buildInfo.exportsArgument
// HarmonyDetectionParserPlugin.js
parser.hooks.program.tap("HarmonyDetectionParserPlugin", ast => {
const isHarmony = //... 检测是否是 ESM
if (isHarmony) {
const compatDep = new HarmonyCompatibilityDependency(module);
module.addDependency(compatDep);
const initDep = new HarmonyInitDependency(module);
module.addDependency(initDep); // 添加依赖
// ...
module.buildInfo.strict = true;
module.buildInfo.exportsArgument = "__webpack_exports__";
}
}
设置了buildInfo.exportsArgument
和buildInfo.strict
,看下HarmonyCompatibilityDependency
的模板
HarmonyCompatibilityDependency.Template = class HarmonyExportDependencyTemplate {
apply(dep, source, runtime) { // runtime: RuntimeTemplate
//...
const content = runtime.defineEsModuleFlagStatement({
exportsArgument: dep.originModule.exportsArgument
});
source.insert(-10, content);
}
};
// Module.js
get exportsArgument() {
return (this.buildInfo && this.buildInfo.exportsArgument) || "exports";
}
// class NormalModule extends Module
runtime指向RuntimeTemplate
,dep.originModule.exportsArgument实际是调用Module.js中的设置的只读属性(get exportsArgument
),在该只读属性中调用前面设置的buildInfo.exportsArgument
,本例中这里content
为:__webpack_require__.r(__webpack_exports__);
__webpack_require__.r
方法来自生成的运行时,代码如下,给模块的定义增加__esModule
属性并设置为true
,显然是用来标识是ESM
规范。
__webpack_require__.r = function(exports) {
//...
Object.defineProperty(exports, '__esModule', { value: true });
};
然后调用source.insert(...)
将变更保存到source.replacements
上(这里的source是ReplaceSource类型),注意此时只是将变更保存以对象的形式下来,并未应用对实际的内容做更改。
class Replacement {
constructor(start, end, content, insertIndex, name) {
this.start = start;
this.end = end;
this.content = content;
this.insertIndex = insertIndex;
this.name = name;
}
}
应该修改的时机被延迟到最终的文件输出阶段(compiler.emitAssets -> writeOut -> .. -> xxx.source())阶段会调用replaceSource.source()会应用这些修改从而获取最终修改的后内容,细节这里不再深入。
sourceBlock(module, childBlock, ...)
childBlock指向:ImportDependenciesBlock(request = './b')
,然后应用该block的dependencies
即这里的ImportDependency(request = './b')
留给读者吧❓
小结
main.js
模块一共有6
个依赖,分别是dependencies中的5
个,blocks中的1
个。
其中HarmonyInitDependency
本身不会产生修改(Replacemnet),因此这里最终由5处修改,如下:
使用replacement.content替换replacement.start/end部分的内容,达到内容的替换和插入。
至此完成了单个模块最终的内容生成,看到这里的变化主要还是和模块化相关,因为将原始的ESM转化为了webpack内置的模块化机制,因此原始的关键字和引用的标识符等需要替换。
小结
ChunkTemplate
当一个chunk不是runtimeChunk时,则会使用chunkTemplate进行代码生成,看到主要的逻辑是
代码语言:javascript复制renderJavascript(chunkTemplate, chunk, moduleTemplate, dependencyTemplates) {
const moduleSources = Template.renderChunkModules(...);
//... chunkTemplate.hooks.modules 无订阅
let source = chunkTemplate.hooks.render.call(...);
//...
chunk.rendered = true;
return new ConcatSource(source, ";");
}
Template.renderChunkModules上面已经分析过,chunkTemplate.hooks.render.call -> JsonpChunkTemplatePlugin
中有订阅(代码),生成类似如下代码:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([...])
解释这里push参数的含义,看到push的实参是一个组,数组可能包含若干个元素:分别是 chunkIds, moreModules, executeModules, prefetchChunks (名称来自构建产物中的运行时代码中的webpackJsonpCallback
方法对于参数的解析)
chunkIds -> chunk.ids,为什么是是数组? 通常情况下:chunk.ids = chunk.id(见compilation.applyChunkIds())。但是可能会存在一个chunk包含子chunk的情况(chunk是模块的集合,如果chunkB的模块包含了chunkA的所有模块,则可以认为是chunkB是chunkA的父亲,二者构成父子关系),为了防止chunk的重复加载,webpack内置插件FlagIncludedChunksPlugin会解析出包含情况,并将chunkA.id的包含情况保存到chunkB.ids。运行时会在加载文件(通常由chunk生成)时会判断chunkId是否已经加载过,已经加载过则不会继续加载。比如这里chunkB.ids = chunkBId,chunkAId,当chunkBId加载完成后,chunkA所对应的文件则没必要再次加载。
Adds chunk ids of chunks which are included in the chunk. This eliminates unnecessary chunk loads.
moreModules -> 来自Template.renderChunkModules(...);的返回的moduleSources,包含了模块id和模块定义的映射(对象或者数组)
executeModules: 表示需要加载和执行的模块。数组结构,第一个元素是 entryModule的模块Id,后面的元素是该模块依赖的chunkId,比如0,0,表示模块Id为0的依赖依赖chunkId为0的chunk。在运行时代码中的checkDeferredModules()方法中的两个变量名deferredModule和installedChunks很好的解释了这个数组的含义。
prefetchChunks:针对下述用法,即import()添加了webpackPrefetch选项
代码语言:javascript复制import(/* webpackChunkName: "ChunkB", webpackPrefetch: true */ './b').then(asyncModule => asyncModule.logB())
运行时代码中会创建下述标签来利用浏览器的prefetch能力。
代码语言:javascript复制<link rel="prefetch" href='xxx' />
小结
在createChunkAssets时,每个chunk首先调用manifest.render生成chunk最终生成文件的内容(存储在ConcatSource类型中),然后调用compilation.emitAsset将source缓存起来
随后返回compilation.seal的回调中,最终来到run()方法中的onCompiled -> compiler.emitAssets
Compiler.js
代码语言:javascript复制// Compiler.js
run(callback){
const onCompiled = (err, compilation) => {
this.emitAssets(compilation, err => {
//...
})
}
//...
this.compile(onCompiled);
}
compile(callback) {
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {
//...
return callback(null, compilation);
});
});
}
下面看下 compiler.emitAssets:将保存到compilation.assets中的文件内容输出到磁盘。
文件输出 compiler.emitAssets
代码语言:javascript复制// Compiler.js
emitAssets(compilation, callback) {
let outputPath;
const emitFiles = err => {
asyncLib.forEachLimit(compilation.getAssets(), 15, ({ name: file, source }, callback) => {
let targetFile = file;
//... 路径有query的场景
const writeOut = err => {
// 异常处理
const targetPath = this.outputFileSystem.join(outputPath, targetFile);
// webpack 4 默认false,webpack5 会默认开启
// 这里看else分支就可以看到这部分做的事情
if (this.options.output.futureEmitAssets) { /*...*/ } else {
let content = source.source();
if (!Buffer.isBuffer(content)) {
content = Buffer.from(content, "utf8");
}
source.existsAt = targetPath;
source.emitted = true;
this.outputFileSystem.writeFile(targetPath, content, err => { /** hooks.assetEmitted 钩子 **/ });
}
};
if (targetFile.match(//|/)) {
// ... 文件名是绝对路径的情况
} else {
writeOut();
}
}, // ...
);
};
this.hooks.emit.callAsync(compilation, err => {
outputPath = compilation.getPath(this.outputPath);
this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
}
遍历compilation.assets -> hooks.emit -> emitFiles -> writeOut -> outputFileSystem.writeFile
在createChunkAssets时每个chunk对应的资源文件内容通过compilation.emitAsset缓存到compilation.assets中,这里首先是遍历compilation.assets获取文件信息(文件名称和文件内容),而后触发hooks.emit钩子在其回调中调用emitFiles,调用outputFileSystem.writeFile
进行文件的输出,最后触发hooks.assetEmitted钩子表示有文件输出。
其中compiler.outputFileSystem的指向是可以指定的。webpack内置了两个相关的类NodeOutputFileSystem
(实际使用的fs
)和MemoryOutputFileSystem
(实际使用的memory-fs
),显然前者是输出到磁盘,后者是输出到内存中。
总结
将Chunk转换为文件的过程
- 先是在compilation.createChunkAssets方法上将Chunk生成的最终的代码
- 然后compiler.emitAssets输出到文件系统(可能是内存,也有可能是本地磁盘)