webpack | 2020-07-09 13:37:13 532次 1次
之前的文章大致上了解了 webpack 的运行然后生成了 hash 后,到这里基本上就是构建资源然后输出到文件,生成我们最终需要的运行文件。执行 createChunkAssets:
createChunkAssets() { ... for (let i = 0; i < this.chunks.length; i++) { const chunk = this.chunks[i]; chunk.files = []; let source; let file; let filenameTemplate; try { //判断使用模板 const template = chunk.hasRuntime() ? this.mainTemplate //主模块 : this.chunkTemplate; //比如有动态引入的模块或者不包含runtime部分的 //获取 render 需要的内容 const manifest = template.getRenderManifest({ chunk, hash: this.hash, fullHash: this.fullHash, outputOptions, moduleTemplates: this.moduleTemplates, dependencyTemplates: this.dependencyTemplates }); // [{ render(), filenameTemplate, pathOptions, identifier, hash }] ... } catch (err) { ... } } }
首先生成 manifest ,不同的模板方法生成的 manifest 中的每一项的 render 方法是不同的,接下来进行不同的模板方法分析。
一、mainTemplate
主模块的生成:
mainTemplate.js getRenderManifest(options) { const result = []; this.hooks.renderManifest.call(result, options); return result; }
触发一个钩子,在 JavascriptModulesPlugin 插件中去创建相关的 render 方法:
compilation.mainTemplate.hooks.renderManifest.tap( "JavascriptModulesPlugin", (result, options) => { ... const useChunkHash = compilation.mainTemplate.useChunkHash(chunk); result.push({ render: () => compilation.mainTemplate.render( hash, chunk, moduleTemplates.javascript, dependencyTemplates ), filenameTemplate, pathOptions: { noChunkHash: !useChunkHash, contentHashType: "javascript", chunk }, identifier: `chunk${chunk.id}`, hash: useChunkHash ? chunk.hash : fullHash }); return result; } );
主要看下这个 render 方法,来自 mainTemplate 中:
MainTemplate.js ... render(hash, chunk, moduleTemplate, dependencyTemplates) { // bundle.js 中的runtime部分 // (function(modules) { ... }) 这里面内容生成 const buf = this.renderBootstrap( hash, chunk, moduleTemplate, dependencyTemplates ); //触发自身中绑定的事件 let source = this.hooks.render.call( new OriginalSource( Template.prefix(buf, " \t") + "\n", "webpack/bootstrap" ), chunk, hash, moduleTemplate, dependencyTemplates ); if (chunk.hasEntryModule()) { source = this.hooks.renderWithEntry.call(source, chunk, hash); } ... chunk.rendered = true; return new ConcatSource(source, ";"); } ...
然后触发在自身的 constructor 中注册事件,生成一个数组,这里面包含最终我们看到 bundle.js 中对应的自执行中的参数部分需要的相关信息,但是这里没有转换,而是将开发的代码原封不动的生成,在最后输出静态文件的时候根据这个数组和源码来生成最终代码,样保证了整个过程中,我们可以追溯对源码做了那些改变,并且在一些 hook 中,我们可以灵活的修改这些操作:
this.hooks.render.tap( "MainTemplate", (bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => { 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( new RawSource(""), chunk, hash, moduleTemplate, dependencyTemplates ) ); source.add(")"); return source; } );
如果配置了 optimization 中 runtimeChunk 为 true(默认false),则会继续进行分包,比如 a 文件动态引入了 b,此时不分包,bundle.js 中的自执行参数会引入 a 相关的逻辑,b 会被单独打包。但此时有个问题,修改 a 文件后,由于会触发 bundle 改变,同时 b 文件虽然是分离出来,但是也会影响 hash 改变,所以,需要进行 runtime(webpack 中加载解析模块的逻辑部分,浏览器运行时)分包处理,这样所有的 mainfest (资源清单)的代码都是单独的包(更准确的说应该是 installedChunks 加载好的模块),runtime 也是一个单独的包,为了页面缓存优化而生,避免修改一个文件而导致所有文件 hash 都发生改变。
二、chunkTemplate
动态导入的模块,同样也是触发 hooks 事件,在 ChunkTemplate:
JavascriptModulesPlugin.js 中对应 chunkTemplate,上面的是 mainTemplate compilation.chunkTemplate.hooks.renderManifest.tap( "JavascriptModulesPlugin", (result, options) => { const chunk = options.chunk; const outputOptions = options.outputOptions; const moduleTemplates = options.moduleTemplates; const dependencyTemplates = options.dependencyTemplates; const filenameTemplate = chunk.filenameTemplate || outputOptions.chunkFilename; //render 方法不同 renderJavascript result.push({ render: () => this.renderJavascript( compilation.chunkTemplate, // chunk模板 chunk, // 需要生成的 chunk 实例 moduleTemplates.javascript, // 模块类型 dependencyTemplates // 不同依赖所对应的渲染模板 ), filenameTemplate, pathOptions: { chunk, contentHashType: "javascript" }, identifier: `chunk${chunk.id}`, hash: chunk.hash }); return result; } );
这里的 render 方法和上面的不同,分为三个步骤来创建。
renderJavascript(chunkTemplate, chunk, moduleTemplate, dependencyTemplates) { //获取每个 chunk 当中所依赖的所有 module 最终需要渲染的代码 const moduleSources = Template.renderChunkModules( chunk, m => typeof m.source === "function", moduleTemplate, dependencyTemplates ); // 触发 hooks.modules 钩子,用以在最终生成 chunk 代码前对 chunk 最修改 const core = chunkTemplate.hooks.modules.call( moduleSources, chunk, moduleTemplate, dependencyTemplates ); //调用 hooks.render 钩子函数,完成这个 chunk 最终的渲染,即在外层添加包裹函数。 let source = chunkTemplate.hooks.render.call( core, chunk, moduleTemplate, dependencyTemplates ); if (chunk.hasEntryModule()) { source = chunkTemplate.hooks.renderWithEntry.call(source, chunk); } chunk.rendered = true; return new ConcatSource(source, ";"); }
进入到 Template.js 中执行它的自身静态方法 renderChunkModules,获取转换后的所有模块:
static renderChunkModules() { ... const allModules = modules.map(module => { return { id: module.id, source: moduleTemplate.render(module, dependencyTemplates, { chunk }) }; }); ... return source; }
其中 source 的生成,通过 ModuleTemplate 中的 render 生成,有三种模板方法:
RuntimeTemplate:这个模板类主要是提供了和 module 运行时相关的代码输出方法,例如你的 module 使用的是 esModule 类型,那么导出的代码模块会带有__esModule标识,而通过 import 语法引入的外部模块都会通过/* harmony import */注释来进行标识。
dependencyTemplates:保存了每个 module 不同依赖的模板,在输出最终代码的时候会通过 dependencyTemplates 来完成模板代码的替换工作。
ModuleTemplate:模板类主要是对外暴露了 render 方法,通过调用 moduleTemplate 实例上的 render 方法,即完成每个 module 的代码渲染工作,这也是每个 module 输出最终代码的入口方法。
tag2 render(module, dependencyTemplates, options) { try { // 资源替换 const moduleSource = module.source( dependencyTemplates, this.runtimeTemplate, this.type ); //content、module主要是用来对于 module 完成依赖代码替换后的代码处理工作, //编写插件时可以通过注册相关的钩子完成对于 module 代码的改造, //因为这个时候得到代码还没有在外层包裹 webpack runtime 的代码 const moduleSourcePostContent = this.hooks.content.call( moduleSource, module, options, dependencyTemplates ); const moduleSourcePostModule = this.hooks.module.call( moduleSourcePostContent, module, options, dependencyTemplates ); // 添加编译 module 外层包裹的函数 const moduleSourcePostRender = this.hooks.render.call( moduleSourcePostModule, module, options, dependencyTemplates ); return this.hooks.package.call( moduleSourcePostRender, module, options, dependencyTemplates ); } catch (e) { e.message = `${module.identifier()}\n${e.message}`; throw e; } }
首先进入了 module.source,这个方法中执行生成方法:
NormalModule.js source(dependencyTemplates, runtimeTemplate, type = "javascript") { ... //生成 const source = this.generator.generate( this, dependencyTemplates, runtimeTemplate, type ); //缓存模块 const cachedSource = new CachedSource(source); this._cachedSources.set(type, { source: cachedSource, hash: hashDigest }); return cachedSource; }
这里的 generate 来自 JavascriptGenerator 插件,第五篇中有介绍一个 resolver 通过 hooks.resolver.call 触发生成,这里面最终是触发了一系列操作创建了 generate,方法主要流程如下:
① 生成一个 ReplaceSource 对象,并将源码保存到对象中。包含模块的替换的内容、替换源码的终止位置、 优先级、 替换源码的起始位置等信息,这个对象还会包含一个 replacements 数组和 source 方法(source 方法会在生成最终代码时被调用,根据 replacements 的内容,将源码 _source 中对应位置代码进行替换,从而得到最终代码)。
② 根据每一个依赖对源码做相应的处理,可能是替换某些代码,也可能是插入一些代码。这些对源码转化的操作,将保存在 ReplaceSource 的 replacements 的数组中。
③ 处理 webpack 内部特有的变量
④ 如有有 block ,则对每个 block 做 1-4 的处理(异步加载对应的 import 的内容会被放在 block 中)。
render 中剩余的两个钩子被注册在 FunctionModuleTemplatePlugin 进行一些 webpack 自身的代码包裹和注释添加,这个方法不管哪种模板方式都是会被触发,其中 mainTemplate 情况下会多传递一个 "/******/ " 参数。
回到 renderJavascript 方法中的 hooks.modules 和 hooks.render 方法,主要用来生成最终的 jsonp 格式包裹代码:
(window['webpackJsonp'] = window['webpackJsonp'] || []).push([ [0] // 前面生成的 chunk 代码 ]);
三、写入文件
经历了上面所有的阶段之后,所有的最终代码信息已经保存在了 Compilation 的 assets 中。然后代码片段会被拼合起来,并且上一步 generator.generate 得到的 ReplaceSource (webpack-sources中)结果中,会遍历 replacement 中的操作,按照要替换的源码的先后位置(同一位置的话,按照 replacement 中的最后一个参数优先级先后)来一一对源码进行替换,然后代码最终代码。
执行完之后,回到 Compiler 中 run 方法的 onCompiled 回调:
//输出构建资源信息 const onCompiled = (err, compilation) => { if (err) return finalCallback(err); // 不需要发送 if (this.hooks.shouldEmit.call(compilation) === false) { const stats = new Stats(compilation); stats.startTime = startTime; stats.endTime = Date.now(); //编译完成钩子 this.hooks.done.callAsync(stats, err => { if (err) return finalCallback(err); return finalCallback(null, stats); }); return; } this.emitAssets(compilation, err => { if (err) return finalCallback(err); //compilation上的钩子 暂时未知用意 ... //记录输出 this.emitRecords(err => { if (err) return finalCallback(err); const stats = new Stats(compilation); stats.startTime = startTime; stats.endTime = Date.now(); this.hooks.done.callAsync(stats, err => { if (err) return finalCallback(err); return finalCallback(null, stats); }); }); }); };
然后里面进行 emitAssets 调用发布资源,这里面调用了 上一小节中提到的 source 触发( ReplaceSource )生成最终的资源内容:
// 发布资源 emitAssets(compilation, callback) { let outputPath; // 输出打包结果文件 const emitFiles = err => { if (err) return callback(err); // 异步的forEach方法 asyncLib.forEachLimit( compilation.getAssets(), 15, // 最多同时执行15个异步任务 ({ name: file, source }, callback) => { let targetFile = file; const queryStringIdx = targetFile.indexOf("?"); if (queryStringIdx >= 0) { targetFile = targetFile.substr(0, queryStringIdx); } // 执行写文件操作 const writeOut = err => { if (err) return callback(err); // 路径拼接,得到真实路径 const targetPath = this.outputFileSystem.join( outputPath, targetFile ); // TODO webpack 5 remove futureEmitAssets option and make it on by default if (this.options.output.futureEmitAssets) { // 用于标记目标路径已经被写入的次数, // key是targetPath。每次targetPath被文件写入,其对应的value会自增。 const targetFileGeneration = this._assetEmittingWrittenFiles.get( targetPath ); // 用于记录资源在不同目标路径被写入的次数。 let cacheEntry = this._assetEmittingSourceCache.get(source); if (cacheEntry === undefined) { cacheEntry = { sizeOnlySource: undefined, // 存储资源被写入的目标路径及其次数,对应this._assetEmittingWrittenFiles的格式 writtenTo: new Map() }; this._assetEmittingSourceCache.set(source, cacheEntry); } // if the target file has already been written if (targetFileGeneration !== undefined) { // check if the Source has been written to this target file const writtenGeneration = cacheEntry.writtenTo.get(targetPath); if (writtenGeneration === targetFileGeneration) { // 如果等式成立,我们跳过写入当前文件,因为它已经被写入过 // (我们假设Compiler在running过程中文件不会被删除) compilation.updateAsset(file, cacheEntry.sizeOnlySource, { size: cacheEntry.sizeOnlySource.size() }); return callback(); } } // TODO webpack 5: if info.immutable check if file already exists in output // skip emitting if it's already there // get the binary (Buffer) content from the Source /** @type {Buffer} */ let content; if (typeof source.buffer === "function") { content = source.buffer(); } else { const bufferOrString = source.source(); if (Buffer.isBuffer(bufferOrString)) { content = bufferOrString; } else { content = Buffer.from(bufferOrString, "utf8"); } } /** * 创建一个source的代替资源,其只有一个size方法返回size属性(sizeOnlySource) * 这步操作是为了让垃圾回收机制能回收由source创建的内存资源 */ cacheEntry.sizeOnlySource = new SizeOnlySource(content.length); compilation.updateAsset(file, cacheEntry.sizeOnlySource, { size: content.length }); // 将content写到目标路径targetPath this.outputFileSystem.writeFile(targetPath, content, err => { if (err) return callback(err); // information marker that the asset has been emitted compilation.emittedAssets.add(file); // 缓存source已经被写入目标路径,写入次数自增 const newGeneration = targetFileGeneration === undefined ? 1 : targetFileGeneration + 1; //将这个自增的值写入cacheEntry.writtenTo和this._assetEmittingWrittenFiles两个Map中 cacheEntry.writtenTo.set(targetPath, newGeneration); this._assetEmittingWrittenFiles.set(targetPath, newGeneration); this.hooks.assetEmitted.callAsync(file, content, callback); }); } else { //若资源已存在在目标路径 则跳过 if (source.existsAt === targetPath) { source.emitted = false; return callback(); } // 获取资源内容,这里开始调用之前提到的source方法 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 => { if (err) return callback(err); this.hooks.assetEmitted.callAsync(file, content, callback); }); } }; // 若目标文件路径包含/或\,先创建文件夹再写入 if (targetFile.match(/\/|\\/)) { const dir = path.dirname(targetFile); this.outputFileSystem.mkdirp( this.outputFileSystem.join(outputPath, dir), writeOut ); } else { writeOut(); } }, err => { ... } ); }; this.hooks.emit.callAsync(compilation, err => { if (err) return callback(err); outputPath = compilation.getPath(this.outputPath); // 递归创建输出目录,并输出资源 this.outputFileSystem.mkdirp(outputPath, emitFiles); }); }
到这里基本上大致梳理了 webpack 从初始到资源生成,当然只是粗糙的分析了流程,里面具体的很深入的细节没有去认真阅读,接下来开始进行生成模板的分析,以及中间使用的其他功能,比如热更新、watch、tree-shaking、splitChunk 等功能的分析。
引用:https://www.cnblogs.com/yanze/p/7999550.html webpack是一个插件架构,所有功能都以插件的形式集成在构建流程中,并通过发布订阅事件来触发各个插件的执行。 核心类是Tapable,用它来实现插件的实例化及挂载。 optimist是node的工具库,根据webpack.config.js及shell options生成option,options包含构建需要的重要信息; (entry-options) webpack创建compiler实例,如果options是数组,则创建多个compiler(compiler包含compiler与watching两个对象), 初始化compiler,为compiler添加上下文context和options,初始化基本插件,把options对应的选项进行require; compiler调用run,run内调用compile方法,开始编译;(make) compiler内创建compilation对象,并将this传入,compilation就包含了对compiler的引用; compiler调用addEntry,addEntry调用_addModuleChain(); _addModuleChain查询合适的工厂函数创建模板,并将其加入module链当中,调用buildModule(),对模块进行build(build-module); buildModule是核心,包括**module.js内调用的build(),build调用doBuild来查找合适的loader,并在回调函数内解析源文件,生成AST,来记录源码间的依赖行为,创建depedency加入依赖数组(期间调用addModuleDepedencies); module创建完毕; compilation调用seal(after-compile),添加hash,调用addModule、addChunk(将module装入chunk)对chunk和module开始封装、合并、抽取公共模块,生成编译后的源码 compilation调用createChunkAsset,开始生成最终代码; createChunkAsset内根据不同的module,调用MainModule.render,chunkTemplate.render进行进一步处理 MainModule.render,chunkTemplate.render内调用moduleTemplate.render 最终生成_webpack_require格式。 moduleTemplate.render调用module.source module.source将生成好的代码放入compilation.assets中 Compiler.emitAssets将compilation的assets输出到目录中 webpack的template有四种子类 MainTemplate.js 生成项目入口文件 ChunkTemplate.js 异步加载的js ModuleTemplate.js 对所有模块的一个代码生成 HotUpdateChunkTemplate.js 对热替换的处理 依赖(dependency) 每一个module都有dependencies字段,这是它的依赖数组,而每一个依赖对象都有一个module字段,指向被依赖的module, 这样module就能找到它依赖的那些module。dependency有许多子类, 如AMDRequireDependency、CommonJsRequireDependency、SystemImportDependency等,分别对应AMD、commonJs、es6等的加载规范。
1人赞