comipler是其webpack的支柱模块,其继承于Tapable类,在compiler上定义了不少钩子函数,贯穿其整个编译流程,这些钩子上注册了不少插件,用于在特定的时机执行特定的操做,同时,用户也能够在这些钩子上注册自定义的插件来进行功能拓展,接下来将围绕这些钩子函数来分析webpack的主流程。javascript
compiler对象的生成过程大体能够简化为以下过程,首先对咱们传入的配置进行格式验证,接着调用Compiler构造函数生成compiler实例,自定义的plugins注册,最后调用new WebpackOptionsApply().process(options, compiler)
进行默认插件的注册,comailer初始化等。css
const webpack = (options,callback)=>{ //options格式验证 const webpackOptionsValidationErrors = validateSchema( webpackOptionsSchema, options ); ... //生成compiler对象 let compiler = new Compiler(options.context); //自定义插件注册 if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { if (typeof plugin === "function") { plugin.call(compiler, compiler); } else { plugin.apply(compiler); } } } //默认插件注册,默认配置等 compiler.options = new WebpackOptionsApply().process(options, compiler); }
Webpackoprionapply
是一个重要的步骤,一般是此处插件注册在compiler.hooks.thisCompilation或compiler.hooks.compilation上,并在compilation钩子上调用时,进一步注册到parser(用于生成依赖及依赖模版)或者mainTemplate(用于seal阶段render)的钩子上:java
process(options, compiler) { //当target是一个函数时,能够自定义该环境下使用哪些plugins if (typeof options.target === "string") { //1.不一样target下引入不一样的plugin进行文件加载 switch (options.target) { case "web": //JsonpTemplatePlugin插件注册在compiler.hooks.this.compilation上,并在该钩子调用时,在compilation.mainTemplate的多个钩子上注册事件以在最后生成的代码中加入Jsonp Script进行文件加载 new JsonpTemplatePlugin().apply(compiler); new FetchCompileWasmTemplatePlugin({ mangleImports: options.optimization.mangleWasmImports }).apply(compiler); //在compiler.hooks.compilation上注册,并挂载在compilation.moduleTemplates.javascript上,在seal阶段template.hooks.render时调用 new FunctionModulePlugin().apply(compiler); new NodeSourcePlugin(options.node).apply(compiler); new LoaderTargetPlugin(options.target).apply(compiler); break; case "node": case "async-node": //若是目标环境为node,能够用require方式加载文件,而不须要使用Jsonp new NodeTemplatePlugin({ asyncChunkLoading: options.target === "async-node" }).apply(compiler); new ReadFileCompileWasmTemplatePlugin({ mangleImports: options.optimization.mangleWasmImports }).apply(compiler); new FunctionModulePlugin().apply(compiler); new NodeTargetPlugin().apply(compiler); new LoaderTargetPlugin("node").apply(compiler); break; ........... } //2. output Library处理 ........... //3. devtool sourceMap处理 ........... //注册在compiler.hooks.compilation上,给normalModuleFactory的js模块提供Parser、JavascriptGenerator对象 ,并给seal阶段的template提供renderManifest数组(包含render方法) new JavascriptModulesPlugin().apply(compiler); //注册在compiler.hooks.compilation上,给normalModuleFactory的jso n模块提供Parser、JavascriptGenerator对象 new JsonModulesPlugin().apply(compiler); //同理,webassembly模块 new WebAssemblyModulesPlugin({ mangleImports: options.optimization.mangleWasmImports }).apply(compiler); //4. 入口不一样格式下的处理,注册在compiler.hooks.entryOption,在调用时新建SingleEntryPlugin或MultiEntryPlugin new EntryOptionPlugin().apply(compiler); compiler.hooks.entryOption.call(options.context, options.entry); //5. 不一样模块写法的处理,通常注册在compiler.hooks.compilation上,调用时在normalModuleFactory.hooks.parse上注册,接着在parse的hooks上注册,在parse阶段,遇到不一样的节点调用不一样的plugin,从而在模块的dependencies数组中推入不一样的dependencyFactory和dependencyTemplate new CompatibilityPlugin().apply(compiler); //es模块 new HarmonyModulesPlugin(options.module).apply(compiler); if (options.amd !== false) { //AMD模块 const AMDPlugin = require("./dependencies/AMDPlugin"); const RequireJsStuffPlugin = require("./RequireJsStuffPlugin"); new AMDPlugin(options.module, options.amd || {}).apply(compiler); new RequireJsStuffPlugin().apply(compiler); } //CommonJS模块 new CommonJsPlugin(options.module).apply(compiler); new LoaderPlugin().apply(compiler); if (options.node !== false) { const NodeStuffPlugin = require("./NodeStuffPlugin"); new NodeStuffPlugin(options.node).apply(compiler); } new ImportPlugin(options.module).apply(compiler); new SystemPlugin(options.module).apply(compiler); ......... //6. 优化 ......... //7. modeId、chunkId相关 ......... //8. resolve初始配置,在resolve时调用this.getResolver时调用 compiler.resolverFactory.hooks.resolveOptions .for("normal") .tap("WebpackOptionsApply", resolveOptions => { return Object.assign( { fileSystem: compiler.inputFileSystem }, cachedCleverMerge(options.resolve, resolveOptions) ); }); compiler.resolverFactory.hooks.resolveOptions .for("context") .tap("WebpackOptionsApply", resolveOptions => { return Object.assign( { fileSystem: compiler.inputFileSystem, resolveToContext: true }, cachedCleverMerge(options.resolve, resolveOptions) ); }); compiler.resolverFactory.hooks.resolveOptions .for("loader") .tap("WebpackOptionsApply", resolveOptions => { return Object.assign( { fileSystem: compiler.inputFileSystem }, cachedCleverMerge(options.resolveLoader, resolveOptions) ); }); compiler.hooks.afterResolvers.call(compiler); return options; }
生成compler实例后,cli.js中就会调用compiler.run方法了,compiler.run的流程大体能够简写以下(去掉错误处理等逻辑),其囊括了整个打包过程,首先依次触发beforeRun、run等钩子,接下来调用compiler.compile()进行编译过程,在回调中取得编译后的compilation对象,调用compiler.emitAssets()输出打包好的文件,最后触发done钩子。node
run(){ const onCompiled = (err, compilation) => { //打包输出 this.emitAssets(compilation, err => { this.hooks.done.callAsync(stats) }; // beforeRun => run => this.compile() this.hooks.beforeRun.callAsync(this, err => { this.hooks.run.callAsync(this, err => { this.readRecords(err => { this.compile(onCompiled); }); }); }); }
在这个方法中主要也是经过回调触发钩子进行流程控制,经过newCompilation=>make=>finsih=>seal
流程来完成一次编译过程,compiler将具体一次编译过程放在了compilation实例上,能够将主流程与编译过程分割开来,当处于watch模式时,能够进行屡次编译。webpack
compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { this.hooks.compile.call(params); const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, err => { compilation.finish(err => { compilation.seal(err => { this.hooks.afterCompile.callAsync(compilation, err => { return callback(null, compilation); }); }); }); }); }); }
从图中能够看到make钩子上注册了singleEntryPlugin(单入口配置时),compilation做为参数传入该插件,接着在插件中调用compilation.addEntry方法开始编译过程。git
compiler.hooks.make.tapAsync( "SingleEntryPlugin", (compilation, callback) => { const { entry, name, context } = this; const dep = SingleEntryPlugin.createDependency(entry, name); compilation.addEntry(context, dep, name, callback); } );
编译过程的入口在compilation._addModuleChain函数,传入entry,context参数,在回调中获得编译生成的module。编译的过程包括文件和loader路径的resolve,loader对源文件的处理,递归的进行依赖处理等等.github
addEntry(context, entry, name, callback) { this.hooks.addEntry.call(entry, name); this._addModuleChain( context, entry, module => { this.entries.push(module); }, (err, module) => { this.hooks.succeedEntry.call(entry, name, module); return callback(null, module); } ); }
在this._addModuleChain
中调用moduleFactory.create()
来开始模块的建立,模块建立第一步须要经过resolve获得入口文件的具体路径。web
webpack 中每涉及到一个文件,就会通过 resolve 的过程。webpack
使用 enhanced-resolve 来提供绝对路径、相对路径、模块路径的多样解析方式。express
在moduleFactory.create()
resolve过程从经过调用normalModuleFactory中factory函数开始。json
factory(result, (err, module) => { if (err) return callback(err); if (module && this.cachePredicate(module)) { for (const d of dependencies) { dependencyCache.set(d, module); } } callback(null, module); }); //传入的result的基本形式以下 result = { context: "/Users/hahaha/project/demo/webpack-demo" contextInfo: {issuer: "", compiler: undefined} dependencies: [SingleEntryDependency] request: "./src/index.js" resolveOptions: {} }
factory方法拿到入口信息result后,将result传递给resolver方法,resolver方法先获得对普通文件和loader文件的resolve方法:
const loaderResolver = this.getResolver("loader"); const normalResolver = this.getResolver("normal", data.resolveOptions);
而后检查路径中是否包含内联loaders, 经过调用loaderResolver和normalResolver并行的resolve文件路径和内联loaders路径。若是使用了内联loaders,则将其保存在loaders变量中,接着对获得的文件路径进行ruler匹配,获得匹配到的loader数值:
const result = this.ruleSet.exec({ resource: resourcePath, realResource: matchResource !== undefined ? resource.replace(/\?.*/, "") : resourcePath, resourceQuery, issuer: contextInfo.issuer, compiler: contextInfo.compiler });·
this.ruleSet
是用户定义的loaders和默认loader的格式化的结果,经过其exec方法能够获得与资源文件匹配的loaders数组。
接下来并行的resolver这些loaders路径,并保存在loaders数组中;值得注意的是,在resolver钩子的回调中初始化了parser和generator对象:
parser: this.getParser(type, settings.parser), generator: this.getGenerator(type, settings.generator),
parser的注册方法以下(webpackOptionapply中各类模块处理的插件就是这样注册的):
compiler.hooks.normalModuleFactory.tap('MyPlugin', factory => { factory.hooks.parser.for('javascript/auto').tap('MyPlugin', (parser, options) => { parser.hooks.someHook.tap(/* ... */); }); });
入口文件经过resolver后获得的结果相似以下(没使用loaders,因此为空数组):
{ context: "/Users/hahaha/project/demo/webpack-demo" dependencies: [SingleEntryDependency] generator: JavascriptGenerator {} loaders: [] matchResource: undefined parser: Parser {_pluginCompat: SyncBailHook, hooks: {…}, options: {…}, sourceType: "auto", scope: undefined, …} rawRequest: "./src/index.js" request: "/Users/hahaha/project/demo/webpack-demo/src/index.js" resolveOptions: {} resource: "/Users/hahaha/project/demo/webpack-demo/src/index.js" resourceResolveData: { context: {…}, path: "/Users/hahaha/project/demo/webpack-demo/src/index.js", request: undefined, query: "", module: false, … } settings: {type: "javascript/auto", resolve: {…}} type: "javascript/auto" userRequest: "/Users/hahaha/project/demo/webpack-demo/src/index.js" }
resolver过程获得的入口文件的路径,接下来在factory方法中会调用createdModule = new NormalModule(result)
生成模块,改构造函数将resolver获得的信息保存到模块上,并提供了一些实例方法来进行后续的build过程。
本文并未深刻resolve具体流程,详情能够参阅:
在moduleFactory.create()
方法的回调中获得resolve后生成的module后,将开始模块的build过程,我将代码主干保留以下:
moduleFactory.create( { contextInfo: { issuer: "", compiler: this.compiler.name }, context: context, dependencies: [dependency] }, (err, module) => { const afterBuild = () => { if (addModuleResult.dependencies) { this.processModuleDependencies(module, err => { if (err) return callback(err); callback(null, module); }); } else { return callback(null, module); } }; this.buildModule(module, false, null, null, err => { afterBuild(); }); } );
首先调用this.buildModule
方法,因为moduleFactory.create()
生成的module是normalModule(本例中)的实例,因此能够其实是调用normalModule.doBuild()
进行build,能够看到首先生成了一个loaderContext对象,在后面运行loader的时候,会经过call方法将loader的this指向loaderContext。
doBuild(options, compilation, resolver, fs, callback) { const loaderContext = this.createLoaderContext( resolver, options, compilation, fs ); runLoaders( { resource: this.resource, loaders: this.loaders, context: loaderContext, readResource: fs.readFile.bind(fs) }, (err, result) => { ... return callback(); } ); }
接下来就进入runloaders方法了,传入的参数包括模块路径,模块的loaders数组,loaderContext等。在该方法内,首先对相关参数进行初始化的操做,特别是将 loaderContext 上的部分属性改写为 getter/setter 函数,这样在不一样的 loader 执行的阶段能够动态的获取一些参数。接下来进入iteratePitchingLoaders
方法:
function iteratePitchingLoaders(options, loaderContext, callback) { //当处理完最后一个loader的pitch后,倒序开始处理loader的normal方法 if(loaderContext.loaderIndex >= loaderContext.loaders.length) return processResource(options, loaderContext, callback); var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // iterate if(currentLoaderObject.pitchExecuted) { loaderContext.loaderIndex++; return iteratePitchingLoaders(options, loaderContext, callback); } // 在loadLoader中,经过module = require(loader.path)加载loader,并将module上的normal、pitch、raw属性拷贝到loader对象上 loadLoader(currentLoaderObject, function(err) { if(err) { loaderContext.cacheable(false); return callback(err); } var fn = currentLoaderObject.pitch; currentLoaderObject.pitchExecuted = true; //若是没有该loader上没有pitch,则跳到下一个loader的pitch if(!fn) return iteratePitchingLoaders(options, loaderContext, callback); //在runSyncOrAsync内执行loader上的pitch函数 runSyncOrAsync( fn, loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}], function(err) { if(err) return callback(err); var args = Array.prototype.slice.call(arguments, 1); if(args.length > 0) { loaderContext.loaderIndex--; iterateNormalLoaders(options, loaderContext, args, callback); } else { iteratePitchingLoaders(options, loaderContext, callback); } } ); }); }
在深刻runSyncOrAsync
函数以前,咱们先来介绍下webpack官网上的loader API:
同步loader
//当不返回map,meta时能够直接返回 module.exports = function(content,map,meta){ return someSyncOperation(content) } //返回多个参数时,要经过this.callback调用 module.exports = function(content,map,meta){ this.callback(null,someSyncOperation(content),map,meta) return }
异步loader
//对于异步loader,使用this.async来获取callback函数 module.exports = function(content,map,meta){ let callback = this.async(); someAsyncOperation(content,function(err,result,sourceMap,meta){ if(err) return callback(err); callback(null,result,sourceMap,meta); }) } //promise写法 module.exports = function(content){ return new Promise(resolve =>{ someAsyncOperation(content,(err,result)=>{ if(err) resolve(err) resolve(null,result) }) }) }
了解了loader的写法后,咱们在来看看loader的执行函数runSyncOrAsync
:
function runSyncOrAsync(fn, context, args, callback) { var isSync = true; var isDone = false; var isError = false; // internal error var reportedError = false; context.async = function async() { if(isDone) { if(reportedError) return; // ignore throw new Error("async(): The callback was already called."); } isSync = false; return innerCallback; }; var innerCallback = context.callback = function() { if(isDone) { if(reportedError) return; // ignore throw new Error("callback(): The callback was already called."); } isDone = true; isSync = false; try { callback.apply(null, arguments); } catch(e) { isError = true; throw e; } }; try { var result = (function LOADER_EXECUTION() { return fn.apply(context, args); }()); if(isSync) { isDone = true; if(result === undefined) return callback(); if(result && typeof result === "object" && typeof result.then === "function") { return result.then(function(r) { callback(null, r); }, callback); } return callback(null, result); } } catch(e) { if(isError) throw e; if(isDone) { // loader is already "done", so we cannot use the callback function // for better debugging we print the error on the console if(typeof e === "object" && e.stack) console.error(e.stack); else console.error(e); return; } isDone = true; reportedError = true; callback(e); } }
结合loader执行的各类写法,runSyncOrAsync
的逻辑就很清晰了。
咱们知道,loader上的方法有pitch和normal之分,它们都是用runSyncOrAsync
执行的,执行顺序为:
//config中 use: [ 'a-loader', 'b-loader', 'c-loader' ] //执行顺序 |- a-loader `pitch` |- b-loader `pitch` |- c-loader `pitch` |- requested module is picked up as a dependency |- c-loader normal execution |- b-loader normal execution |- a-loader normal execution
loader的pitch方法通常写法以下:
//data对象保存在loaderContext对象的data属性中,能够用于在循环时,捕获和共享前面的信息。 module.exports.pitch = function(remainingRequest, precedingRequest, data) { data.value = 42; }; //pitch中有返回值时,会跳事后续的pitch和内层的normal方法 module.exports.pitch = function(remainingRequest, precedingRequest, data) { if (someCondition()) { return 'module.exports = require(' + JSON.stringify('-!' + remainingRequest) + ');'; } };
例如在style-loader和css-loader一块儿使用时,先执行的style-loader的pitch方法,返回值以下:
var content = require("!!../node_modules/css-loader/dist/cjs.js!./style.css"); if (typeof content === 'string') { content = [[module.id, content, '']]; } var options = {} options.insert = "head"; options.singleton = false; var update = require("!../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js")(content, options); if (content.locals) { module.exports = content.locals; }
因为有返回值,会跳事后续的style-loader的pitch方法、css-loader的pitch方法、css-loader的normal方法和css-loader的normal方法。而后在后续处理依赖时处理内联loader的时候再进行css-loader的处理。
loaders处理以后,获得处理后的文件的内容字符串保存在module的_source变量中,如何从这个字符串中获得依赖呢?这就须要对这个字符串进行处理了,在回调函数中this.parser.parse
方法被执行:
parse(source, initialState) { let ast; let comments; if (typeof source === "object" && source !== null) { ast = source; comments = source.comments; } else { comments = []; ast = Parser.parse(source, { sourceType: this.sourceType, onComment: comments }); } const oldScope = this.scope; const oldState = this.state; const oldComments = this.comments; this.scope = { topLevelScope: true, inTry: false, inShorthand: false, isStrict: false, definitions: new StackedSetMap(), renames: new StackedSetMap() }; const state = (this.state = initialState || {}); this.comments = comments; if (this.hooks.program.call(ast, comments) === undefined) { this.detectStrictMode(ast.body); this.prewalkStatements(ast.body); this.blockPrewalkStatements(ast.body); this.walkStatements(ast.body); } this.scope = oldScope; this.state = oldState; this.comments = oldComments; return state; }
先调用Parse.parse方法获得AST,而后就是对这个树进行遍历了,流程为: program事件 -> detectStrictMode -> prewalkStatements -> walkStatements。这个过程会经过遍历AST的各个节点,从而触发不一样的钩子函数,在这些钩子函数上会触发一些模块处理的方法(这些方法大可能是在webpackOptionapply中注册到parser上的)给 module 增长不少 dependency 实例,每一个 dependency 类都会有一个 template 方法,而且保存了原来代码中的字符位置 range,在最后生成打包后的文件时,会用 template 的结果替换 range 部分的内容。
因此最终获得的 dependency 不只包含了文件中全部的依赖信息,还被用于最终生成打包代码时对原始内容的修改和替换,例如将 return 'sssss' + A
替换为 return 'sssss' + _a_js__WEBPACK_IMPORTED_MODULE_0__["A"]
program 事件中,会触发两个 plugin 的回调:HarmonyDetectionParserPlugin 和 UseStrictPlugin
HarmonyDetectionParserPlugin中,若是代码中有 import 或者 export 或者类型为 javascript/esm,那么会增长了两个依赖:HarmonyCompatibilityDependency, HarmonyInitDependency 依赖。
UseStrictPlugin用来检测文件是否有 use strict
,若是有,则增长一个 ConstDependency 依赖。
整个 parse 的过程关于依赖的部分,咱们总结一下:
全部的依赖都被保存在 module.dependencies 中。module.dependencies大体内容以下:
0:CommonJsRequireDependency loc: SourceLocation end: Position {line: 1, column: 77} start: Position {line: 1, column: 14} __proto__: Object module: null optional: false range: (2) [22, 76] request: "!!../node_modules/css-loader/dist/cjs.js!./style.css" userRequest: "!!../node_modules/css-loader/dist/cjs.js!./style.css" weak: false type: (...) 1: RequireHeaderDependency {module: null, weak: false, optional: false, loc: SourceLocation, range: Array(2)} 2: ConstDependency {module: null, weak: false, optional: false, loc: SourceLocation, expression: "module.i", …} 3: CommonJsRequireDependency {module: null, weak: false, optional: false, loc: SourceLocation, request: "!../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js", …} 4: RequireHeaderDependency {module: null, weak: false, optional: false, loc: SourceLocation, range: Array(2)}
如图中所示,接下来就是处理依赖了,进入回调中的processModuleDependencies
方法:
processModuleDependencies(module, callback) { const dependencies = new Map(); const addDependency = dep => { const resourceIdent = dep.getResourceIdentifier(); // 过滤掉没有 ident 的,就是请求路径,例如 constDependency 这些只用在最后打包文件生成的依赖 if (resourceIdent) { const factory = this.dependencyFactories.get(dep.constructor); if (factory === undefined) { throw new Error( `No module factory available for dependency type: ${dep.constructor.name}` ); } let innerMap = dependencies.get(factory); if (innerMap === undefined) { dependencies.set(factory, (innerMap = new Map())); } let list = innerMap.get(resourceIdent); if (list === undefined) innerMap.set(resourceIdent, (list = [])); list.push(dep); } }; const addDependenciesBlock = block => { if (block.dependencies) { iterationOfArrayCallback(block.dependencies, addDependency); } if (block.blocks) { iterationOfArrayCallback(block.blocks, addDependenciesBlock); } if (block.variables) { iterationBlockVariable(block.variables, addDependency); } }; try { addDependenciesBlock(module); } catch (e) { callback(e); } const sortedDependencies = []; for (const pair1 of dependencies) { for (const pair2 of pair1[1]) { sortedDependencies.push({ factory: pair1[0], dependencies: pair2[1] }); } } this.addModuleDependencies( module, sortedDependencies, this.bail, null, true, callback ); }
接下来进入this.addModuleDependencies,在该函数中,递归进行以前的resolve=》buildMoudule过程直到全部的依赖处理完成,到此build过程就完成了。
详情参阅https://juejin.im/post/5cc51b...
在上一步build完成后,build好的module保存在compilation._modules对象中,接下来须要根据这些modules生成chunks,并生成最后打包好的代码保存到compilation.assets中。
去除优化的钩子和一些支线剧情,seal方法能够简写以下:
seal(callback) { this.hooks.seal.call(); // 初始化chunk、chunkGroups等 for (const preparedEntrypoint of this._preparedEntrypoints) { const module = preparedEntrypoint.module; const name = preparedEntrypoint.name; const chunk = this.addChunk(name); const entrypoint = new Entrypoint(name); entrypoint.setRuntimeChunk(chunk); entrypoint.addOrigin(null, name, preparedEntrypoint.request); this.namedChunkGroups.set(name, entrypoint); this.entrypoints.set(name, entrypoint); this.chunkGroups.push(entrypoint); //在chunkGroups的chunk数组中推入chunk,在chunk的_groups Set中加入chunhGroups,创建二者联系 GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk); //在module的_chunks Set中加入chunk,chunk的_modules Set中加入module GraphHelpers.connectChunkAndModule(chunk, module); chunk.entryModule = module; chunk.name = name; //给各个依赖的module按照引用层级加上depth属性,如入口为的depth为0 this.assignDepth(module); } //生成module graph 和chunk graph buildChunkGraph( this, /** @type {Entrypoint[]} */ (this.chunkGroups.slice()) ); this.sortModules(this.modules); this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => { this.hooks.beforeModuleIds.call(this.modules); this.hooks.moduleIds.call(this.modules); this.applyModuleIds(); this.hooks.beforeChunkIds.call(this.chunks); this.applyChunkIds(); this.hooks.beforeHash.call(); this.createHash(); this.hooks.afterHash.call(); return this.hooks.afterSeal.callAsync(callback); }); }
首先是在compilation对象上初始化chunk、chunkGroups等变量,利用GraphHelpers
方法创建module和chunk,chunk和chunkGroups之间的关系,调用assignDepth
方法给每一个module加上依赖层级depth,接着进入buildChunkGraph
生成chunk graph。
const buildChunkGraph = (compilation, inputChunkGroups) => { // SHARED STATE /** @type {Map<ChunkGroup, ChunkGroupDep[]>} */ const chunkDependencies = new Map(); /** @type {Set<ChunkGroup>} */ const allCreatedChunkGroups = new Set(); /** @type {Map<ChunkGroup, ChunkGroupInfo>} */ const chunkGroupInfoMap = new Map(); /** @type {Set<DependenciesBlock>} */ const blocksWithNestedBlocks = new Set(); // PART ONE visitModules( compilation, inputChunkGroups, chunkGroupInfoMap, chunkDependencies, blocksWithNestedBlocks, allCreatedChunkGroups ); // PART TWO connectChunkGroups( blocksWithNestedBlocks, chunkDependencies, chunkGroupInfoMap ); // Cleaup work cleanupUnconnectedGroups(compilation, allCreatedChunkGroups); };
主要逻辑在visitModules
方法中,首先经过const blockInfoMap = extraceBlockInfoMap(compilation)
获得module graph,module是一个Map,键名是各个module,键值是module的依赖,分为异步加载的依赖blocks和同步依赖modules:
0: {NormalModule => Object} key: NormalModule {dependencies: Array(4), blocks: Array(1), variables: Array(0), type: "javascript/auto", context: "/Users/hahaha/project/demo/webpack-demo/src", …} value: blocks: [ImportDependenciesBlock] modules: Set(1) {NormalModule} 1: {ImportDependenciesBlock => Object} 2: {NormalModule => Object} 3: {NormalModule => Object} 4: {NormalModule => Object} 5: {NormalModule => Object} 6: {NormalModule => Object}
而后利用两层循环将栈内的模块及其依赖一层层的加入到chunk的this._modules对象中,同步依赖放在内层循环处理,异步依赖放在外层循环处理。(利用栈处理递归依赖以及利用swtich进行流程管理)
接下来connectChunkGroups
、cleanupUnconnectedGroups
,遍历 chunk graph,经过和依赖的 module 之间的使用关系来创建起不一样 chunkGroup 之间的父子关系,同时剔除一些没有创建起联系的 chunk,没细看
接下来就是生成module id和chunk id了,以前好像是生成的数字id,如今好像在NamedModulesPlugin和NamedChunksPlugin插件中将id命名成文件名了。
在his.createHash
方法中生成hash,包括本次编译的hash、chunkhash、modulehash。hash的生成步骤基本以下,首先create获得moduleHash
方法,再在updateHash
方法中不断的加各类内容,例如modulehash生成过程当中就用到了module id、各类依赖、export信息等,最后调用digest方法生成hash:
const moduleHash = createHash(hashFunction); module.updateHash(moduleHash); module.hash = /** @type {string} */ (moduleHash.digest(hashDigest)); module.renderedHash = module.hash.substr(0, hashDigestLength);
chunkhash生成过程当中会用到chunk id、module id、name、template信息等。
最后就是调用
createChunkAssets() { const outputOptions = this.outputOptions; const cachedSourceMap = new Map(); const alreadyWrittenFiles = new Map(); //遍历chunks数组 for (let i = 0; i < this.chunks.length; i++) { const chunk = this.chunks[i]; chunk.files = []; let source; let file; let filenameTemplate; try { //入口模块就是hasRuntime,相对于普通模块,加了一层webpack runtime bootstrap 自执行函数包裹 const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate; //在该函数内会触发相应template.hooks.renderManifest钩子,在webpackoptionapply中注册的javaScriptModulesPlugin(通常是这个)中执行逻辑,在返回结果中推入render方法。 const manifest = template.getRenderManifest({ chunk, hash: this.hash, fullHash: this.fullHash, outputOptions, moduleTemplates: this.moduleTemplates, dependencyTemplates: this.dependencyTemplates }); // [{ render(), filenameTemplate, pathOptions, identifier, hash }] for (const fileManifest of manifest) { //缓存处理 ........ //调用上一步获得的render方法 source = fileManifest.render(); } this.assets[file] = source; chunk.files.push(file); this.hooks.chunkAsset.call(chunk, file); alreadyWrittenFiles.set(file, { hash: usedHash, source, chunk }); } } } }
当为chunkTemplate时,javaScriptModulesPlugin中的render方法:
renderJavascript(chunkTemplate, chunk, moduleTemplate, dependencyTemplates) { //获取每一个 chunk 当中所依赖的全部 module 最终须要渲染的代码 const moduleSources = Template.renderChunkModules( chunk, m => typeof m.source === "function", moduleTemplate, dependencyTemplates ); //最终生成 chunk 代码前对 chunk 最修改 const core = chunkTemplate.hooks.modules.call( moduleSources, chunk, moduleTemplate, dependencyTemplates ); //外层添加包裹函数 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, ";"); }
moduleSources示例:
{ , /***/ "./src/foo.js": ,/*!********************!*\ , !*** ./src/foo.js ***! , \********************/ ,/*! exports provided: default */ ,/***/ (function(module, __webpack_exports__, __webpack_require__) { ,"use strict"; ,"eval("__webpack_require__.r(__webpack_exports__);\n /* harmony default export */ __webpack_exports__[\"default\"] = (function(){\n console.log('here are foo')\n } ); \n\n\n//# sourceURL=webpack:///./src/foo.js?");", /***/ })
最后获得source的示例:
"(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],,{ , /***/ "./src/foo.js": ,/*!********************!*\ , !*** ./src/foo.js ***! , \********************/ ,/*! exports provided: default */ ,/***/ (function(module, __webpack_exports__, __webpack_require__) { ,"use strict"; ,"eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (function(){\n console.log('here are foo')\n});\n\n\n//# sourceURL=webpack:///./src/foo.js?");", /***/ }), },])"
当为mainTemplate时,调用的是mainTemplate中的render方法以下:
render(hash, chunk, moduleTemplate, dependencyTemplates) { 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); } if (!source) { throw new Error( "Compiler error: MainTemplate plugin 'render' should return something" ); } chunk.rendered = true; return new ConcatSource(source, ";"); }
获得的Bootstrap以下:
// install a JSONP callback for chunk loading function webpackJsonpCallback(data) { var chunkIds = data[0]; var moreModules = data[1]; // add "moreModules" to the modules object, // then flag all "chunkIds" as loaded and fire callback var moduleId, chunkId, i = 0, resolves = []; for(;i < chunkIds.length; i++) { chunkId = chunkIds[i]; if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); } installedChunks[chunkId] = 0; } for(moduleId in moreModules) { if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if(parentJsonpFunction) parentJsonpFunction(data); while(resolves.length) { resolves.shift()(); } }; , // The module cache var installedModules = {}; // object to store loaded and loading chunks // undefined = chunk not loaded, null = chunk preloaded/prefetched // Promise = chunk loading, 0 = chunk loaded var installedChunks = { "main": 0 }; // script path function function jsonpScriptSrc(chunkId) { return __webpack_require__.p + "" + chunkId + ".bundle." + "eecd41ca7ca8f56e3293" + ".js" },,// The require function,function __webpack_require__(moduleId) {, // Check if module is in cache if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // Create a new module (and put it into the cache) var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // Execute the module function modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // Flag the module as loaded module.l = true; // Return the exports of the module return module.exports;,},,// This file contains only the entry chunk. // The chunk loading function for additional chunks __webpack_require__.e = function requireEnsure(chunkId) { var promises = []; // JSONP chunk loading for javascript var installedChunkData = installedChunks[chunkId]; if(installedChunkData !== 0) { // 0 means "already installed". // a Promise means "currently loading". if(installedChunkData) { promises.push(installedChunkData[2]); } else { // setup Promise in chunk cache var promise = new Promise(function(resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); promises.push(installedChunkData[2] = promise); // start chunk loading var script = document.createElement('script'); var onScriptComplete; script.charset = 'utf-8'; script.timeout = 120; if (__webpack_require__.nc) { script.setAttribute("nonce", __webpack_require__.nc); } script.src = jsonpScriptSrc(chunkId); // create error before stack unwound to get useful stacktrace later var error = new Error(); onScriptComplete = function (event) { // avoid mem leaks in IE. script.onerror = script.onload = null; clearTimeout(timeout); var chunk = installedChunks[chunkId]; if(chunk !== 0) { if(chunk) { var errorType = event && (event.type === 'load' ? 'missing' : event.type); var realSrc = event && event.target && event.target.src; error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')'; error.name = 'ChunkLoadError'; error.type = errorType; error.request = realSrc; chunk[1](error); } installedChunks[chunkId] = undefined; } }; var timeout = setTimeout(function(){ onScriptComplete({ type: 'timeout', target: script }); }, 120000); script.onerror = script.onload = onScriptComplete; document.head.appendChild(script); } } return Promise.all(promises); }; // expose the modules object (__webpack_modules__) __webpack_require__.m = modules; // expose the module cache __webpack_require__.c = installedModules; // define getter function for harmony exports __webpack_require__.d = function(exports, name, getter) { if(!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { enumerable: true, get: getter }); } }; // define __esModule on exports __webpack_require__.r = function(exports) { if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); } Object.defineProperty(exports, '__esModule', { value: true }); }; // create a fake namespace object // mode & 1: value is a module id, require it // mode & 2: merge all properties of value into the ns // mode & 4: return value when already ns object // mode & 8|1: behave like require __webpack_require__.t = function(value, mode) { if(mode & 1) value = __webpack_require__(value); if(mode & 8) return value; if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; var ns = Object.create(null); __webpack_require__.r(ns); Object.defineProperty(ns, 'default', { enumerable: true, value: value }); if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); return ns; }; // getDefaultExport function for compatibility with non-harmony modules __webpack_require__.n = function(module) { var getter = module && module.__esModule ? function getDefault() { return module['default']; } : function getModuleExports() { return module; }; __webpack_require__.d(getter, 'a', getter); return getter; }; // Object.prototype.hasOwnProperty.call __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; // __webpack_public_path__ __webpack_require__.p = ""; // on error function for async loading __webpack_require__.oe = function(err) { console.error(err); throw err; };,,var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); jsonpArray.push = webpackJsonpCallback; jsonpArray = jsonpArray.slice(); for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); var parentJsonpFunction = oldJsonpFunction; ,// Load entry module and return exports return __webpack_require__(__webpack_require__.s = "./src/index.js");"
获得的source以下:
"/******/ (function(modules) { // webpackBootstrap ,[object Object],/******/ }) ,/************************************************************************/ ,/******/ (,{ , /***/ "./node_modules/css-loader/dist/cjs.js!./src/style.css": ,/*!*************************************************************!*\ , !*** ./node_modules/css-loader/dist/cjs.js!./src/style.css ***! , \*************************************************************/ ,/*! no static exports found */ ,/***/ (function(module, exports, __webpack_require__) { ,[object Object], /***/ }),, , /***/ "./node_modules/css-loader/dist/runtime/api.js": ,/*!*****************************************************!*\ , !*** ./node_modules/css-loader/dist/runtime/api.js ***! , \*****************************************************/ ,/*! no static exports found */ ,/***/ (function(module, exports, __webpack_require__) { ,"use strict"; ,[object Object], /***/ }),, , /***/ "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js": ,/*!****************************************************************************!*\ , !*** ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js ***! , \****************************************************************************/ ,/*! no static exports found */ ,/***/ (function(module, exports, __webpack_require__) { ,"use strict"; ,[object Object], /***/ }),, , /***/ "./src/index.js": ,/*!**********************!*\ , !*** ./src/index.js ***! , \**********************/ ,/*! no exports provided */ ,/***/ (function(module, __webpack_exports__, __webpack_require__) { ,"use strict"; ,[object Object], /***/ }),, , /***/ "./src/style.css": ,/*!***********************!*\ , !*** ./src/style.css ***! , \***********************/ ,/*! no static exports found */ ,/***/ (function(module, exports, __webpack_require__) { ,[object Object], /***/ }), /******/ },)
经历了上面全部的阶段以后,全部的最终代码信息已经保存在了 Compilation 的 assets 中,当 assets 资源相关的优化工做结束后,seal 阶段也就结束了。这时候执行 seal 函数接受到 callback,callback回溯到compiler.run中,执行compiler.emitAssets.
在这个方法当中首先触发 hooks.emit 钩子函数,即将进行写文件的流程。接下来开始建立目标输出文件夹,并执行 emitFiles 方法,将内存当中保存的 assets 资源输出到目标文件夹当中,这样就完成了内存中保存的 chunk 代码写入至最终的文件
参考资料: