9. 从chunk到最终的文件内容到最后的文件输出?

2022-11-23 15:29:13 浏览数 (1)

通过一个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属性上。

代码语言:javascript复制
// Compilation.js
emitAsset(file, source, assetInfo = {}) {
    //...
    this.assets[file] = source;
    this.assetsInfo.set(file, assetInfo);
}

compilation.seal中有两个和文件内容生成相关的方法:createModuleAssetscreateChunkAssets

createModuleAssets

处理单个模块在构建过程中由额外生成的文件。在normalModule.doBuild调用runLoaders方法之前会先调用createLoaderContext创建的上下文,该上下文对象包含emitFile方法,在loader执行阶段时可以调用该方法来输出文件内容(此时只是缓存到module.buildInfo.assets/assetsInfo属性上),比如file-loader就会使用该方法来输出文件。

代码语言:javascript复制
// 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上。

代码语言:javascript复制
// 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方法,该方法中看到两个核心属性:mainTemplatechunkTemplatechunkTemplate根据chunk中包含的模块信息来生成最终该chunk对应输出js文件的内容,而mainTemplate具有chunkTemplate能力之外还具有生成运行时runtime代码的能力。

mainTemplatechunkTemplate是在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方法

代码语言:javascript复制
// 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方法中,该方法包含两个部分:
代码语言:txt复制
- `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方法的主要过程如下

  1. 调用renderBootstrap方法生成运行时(runtime)代码(不深入分析了,主要是运行时代码的连接,涉及的核心插件是;JsonpMainTemplatePlugin)
  2. 然后通过钩子hooks.render.call -> hooks.modules.call -> Template.renderChunkModules生成该chunk中所有模块的定义,这里的hooks.render.tap回调将运行时代码和模块代码进行组合。

Template.renderChunkModules 模块代码生成的入口

下面分析Template.renderChunkModules方法

代码语言:javascript复制
// 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')在这里的效果如下:

代码语言:javascript复制
[/* 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,但是挂载的实例是不同的,比如这里是moduleTemplateFunctionModuleTemplatePlugin中监听了该钩子,目的是给原始模块套一层function的壳子,以配合webpack自己的模块化(runtime)机制来保证模块正常加载。

代码语言:javascript复制
// 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()

代码语言:javascript复制
// 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返回的容;注意这里通过ReplaceSourceoriginalSource进行了包装。

代码语言:javascript复制
// 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的三个属性的处理(dependenciesvariablesblocks),比如本文案例中的main.jsdependencies,blocks都是有值的。

遍历dependencies并调用sourceDependency() -> template.apply()应用依赖的模板修改模块的原始内容,后面会分析main.jsdependencies是如何修改原始内容。收集的XxxDependency比如HarmonyImportSpecifierDependency都会有一个与之对应的Template类,该类提供了apply方法会被sourceDependency调用。看到sourceDependency中有使用dependencyTemplates,通过该属性来获取依赖关联的模板,和dependencyFactories使用方式类似,下面举个具体的例子来说明XxxDependency.TemplatedependencyTemplates用来存储XxxDependency关联的template,前面在介绍parser.parse()部分提过依赖收集的相关的插件如HarmonyModulesPlugin,在类似插件的构造函数中会设置依赖到模板的映射,如下例

代码语言:javascript复制
// 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.jsblocks中的ImportDependenciesBlock


插播

这里为什么要将模板实例保存到dependencyTemplates中,而不是直接通过一个类似templateInstancesMap来直接保存所有的模板实例❓

因为这里的设置是动态的,在其他场景下可能会将XxxTemplate对应的模板实例设置为null,比如在 ConcatenatedModule 中的source()方法中重新设置了部分依赖的模板,使得获取的template实例可以动态变更。

思考:什么时候会用到ConcatenatedModule,作用是什么?

提示:options.optimization.concatenateModules

代码语言:javascript复制
// 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,如果是则会添加HarmonyCompatibilityDependencyHarmonyInitDependency依赖

在添加该依赖的过程中,会设置module.buildInfo.exportsArgument

代码语言:javascript复制
// 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.exportsArgumentbuildInfo.strict,看下HarmonyCompatibilityDependency的模板

代码语言:javascript复制
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 &amp;&amp; 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规范。

代码语言:javascript复制
__webpack_require__.r = function(exports) {
    //...
    Object.defineProperty(exports, '__esModule', { value: true });
};

然后调用source.insert(...)将变更保存到source.replacements上(这里的source是ReplaceSource类型),注意此时只是将变更保存以对象的形式下来,并未应用对实际的内容做更改。

代码语言:javascript复制
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中有订阅(代码),生成类似如下代码:

代码语言:javascript复制
(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转换为文件的过程

  1. 先是在compilation.createChunkAssets方法上将Chunk生成的最终的代码
  2. 然后compiler.emitAssets输出到文件系统(可能是内存,也有可能是本地磁盘)

0 人点赞