Webpack 源碼是一個插件的架構,他的不少功能都是經過諸多的內置插件實現的。Webpack爲此專門本身寫一個插件系統,叫 Tapable 主要提供了註冊和調用插件的功能。css
tabpable是一個事件發佈訂閱插件,它支持同步和異步兩種;在須要使用的類上繼承tabpable,而且該類的構造函數中使用this.hooks
添加事件名稱。node
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
break: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
複製代碼
要使用訂閱功能,須要先拿到上面說到的類實例,經過實例對象.hooks.break.tap
來訂閱。webpack
myCar.hooks.break.tap("WarningLampPlugin", () => warningLamp.on());
複製代碼
在須要觸發的時機調用this.hooks.accelerate.call
就能夠觸發訂閱accelerate
的全部監聽函數,newSpeed是傳入的參數。git
setSpeed(newSpeed) {
this.hooks.accelerate.call(newSpeed);
}
複製代碼
webpack從配置初始化到build完成定義了一個生命週期,在這個生命週中的每個階段定義一些完成不一樣的功能的含義,webpack的流程就是定義了一個規範,不管是內部插件仍是自定義插件只要遵循這個規範就能完成構建;上面提到了webpack是一個插件架構,webpack主要是使用Compiler
和Compilation
類來控制webpack的整個生命週期,定義執行流程;他們都繼承了tabpable而且經過tabpable來註冊了生命週期中的每個流程須要觸發的事件。webpack內部實現了一堆plugin,這些內部plugin是webpack打包構建過程當中的功能實現,訂閱感興趣的事件,在執行流程中調用不一樣的訂閱函數就構成了webpack的完整生命週期。github
Webpack首先會把配置參數和命令行的參數及默認參數合併,並初始化須要使用的插件和配置插件等執行環境所須要的參數;初始化完成後會調用Compiler的run來真正啓動webpack編譯構建過程,webpack的構建流程包括compile、make、build、seal、emit階段,執行完這些階段就完成了構建過程。web
compilation
,他會註冊好不一樣類型的module對應的 factory,否則後面碰到了就不知道如何處理了make
階段,會從 entry
開始進行兩步操做:compilation.seal
進入 render
階段,根據以前收集的依賴,決定生成多少文件,每一個文件的內容是什麼首先從bin/webpack.js開始調用webpack-cli插件的
./bin/cli.js`文件,在cli.js中使用yargs來解析命令行參數併合並配置文件中的參數(options),而後調用lib/webpack.js實例化compiler。設計模式
實例化compiler是在lib/webpack.js中完成的,首先會檢查配置參數是否合法;而後根據傳入的參數判斷是否爲數組,如果數組則建立多個compiler,不然建立一個compiler;下面以建立一個compiler來說述,首先會調用WebpackOptionsDefaulter把傳入的參數和默認參數合併獲得新的options,建立Compiler,建立讀寫文件對象和執行註冊配置的plugin插件,最後經過WebpackOptionsApply初始化一堆構建須要的內部默認插件。數組
實例compiler後根據options的watch判斷是否啓動了watch,若是啓動watch了就調用compiler.watch
來監控構建文件,不然啓動compiler.run
來構建文件。緩存
接下來正式進入webpack的構建流程,webpack構建流程入口是compiler的run或者watch方法,下面經過run來描述編譯過程;在run方法中先執行beforeRun、run鉤子函數後進入compile,能夠寫插件在構建以前來處理一些初始化數據。bash
在進入構建以前解釋兩個類
- Compiler:該類是webpack的神經中樞,一方面全部的配置數據都存儲在該實例上,另外一方面它是在構建過程當中控制整個大致的流程。
- Compilation:該類是webpack的cto,全部的構建過程當中產生的構建數據都存儲在該對象上,它掌控着構建過程當中每個細節流程。
在run中先實例化normalModuleFactory等參數,而後調用this.hooks.beforeCompile事件執行一些編譯以前須要處理的插件,最後才執行
this.hooks.compile事件(好比compile鉤子中會執行DllReferencePlugin,在這裏註冊代理插件);this.hooks.compile
執行完後實例化Compilation對象,並調用this.hooks.compilation
通知感興趣的插件,好比在compilation.dependencyFactories中添加依賴工廠類等操做。compile階段主要是爲了進入make階段作準備,make階段纔是從入口開始遞歸查找構建模塊。
make是compilation初始化完成觸發的事件,該事件通常狀況是通知在WebpackOptionsApply中註冊的EntryOptionPlugin插件,在該插件中使用entries參數建立一個單入口(SingleEntryDependency)或者多入口(MultiEntryDependency)依賴,多個入口時在make事件上註冊多個相同的監聽,並行執行多個入口;而後調用compilation.addEntry(context, dep, name, callback)
正式進入make階段。
addEntry中並無作任何事,就調用this._addModuleChain方法,在_addModuleChain中根據依賴查找對應的工廠函數,並調用工廠函數的create來生成一個空的MultModule對象,而且把MultModule對象存入compilation的modules中後執行MultModule.build,由於是入口module,因此在build中沒處理任何事直接調用了afterBuild;在afterBuild中判斷是否有依賴,如果葉子結點直接結束,不然調用processModuleDependencies方法來查找依賴;由於入口傳入了一個SingleEntryDependency,因此下面正式講述從SingleEntryDependency開始的構建。
上面提到入口會建立一個SingleEntryDependency傳入,因此上面講述的afterBuild確定至少存在一個依賴,processModuleDependencies方法就會被調用;processModuleDependencies根據當前的module.dependencies對象查找該module依賴中全部須要加載的資源和對應的工廠類,並把module和須要加載資源的依賴做爲參數傳給addModuleDependencies方法;在addModuleDependencies中異步執行全部的資源依賴,在異步中調用依賴的工廠類的create去查找該資源的絕對路徑和該資源所依賴全部loader的絕對路徑,而且建立對應的module後返回;而後根據該moduel的資源路徑做爲key判斷該資源是否被加載過,若加載過直接把該資源引用指向加載過的module返回;不然調用this.buildModule方法執行module.build加載資源;build完成就獲得了loader處理事後的最終module了,而後遞歸調用afterBuild,直到全部的模塊都加載完成後make階段才結束。
在make階段webpack會根據模塊工廠(normalModuleFactory)的create去實例化module;實例化moduel後觸發this.hooks.module事件,若構建配置中註冊了DllReferencePlugin插件,DelegatedModuleFactoryPlugin會監聽this.hooks.module事件,在該插件裏判斷該moduel的路徑是否在this.options.content中,若存在則建立代理module(DelegatedModule)去覆蓋默認module;DelegatedModule對象的delegateData中存放manifest中對應的數據(文件路徑和id),因此DelegatedModule對象不會執行bulled,在生成源碼時只須要在使用的地方引入對應的id便可。
上面在make階段提到了build,可是沒有深刻講解,由於build是在module對象中執行,這節單獨說一下build是如何加載和執行loader最後查找該module的依賴後返回的。
在build中會調用doBuild去加載資源,doBuild中會傳入資源路徑和插件資源去調用loader-runner插件的runLoaders方法去加載和執行loader。執行完成後會返回以下圖的result結果,根據返回數據把源碼和sourceMap存儲在module的_source屬性上;doBuild的回調函數中調用Parser類生成AST語法樹,並根據AST語法樹生成依賴後回調buildModule方法返回compilation類。
runLoaders方法調用iteratePitchingLoaders去遞歸查找執行有pich屬性的loader;若存在多個pitch屬性的loader則依次執行全部帶pitch屬性的loader,執行完後逆向執行全部帶pitch屬性的normal的normal loader後返回result,沒有pitch屬性的loader就不會再執行;若loaders中沒有pitch屬性的loader則逆向執行loader;執行正常loader是在iterateNormalLoaders方法完成的,處理完全部loader後返回result;以下列是loader的執行規則。
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
複製代碼
在Parser類中調用acorn插件生產AST語法樹,acorn不在本文的分析範圍,有興趣的能夠去閱讀一下;Parser中生產AST語法樹後調用walkStatements方法分析語法樹,根據AST的node的type來遞歸查找每個node的類型和執行不一樣的邏輯,並建立依賴。
若是在webpack中使用MiniCssExtractPlugin插件把css單獨打包成文件,會在樣式處理規則中配置MiniCssExtractPlugin.loader
,當解析到css文件時,會首先執行MiniCssExtractPlugin的loader中實現的pitch方法,pitch方法會爲每個css模塊調用this._compilation.createChildCompiler
建立一個childCompiler和childCompilation;childCompiler控制完成該模塊的加載和構建後返回。childCompilation中構建的module是CssModule,而且使用type='css/mini-extract'來區分。
在seal中MiniCssExtractPlugin會根據module的type='css/mini-extract'的類型來區分是否css樣式,進行單獨處理,而其餘js模版不認識type='css/mini-extract'類型的module也就被過濾掉了,這樣就實現了樣式分離。
在全部的資源bulid完成後,webpack的make階段就結束了,make階段是最耗時的,由於會進行文件路徑解析和讀文件等IO流操做;make結束後會把全部的編譯完成的module存放在compilation的modules數組中,modules中的全部的module會構成一個圖。
在全部模塊及其依賴模塊 build 完成後,webpack 會監聽
seal
事件調用各插件對構建後的結果進行封裝,要逐次對每一個 module 和 chunk 進行整理,生成編譯後的源碼,合併,拆分,生成 hash 。 同時這是咱們在開發時進行代碼優化和功能添加的關鍵環節。
在seal中首先會觸發optimizeDependencies
類型的一些事件去優化依賴(好比tree shaking就是在這個地方執行的),你們要注意一點是在優化類插件中是不能有異步的;優化完成後根據入口module建立chunk,若是是單入口就只有一個chunk,多入口就有多個chunk;該階段結束後會根據chunk遞歸分析查找module中存在的異步導module,並以該module爲節點建立一個chunk,和入口建立的chunk區別在於後面調用模版不同。全部chunk執行完後會觸發optimizeModules
和optimizeChunks
等優化事件通知感興趣的插件進行優化處理。全部優化完成後給chunk生成hash而後調用createChunkAssets
來根據模版生成源碼對象;使用summarizeDependencies
把全部解析的文件緩存起來,最後調用插件生成soureMap和最終的數據,下圖是seal階段的流程圖。
在封裝過程當中,webpack 會調用 Compilation 中的
createChunkAssets
方法進行打包後代碼的生成。 createChunkAssets 流程以下
從上圖能夠看出不一樣的chunk處理模版不同,根據chunk的entry判斷是選擇mainTemplate(入口文件打包模版)仍是chunkTemplate(異步加載js打包模版);選擇模版後根據模版的template.getRenderManifest生成manifest對象,該對象中的render方法就是chunk打包封裝的入口;mainTemplate和chunkTemplate的惟一區別就是mainTemplate多了wepback執行的bootsrap代碼。當調用render時會調用template.renderChunkModules方法,該方法會建立一個ConcatSource容器用來存放chunk的源碼,該方法接下來會對當前chunk的module遍歷並執行moduleTemplate.render得到每個module的源碼;在moduleTemplate.render中獲取源碼後會觸發插件去封裝成wepack須要的代碼格式;當全部的module都生成完後放入ConcatSource中返回;並以該chunk的輸出文件名稱爲key存放在Compilation的assets中。
經過seal階段各類優化和生成最終代碼會存放在Compilation的assets屬性上,assets是一個對象,以最終輸出名稱爲key存放的輸出對象,每個輸出文件對應着一個輸出對象,以下圖所示。
最後一步,webpack 調用 Compiler 中的 emitAssets()
,按照 output 中的配置項異步將文件輸出到了對應的 path 中,從而 webpack 整個打包過程結束。要注意的是,若想對結果進行處理,則須要在 emit
觸發後對自定義插件進行擴展。
當配置了watch時webpack-dev-middleware 將 webpack 本來的 outputFileSystem 替換成了MemoryFileSystem(memory-fs 插件) 實例。
當執行watch時會實例化一個Watching對象,監控和構建打包都是Watching實例來控制;在Watching構造函數中設置變化延遲通知時間(默認200),而後調用_go方法;webpack首次構建和後續的文件變化從新構建都是_執行_go方法,在__go方法中調用this.compiler.compile啓動編譯。webpack構建完成後會觸發 _done方法,在 _done方法中調用this.watch方法,傳入compilation.fileDependencies和compilation.contextDependencies須要監控的文件夾和目錄;在watch中調用this.compiler.watchFileSystem.watch方法正式開始建立監聽。
在this.compiler.watchFileSystem.watch中每次會從新建立一個Watchpack實例,建立完成後監控aggregated事件和觸發this.watcher.watch(files.concat(missing), dirs.concat(missing), startTime)
方法,而且關閉舊的Watchpack實例;在watch中會調用WatcherManager爲每個文件所在目錄建立的文件夾建立一個DirectoryWatcher對象,在DirectoryWatcher對象的watch構造函數中調用chokidar插件進行文件夾監聽,而且綁定一堆觸發事件並返回watcher;Watchpack會給每個watcher註冊一個監聽change事件,每當有文件變化時會觸發change事件。
在Watchpack插件監聽的文件變化後設置一個定時器去延遲觸發change事件,解決屢次快速修改時頻繁觸發問題。
當文件變化時NodeWatchFileStstem中的aggregated監聽事件根據watcher獲取每個監聽文件的最後修改時間,並把該對象存放在this.compiler.fileTimestamps上而後觸發 _go方法去構建。
在compile中會把this.fileTimestamps賦值給compilation對象,在make階段從入口開始,遞歸構建全部module,和首次構建不一樣的是在compilation.addModule方法會首先去緩存中根據資源路徑取出module,而後拿module.buildTimestamp(module最後修改時間)和fileTimestamps中的該文件最後修改時間進行比較,若文件修改時間大於buildTimestamp則從新bulid該module,不然遞歸查找該module的的依賴。
在webpack構建過程當中是文件解析和模塊構建比較耗時,因此webpack在build過程當中已經把文件絕對路徑和module已經緩存起來,在rebuild時只會操做變化的module,這樣能夠大大提高webpack的rebuild過程。
剛開始讀webpack源碼時心中的萬馬奔騰,MMMP數不清的事件名、看不完的內部插件,各類事件之間調過去調過來;~~~就這樣吧,~_~