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 代碼寫入至最終的文件
參考資料: