前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >9. 从chunk到最终的文件内容到最后的文件输出?

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

作者头像
tinyant
发布2022-11-23 15:29:13
1.6K0
发布2022-11-23 15:29:13
举报

通过一个demo带你深入进入webpack@4.46.0源码的世界,分析构建原理,专栏地址,共有十篇。


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) { // webpackBootstrap\n");
      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(`\n\n${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(", ") + ") {\n\n");
        // ....
        source.add(moduleSource);
        source.add("\n\n/***/ })");
   }
)

实际效果可能如下:

代码语言: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输出到文件系统(可能是内存,也有可能是本地磁盘)
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-11-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • createModuleAssets
    • 小结
    • createChunkAssets
      • mainTemplate.render
        • Template.renderChunkModules 模块代码生成的入口
        • ModuleTemplate.render 单个模块内容生成
        • normalModule.source() 获取原始文件转换后的内容
        • 小结
      • ChunkTemplate
        • 小结
        • 文件输出 compiler.emitAssets
        • 总结
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档