webpack4原始碼分析

外星人180發表於2019-03-04

webpack設計模式

Webpack 原始碼是一個外掛的架構,他的很多功能都是通過諸多的內建外掛實現的。Webpack為此專門自己寫一個外掛系統,叫 Tapable 主要提供了註冊和呼叫外掛的功能。

Tapable

tabpable是一個事件釋出訂閱外掛,它支援同步和非同步兩種;在需要使用的類上繼承tabpable,並且該類的建構函式中使用this.hooks新增事件名稱。

 this.hooks = {
            accelerate: new SyncHook(["newSpeed"]),
            break: new SyncHook(),
            calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
        };
複製程式碼
訂閱

要使用訂閱功能,需要先拿到上面說到的類例項,通過例項物件.hooks.break.tap來訂閱。

myCar.hooks.break.tap("WarningLampPlugin", () => warningLamp.on());
複製程式碼
釋出

在需要觸發的時機呼叫this.hooks.accelerate.call就可以觸發訂閱accelerate的所有監聽函式,newSpeed是傳入的引數。

setSpeed(newSpeed) {
        this.hooks.accelerate.call(newSpeed);
    }
複製程式碼
webpack的外掛架構

webpack從配置初始化到build完成定義了一個生命週期,在這個生命週中的每一個階段定義一些完成不同的功能的含義,webpack的流程就是定義了一個規範,無論是內部外掛還是自定義外掛只要遵循這個規範就能完成構建;上面提到了webpack是一個外掛架構,webpack主要是使用CompilerCompilation類來控制webpack的整個生命週期,定義執行流程;他們都繼承了tabpable並且通過tabpable來註冊了生命週期中的每一個流程需要觸發的事件。webpack內部實現了一堆plugin,這些內部plugin是webpack打包構建過程中的功能實現,訂閱感興趣的事件,在執行流程中呼叫不同的訂閱函式就構成了webpack的完整生命週期。

webpack流程概述

Webpack首先會把配置引數和命令列的引數及預設引數合併,並初始化需要使用的外掛和配置外掛等執行環境所需要的引數;初始化完成後會呼叫Compiler的run來真正啟動webpack編譯構建過程,webpack的構建流程包括compile、make、build、seal、emit階段,執行完這些階段就完成了構建過程。

  • 根據我們的webpack配置註冊好對應的外掛呼叫 compile.run 進入編譯階段
  • 在編譯的第一階段是 compilation,他會註冊好不同型別的module對應的 factory,不然後面碰到了就不知道如何處理了
  • 進入 make 階段,會從 entry 開始進行兩步操作:
  • 第一步是呼叫 loaders 對模組的原始程式碼進行編譯,轉換成標準的JS程式碼
  • 第二步是呼叫 acorn 對JS程式碼進行語法分析,然後收集其中的依賴關係。每個模組都會記錄自己的依賴關係,從而形成一顆關係樹
  • 最後呼叫 compilation.seal 進入 render 階段,根據之前收集的依賴,決定生成多少檔案,每個檔案的內容是什麼

初始化

啟動

首先從bin/webpack.js開始呼叫webpack-cli外掛的./bin/cli.js`檔案,在cli.js中使用yargs來解析命令列引數併合並配置檔案中的引數(options),然後呼叫lib/webpack.js例項化compiler。

例項化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,可以寫外掛在構建之前來處理一些初始化資料。

在進入構建之前解釋兩個類

  • Compiler:該類是webpack的神經中樞,一方面所有的配置資料都儲存在該例項上,另一方面它是在構建過程中控制整個大體的流程。
  • Compilation:該類是webpack的cto,所有的構建過程中產生的構建資料都儲存在該物件上,它掌控著構建過程中每一個細節流程。

compile

在run中先例項化normalModuleFactory等引數,然後呼叫this.hooks.beforeCompile事件執行一些編譯之前需要處理的外掛,最後才執行this.hooks.compile事件(比如compile鉤子中會執行DllReferencePlugin,在這裡註冊代理外掛);this.hooks.compile執行完後例項化Compilation物件,並呼叫this.hooks.compilation通知感興趣的外掛,比如在compilation.dependencyFactories中新增依賴工廠類等操作。compile階段主要是為了進入make階段做準備,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
DllReferencePlugin

在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即可。

build

上面在make階段提到了build,但是沒有深入講解,因為build是在module物件中執行,這節單獨說一下build是如何載入和執行loader最後查詢該module的依賴後返回的。

在build中會呼叫doBuild去載入資源,doBuild中會傳入資源路徑和外掛資源去呼叫loader-runner外掛的runLoaders方法去載入和執行loader。執行完成後會返回如下圖的result結果,根據返回資料把原始碼和sourceMap儲存在module的_source屬性上;doBuild的回撥函式中呼叫Parser類生成AST語法樹,並根據AST語法樹生成依賴後回撥buildModule方法返回compilation類。

result
loader-runner處理流程

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

在Parser類中呼叫acorn外掛生產AST語法樹,acorn不在本文的分析範圍,有興趣的可以去閱讀一下;Parser中生產AST語法樹後呼叫walkStatements方法分析語法樹,根據AST的node的type來遞迴查詢每一個node的型別和執行不同的邏輯,並建立依賴。

ast
MiniCssExtractPlugin

如果在webpack中使用MiniCssExtractPlugin外掛把css單獨打包成檔案,會在樣式處理規則中配置MiniCssExtractPlugin.loader,當解析到css檔案時,會首先執行MiniCssExtractPlugin的loader中實現的pitch方法,pitch方法會為每一個css模組呼叫this._compilation.createChildCompiler建立一個childCompiler和childCompilation;childCompiler控制完成該模組的載入和構建後返回。childCompilation中構建的module是CssModule,並且使用type=`css/mini-extract`來區分。

css

在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會構成一個圖。

moule

seal

在所有模組及其依賴模組 build 完成後,webpack 會監聽 seal 事件呼叫各外掛對構建後的結果進行封裝,要逐次對每個 module 和 chunk 進行整理,生成編譯後的原始碼,合併,拆分,生成 hash 。 同時這是我們在開發時進行程式碼優化和功能新增的關鍵環節。

在seal中首先會觸發optimizeDependencies型別的一些事件去優化依賴(比如tree shaking就是在這個地方執行的),大家要注意一點是在優化類外掛中是不能有非同步的;優化完成後根據入口module建立chunk,如果是單入口就只有一個chunk,多入口就有多個chunk;該階段結束後會根據chunk遞迴分析查詢module中存在的非同步導module,並以該module為節點建立一個chunk,和入口建立的chunk區別在於後面呼叫模版不一樣。所有chunk執行完後會觸發optimizeModulesoptimizeChunks等優化事件通知感興趣的外掛進行優化處理。所有優化完成後給chunk生成hash然後呼叫createChunkAssets來根據模版生成原始碼物件;使用summarizeDependencies把所有解析的檔案快取起來,最後呼叫外掛生成soureMap和最終的資料,下圖是seal階段的流程圖。

seal
生成 assets

在封裝過程中,webpack 會呼叫 Compilation 中的 createChunkAssets 方法進行打包後程式碼的生成。 createChunkAssets 流程如下

create-assets

從上圖可以看出不同的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中。

create-assets-code
seal產物

通過seal階段各種優化和生成最終程式碼會存放在Compilation的assets屬性上,assets是一個物件,以最終輸出名稱為key存放的輸出物件,每一個輸出檔案對應著一個輸出物件,如下圖所示。

assets

emit

最後一步,webpack 呼叫 Compiler 中的 emitAssets() ,按照 output 中的配置項非同步將檔案輸出到了對應的 path 中,從而 webpack 整個打包過程結束。要注意的是,若想對結果進行處理,則需要在 emit 觸發後對自定義外掛進行擴充套件。

watch

當配置了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方法正式開始建立監聽。

Watchpack

在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方法去構建。

watcher1

在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數不清的事件名、看不完的內部外掛,各種事件之間調過去調過來;~~~就這樣吧,~_~

相關文章