摘要:最后執(zhí)行了的回調(diào)函數(shù),觸發(fā)了事件點(diǎn),并回到函數(shù)的回調(diào)函數(shù)觸發(fā)了事件點(diǎn)執(zhí)行對(duì)于當(dāng)前模塊,或許存在著多個(gè)依賴模塊。
系列文章
Webpack系列-第一篇基礎(chǔ)雜記
Webpack系列-第二篇插件機(jī)制雜記
Webpack系列-第三篇流程雜記
本文章個(gè)人理解, 只是為了理清webpack流程, 沒(méi)有關(guān)注內(nèi)部過(guò)多細(xì)節(jié), 如有錯(cuò)誤, 請(qǐng)輕噴~
調(diào)試1.使用以下命令運(yùn)行項(xiàng)目,./scripts/build.js是你想要開(kāi)始調(diào)試的地方
node --inspect-brk ./scripts/build.js --inline --progress
2.打開(kāi)chrome://inspect/#devices即可調(diào)試
流程圖 入口入口處在bulid.js,可以看到其中的代碼是先實(shí)例化webpack,然后調(diào)用compiler的run方法。
function build(previousFileSizes) { let compiler = webpack(config); return new Promise((resolve, reject) => { compiler.run((err, stats) => { ... }); }entry-option(compiler) webpack.js
webpack在node_moduls下面的webpacklibwebpack.js(在此前面有入口參數(shù)合并),找到該文件可以看到相關(guān)的代碼如下
const webpack = (options, callback) => { ...... let compiler; // 處理多個(gè)入口 if (Array.isArray(options)) { compiler = new MultiCompiler(options.map(options => webpack(options))); } else if (typeof options === "object") { // webpack的默認(rèn)參數(shù) options = new WebpackOptionsDefaulter().process(options); console.log(options) // 見(jiàn)下圖 // 實(shí)例化compiler compiler = new Compiler(options.context); compiler.options = options; // 對(duì)webpack的運(yùn)行環(huán)境處理 new NodeEnvironmentPlugin().apply(compiler); // 根據(jù)上篇的tabpable可知 這里是為了注冊(cè)插件 if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { plugin.apply(compiler); } } // 觸發(fā)兩個(gè)事件點(diǎn) environment/afterEnviroment compiler.hooks.environment.call(); compiler.hooks.afterEnvironment.call(); // 設(shè)置compiler的屬性并調(diào)用默認(rèn)配置的插件,同時(shí)觸發(fā)事件點(diǎn)entry-option compiler.options = new WebpackOptionsApply().process(options, compiler); } else { throw new Error("Invalid argument: options"); } if (callback) { ...... compiler.run(callback); } return compiler; };
可以看出options保存的就是本次webpack的一些配置參數(shù),而其中的plugins屬性則是webpack中最重要的插件。
process(options, compiler) { let ExternalsPlugin; compiler.outputPath = options.output.path; compiler.recordsInputPath = options.recordsInputPath || options.recordsPath; compiler.recordsOutputPath = options.recordsOutputPath || options.recordsPath; compiler.name = options.name; compiler.dependencies = options.dependencies; if (typeof options.target === "string") { let JsonpTemplatePlugin; let FetchCompileWasmTemplatePlugin; let ReadFileCompileWasmTemplatePlugin; let NodeSourcePlugin; let NodeTargetPlugin; let NodeTemplatePlugin; switch (options.target) { case "web": JsonpTemplatePlugin = require("./web/JsonpTemplatePlugin"); FetchCompileWasmTemplatePlugin = require("./web/FetchCompileWasmTemplatePlugin"); NodeSourcePlugin = require("./node/NodeSourcePlugin"); new JsonpTemplatePlugin().apply(compiler); new FetchCompileWasmTemplatePlugin({ mangleImports: options.optimization.mangleWasmImports }).apply(compiler); new FunctionModulePlugin().apply(compiler); new NodeSourcePlugin(options.node).apply(compiler); new LoaderTargetPlugin(options.target).apply(compiler); break; case "webworker":...... ...... } } new JavascriptModulesPlugin().apply(compiler); new JsonModulesPlugin().apply(compiler); new WebAssemblyModulesPlugin({ mangleImports: options.optimization.mangleWasmImports }).apply(compiler); new EntryOptionPlugin().apply(compiler); // 觸發(fā)事件點(diǎn)entry-options并傳入?yún)?shù) context和entry compiler.hooks.entryOption.call(options.context, options.entry); new CompatibilityPlugin().apply(compiler); ...... new ImportPlugin(options.module).apply(compiler); new SystemPlugin(options.module).apply(compiler); }run(compiler)
調(diào)用run時(shí),會(huì)先在內(nèi)部觸發(fā)beforeRun事件點(diǎn),然后再在讀取recodes(關(guān)于records可以參考該文檔)之前觸發(fā)run事件點(diǎn),這兩個(gè)事件都是異步的形式,注意run方法是實(shí)際上整個(gè)webpack打包流程的入口??梢钥吹?,最后調(diào)用的是compile方法,同時(shí)傳入的是onCompiled函數(shù)
run(callback) { if (this.running) return callback(new ConcurrentCompilationError()); const finalCallback = (err, stats) => { ...... }; this.running = true; const onCompiled = (err, compilation) => { .... }; this.hooks.beforeRun.callAsync(this, err => { if (err) return finalCallback(err); this.hooks.run.callAsync(this, err => { if (err) return finalCallback(err); this.readRecords(err => { if (err) return finalCallback(err); this.compile(onCompiled); }); }); }); }compile(compiler)
compile方法主要上觸發(fā)beforeCompile、compile、make等事件點(diǎn),并實(shí)例化compilation,這里我們可以看到傳給compile的newCompilationParams參數(shù), 這個(gè)參數(shù)在后面相對(duì)流程中也是比較重要,可以在這里先看一下
compile(callback) { const params = this.newCompilationParams(); // 觸發(fā)事件點(diǎn)beforeCompile,并傳入?yún)?shù)CompilationParams this.hooks.beforeCompile.callAsync(params, err => { if (err) return callback(err); // 觸發(fā)事件點(diǎn)compile,并傳入?yún)?shù)CompilationParams this.hooks.compile.call(params); // 實(shí)例化compilation const compilation = this.newCompilation(params); // 觸發(fā)事件點(diǎn)make this.hooks.make.callAsync(compilation, err => { .... }); }); }
newCompilationParams返回的參數(shù)分別是兩個(gè)工廠函數(shù)和一個(gè)Set集合
newCompilationParams() { const params = { normalModuleFactory: this.createNormalModuleFactory(), contextModuleFactory: this.createContextModuleFactory(), compilationDependencies: new Set() }; return params; }compilation(compiler)
從上面的compile方法看, compilation是通過(guò)newCompilation方法調(diào)用生成的,然后觸發(fā)事件點(diǎn)thisCompilation和compilation,可以看出compilation在這兩個(gè)事件點(diǎn)中最早當(dāng)成參數(shù)傳入,如果你在編寫(xiě)插件的時(shí)候需要盡快使用該對(duì)象,則應(yīng)該在該兩個(gè)事件中進(jìn)行。
createCompilation() { return new Compilation(this); } newCompilation(params) { const compilation = this.createCompilation(); compilation.fileTimestamps = this.fileTimestamps; compilation.contextTimestamps = this.contextTimestamps; compilation.name = this.name; compilation.records = this.records; compilation.compilationDependencies = params.compilationDependencies; // 觸發(fā)事件點(diǎn)thisCompilation和compilation, 同時(shí)傳入?yún)?shù)compilation和params this.hooks.thisCompilation.call(compilation, params); this.hooks.compilation.call(compilation, params); return compilation; }
下面是打印出來(lái)的compilation屬性
關(guān)于這里為什么要有thisCompilation這個(gè)事件點(diǎn)和子編譯器(childCompiler),可以參考該文章
總結(jié)起來(lái)就是:
子編譯器擁有完整的模塊解析和chunk生成階段,但是少了某些事件點(diǎn),如"make", "compile", "emit", "after-emit", "invalid", "done", "this-compilation"。 也就是說(shuō)我們可以利用子編譯器來(lái)獨(dú)立(于父編譯器)跑完一個(gè)核心構(gòu)建流程,額外生成一些需要的模塊或者chunk。make(compiler)
從上面的compile方法知道, 實(shí)例化Compilation后就會(huì)觸發(fā)make事件點(diǎn)了。
觸發(fā)了make時(shí), 因?yàn)閣ebpack在前面實(shí)例化SingleEntryPlugin或者M(jìn)ultleEntryPlugin,SingleEntryPlugin則在其apply方法中注冊(cè)了一個(gè)make事件,
apply(compiler) { compiler.hooks.compilation.tap( "SingleEntryPlugin", (compilation, { normalModuleFactory }) => { compilation.dependencyFactories.set( SingleEntryDependency, normalModuleFactory // 工廠函數(shù),存在compilation的dependencyFactories集合 ); } ); compiler.hooks.make.tapAsync( "SingleEntryPlugin", (compilation, callback) => { const { entry, name, context } = this; const dep = SingleEntryPlugin.createDependency(entry, name); // 進(jìn)入到addEntry compilation.addEntry(context, dep, name, callback); } ); }
事實(shí)上addEntry調(diào)用的是Comilation._addModuleChain,acquire函數(shù)比較簡(jiǎn)單,主要是處理module時(shí)如果任務(wù)太多,就將moduleFactory.create存入隊(duì)列等待
_addModuleChain(context, dependency, onModule, callback) { ...... // 取出對(duì)應(yīng)的Factory const Dep = /** @type {DepConstructor} */ (dependency.constructor); const moduleFactory = this.dependencyFactories.get(Dep); ...... this.semaphore.acquire(() => { moduleFactory.create( { contextInfo: { issuer: "", compiler: this.compiler.name }, context: context, dependencies: [dependency] }, (err, module) => { ...... } ); }); }
moduleFactory.create則是收集一系列信息然后創(chuàng)建一個(gè)module傳入回調(diào)
buildModule(compilation)回調(diào)函數(shù)主要上執(zhí)行buildModule方法
this.buildModule(module, false, null, null, err => { ...... afterBuild(); });
buildModule(module, optional, origin, dependencies, thisCallback) { // 處理回調(diào)函數(shù) let callbackList = this._buildingModules.get(module); if (callbackList) { callbackList.push(thisCallback); return; } this._buildingModules.set(module, (callbackList = [thisCallback])); const callback = err => { this._buildingModules.delete(module); for (const cb of callbackList) { cb(err); } }; // 觸發(fā)buildModule事件點(diǎn) this.hooks.buildModule.call(module); module.build( this.options, this, this.resolverFactory.get("normal", module.resolveOptions), this.inputFileSystem, error => { ...... } ); }
build方法中調(diào)用的是doBuild,doBuild又通過(guò)runLoaders獲取loader相關(guān)的信息并轉(zhuǎn)換成webpack需要的js文件,最后通過(guò)doBuild的回調(diào)函數(shù)調(diào)用parse方法,創(chuàng)建依賴Dependency并放入依賴數(shù)組
return this.doBuild(options, compilation, resolver, fs, err => { // 在createLoaderContext函數(shù)中觸發(fā)事件normal-module-loader const loaderContext = this.createLoaderContext( resolver, options, compilation, fs ); ..... const handleParseResult = result => { this._lastSuccessfulBuildMeta = this.buildMeta; this._initBuildHash(compilation); return callback(); }; try { // 調(diào)用parser.parse const result = this.parser.parse( this._ast || this._source.source(), { current: this, module: this, compilation: compilation, options: options }, (err, result) => { if (err) { handleParseError(err); } else { handleParseResult(result); } } ); if (result !== undefined) { // parse is sync handleParseResult(result); } } catch (e) { handleParseError(e); } });
在ast轉(zhuǎn)換過(guò)程中也很容易得到了需要依賴的哪些其他模塊。
succeedModule(compilation)最后執(zhí)行了module.build的回調(diào)函數(shù),觸發(fā)了事件點(diǎn)succeedModule,并回到Compilation.buildModule函數(shù)的回調(diào)函數(shù)
module.build( this.options, this, this.resolverFactory.get("normal", module.resolveOptions), this.inputFileSystem, error => { ...... 觸發(fā)了事件點(diǎn)succeedModule this.hooks.succeedModule.call(module); return callback(); } ); this.buildModule(module, false, null, null, err => { ...... // 執(zhí)行afterBuild afterBuild(); });
對(duì)于當(dāng)前模塊,或許存在著多個(gè)依賴模塊。當(dāng)前模塊會(huì)開(kāi)辟一個(gè)依賴模塊的數(shù)組,在遍歷 AST 時(shí),將 require() 中的模塊通過(guò) addDependency() 添加到數(shù)組中。當(dāng)前模塊構(gòu)建完成后,webpack 調(diào)用 processModuleDependencies 開(kāi)始遞歸處理依賴的 module,接著就會(huì)重復(fù)之前的構(gòu)建步驟。
Compilation.prototype.addModuleDependencies = function(module, dependencies, bail, cacheGroup, recursive, callback) { // 根據(jù)依賴數(shù)組(dependencies)創(chuàng)建依賴模塊對(duì)象 var factories = []; for (var i = 0; i < dependencies.length; i++) { var factory = _this.dependencyFactories.get(dependencies[i][0].constructor); factories[i] = [factory, dependencies[i]]; } ... // 與當(dāng)前模塊構(gòu)建步驟相同 }
最后, 所有的模塊都會(huì)被放入到Compilation的modules里面, 如下:
總結(jié)一下:
module 是 webpack 構(gòu)建的核心實(shí)體,也是所有 module 的 父類(lèi),它有幾種不同子類(lèi):NormalModule , MultiModule , ContextModule , DelegatedModule 等,一個(gè)依賴對(duì)象(Dependency,還未被解析成模塊實(shí)例的依賴對(duì)象。比如我們運(yùn)行 webpack 時(shí)傳入的入口模塊,或者一個(gè)模塊依賴的其他模塊,都會(huì)先生成一個(gè) Dependency 對(duì)象。)經(jīng)過(guò)對(duì)應(yīng)的工廠對(duì)象(Factory)創(chuàng)建之后,就能夠生成對(duì)應(yīng)的模塊實(shí)例(Module)。seal(compilation)
構(gòu)建module后, 就會(huì)調(diào)用Compilation.seal, 該函數(shù)主要是觸發(fā)了事件點(diǎn)seal, 構(gòu)建chunk, 在所有 chunks 生成之后,webpack 會(huì)對(duì) chunks 和 modules 進(jìn)行一些優(yōu)化相關(guān)的操作,比如分配id、排序等,并且觸發(fā)一系列相關(guān)的事件點(diǎn)
seal(callback) { // 觸發(fā)事件點(diǎn)seal this.hooks.seal.call(); // 優(yōu)化 ...... this.hooks.afterOptimizeDependencies.call(this.modules); this.hooks.beforeChunks.call(); // 生成chunk for (const preparedEntrypoint of this._preparedEntrypoints) { const module = preparedEntrypoint.module; const name = preparedEntrypoint.name; // 整理每個(gè)Module和chunk,每個(gè)chunk對(duì)應(yīng)一個(gè)輸出文件。 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); GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk); GraphHelpers.connectChunkAndModule(chunk, module); chunk.entryModule = module; chunk.name = name; this.assignDepth(module); } this.processDependenciesBlocksForChunkGroups(this.chunkGroups.slice()); this.sortModules(this.modules); this.hooks.afterChunks.call(this.chunks); this.hooks.optimize.call(); ...... this.hooks.afterOptimizeModules.call(this.modules); ...... this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups); this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => { ...... this.hooks.beforeChunkAssets.call(); this.createChunkAssets(); // 生成對(duì)應(yīng)的Assets this.hooks.additionalAssets.callAsync(...) }); }
每個(gè) chunk 的生成就是找到需要包含的 modules。這里大致描述一下 chunk 的生成算法:
1.webpack 先將 entry 中對(duì)應(yīng)的 module 都生成一個(gè)新的 chunk
2.遍歷 module 的依賴列表,將依賴的 module 也加入到 chunk 中
3.如果一個(gè)依賴 module 是動(dòng)態(tài)引入的模塊,那么就會(huì)根據(jù)這個(gè) module 創(chuàng)建一個(gè)新的 chunk,繼續(xù)遍歷依賴
4.重復(fù)上面的過(guò)程,直至得到所有的 chunks
chunk屬性圖
在觸發(fā)這兩個(gè)事件點(diǎn)的中間時(shí), 會(huì)調(diào)用Compilation.createCHunkAssets來(lái)創(chuàng)建assets,
createChunkAssets() { ...... // 遍歷chunk for (let i = 0; i < this.chunks.length; i++) { const chunk = this.chunks[i]; chunk.files = []; let source; let file; let filenameTemplate; try { // 調(diào)用何種Template const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate; 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) { ..... } ..... // 寫(xiě)入assets對(duì)象 this.assets[file] = source; chunk.files.push(file); this.hooks.chunkAsset.call(chunk, file); alreadyWrittenFiles.set(file, { hash: usedHash, source, chunk }); } } catch (err) { ...... } } }
createChunkAssets會(huì)生成文件名和對(duì)應(yīng)的文件內(nèi)容,并放入Compilation.assets對(duì)象, 這里有四個(gè)Template 的子類(lèi),分別是 MainTemplate.js , ChunkTemplate.js ,ModuleTemplate.js , HotUpdateChunkTemplate.js
MainTemplate.js: 對(duì)應(yīng)了在 entry 配置的入口 chunk 的渲染模板
ChunkTemplate: 動(dòng)態(tài)引入的非入口 chunk 的渲染模板
ModuleTemplate.js: chunk 中的 module 的渲染模板
HotUpdateChunkTemplate.js: 對(duì)熱替換模塊的一個(gè)處理。
模塊封裝(引用自http://taobaofed.org/blog/201...)
模塊在封裝的時(shí)候和它在構(gòu)建時(shí)一樣,都是調(diào)用各模塊類(lèi)中的方法。封裝通過(guò)調(diào)用 module.source() 來(lái)進(jìn)行各操作,比如說(shuō) require() 的替換。
MainTemplate.prototype.requireFn = "__webpack_require__"; MainTemplate.prototype.render = function(hash, chunk, moduleTemplate, dependencyTemplates) { var buf = []; // 每一個(gè)module都有一個(gè)moduleId,在最后會(huì)替換。 buf.push("function " + this.requireFn + "(moduleId) {"); buf.push(this.indent(this.applyPluginsWaterfall("require", "", chunk, hash))); buf.push("}"); buf.push(""); ... // 其余封裝操作 };
最后看看Compilation.assets對(duì)象
最后一步,webpack 調(diào)用 Compiler 中的 emitAssets() ,按照 output 中的配置項(xiàng)將文件輸出到了對(duì)應(yīng)的 path 中,從而 webpack 整個(gè)打包過(guò)程結(jié)束。要注意的是,若想對(duì)結(jié)果進(jìn)行處理,則需要在 emit 觸發(fā)后對(duì)自定義插件進(jìn)行擴(kuò)展。
總結(jié)webpack的內(nèi)部核心還是在于compilationcompilermodulechunk等對(duì)象或者實(shí)例。寫(xiě)下這篇文章也有助于自己理清思路,學(xué)海無(wú)涯~~~
引用玩轉(zhuǎn)webpack(一):webpack的基本架構(gòu)和構(gòu)建流程
玩轉(zhuǎn)webpack(二):webpack的核心對(duì)象
細(xì)說(shuō) webpack 之流程篇
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/109130.html
摘要:系列文章系列第一篇基礎(chǔ)雜記系列第二篇插件機(jī)制雜記系列第三篇流程雜記前言本身并不難,他所完成的各種復(fù)雜炫酷的功能都依賴于他的插件機(jī)制。的插件機(jī)制依賴于一個(gè)核心的庫(kù),。是什么是一個(gè)類(lèi)似于的的庫(kù)主要是控制鉤子函數(shù)的發(fā)布與訂閱。 系列文章 Webpack系列-第一篇基礎(chǔ)雜記 Webpack系列-第二篇插件機(jī)制雜記 Webpack系列-第三篇流程雜記 前言 webpack本身并不難,他所完成...
摘要:系列文章系列第一篇基礎(chǔ)雜記系列第二篇插件機(jī)制雜記系列第三篇流程雜記前言公司的前端項(xiàng)目基本都是用來(lái)做工程化的,而雖然只是一個(gè)工具,但內(nèi)部涉及到非常多的知識(shí),之前一直靠來(lái)解決問(wèn)題,之知其然不知其所以然,希望這次能整理一下相關(guān)的知識(shí)點(diǎn)。 系列文章 Webpack系列-第一篇基礎(chǔ)雜記 Webpack系列-第二篇插件機(jī)制雜記 Webpack系列-第三篇流程雜記 前言 公司的前端項(xiàng)目基本都是用...
摘要:入口模塊返回的賦值給總結(jié)在剖析了整體的流程之后,可以看到相關(guān)的技術(shù)細(xì)節(jié)還是比較清晰的,學(xué)無(wú)止境引用混合使用詳解的語(yǔ)法前端模塊化規(guī)范 前言 CMDAMD簡(jiǎn)介 Commonjs簡(jiǎn)介 Module簡(jiǎn)介 Common和Module的區(qū)別 Module與webpack Module與Babel 一些問(wèn)題 總結(jié) 引用 前言 前端模塊化在近幾年層出不窮,有Node的CommonJs,也有屬于cl...
摘要:流程控制首先來(lái)介紹程序的流程控制。后面跟一個(gè)代碼塊邏輯關(guān)系是當(dāng)布爾表達(dá)式為真的時(shí)候執(zhí)行代碼塊,為假的時(shí)候不執(zhí)行。 流程控制 首先來(lái)介紹程序的流程控制。 所謂的流程控制,就是在我們前面的課程中我們已經(jīng)學(xué)過(guò)了變量,數(shù)據(jù)類(lèi)型,運(yùn)算符,表達(dá)式,這些都是計(jì)算機(jī)編程的基本元素,但是我們程序的基本執(zhí)行單元應(yīng)該是語(yǔ)句,程序執(zhí)行,執(zhí)行的不是表達(dá)式,而是執(zhí)行語(yǔ)句。就好像我們小時(shí)候先學(xué)認(rèn)字兒,再學(xué)組詞,但最...
摘要:前提最近通過(guò)閱讀官方文檔的事件模塊,有了一些思考和收獲,在這里記錄一下調(diào)用方法時(shí)需要手動(dòng)綁定先從一段官方代碼看起代碼中的注釋提到了一句話的綁定是必須的,其實(shí)這一塊是比較容易理解的,因?yàn)檫@并不是的一個(gè)特殊點(diǎn),而是這門(mén)語(yǔ)言的特性。 前提 最近通過(guò)閱讀React官方文檔的事件模塊,有了一些思考和收獲,在這里記錄一下~ 調(diào)用方法時(shí)需要手動(dòng)綁定this 先從一段官方代碼看起: showImg(...
閱讀 768·2021-11-18 10:02
閱讀 2303·2021-11-15 18:13
閱讀 3313·2021-11-15 11:38
閱讀 3072·2021-09-22 15:55
閱讀 3744·2021-08-09 13:43
閱讀 2521·2021-07-25 14:19
閱讀 2521·2019-08-30 14:15
閱讀 3509·2019-08-30 14:15