Webpack原理與實踐(一):打包流程

薄荷前端發表於2018-11-12

寫在前面的話

在閱讀 webpack4.x 原始碼的過程中,參考了《深入淺出webpack》一書和眾多大神的文章,結合自己的一點體會,總結如下。

總述

webpack 就像一條生產線,要經過一系列處理流程後才能將原始檔轉換成輸出結果。 這條生產線上的每個處理流程的職責都是單一的,多個流程之間有存在依賴關係,只有完成當前處理後才能交給下一個流程去處理。 外掛就像是一個插入到生產線中的一個功能,在特定的時機對生產線上的資源做處理。 webpack 通過 Tapable 來組織這條複雜的生產線。 webpack 在執行過程中會廣播事件,外掛只需要監聽它所關心的事件,就能加入到這條生產線中,去改變生產線的運作。 webpack 的事件流機制保證了外掛的有序性,使得整個系統擴充套件性很好。 --吳浩麟《深入淺出webpack》

核心的概念

entryloaderpluginmodulechunk 不論文件還是相關的介紹都很多了,不贅述,有疑問的移步文件。

構建流程

webpack 的執行流程是一個序列的過程,從啟動到結束會依次執行以下流程:

  1. 初始化引數:從配置檔案和 Shell 語句中讀取與合併引數,得出最終的引數;
  2. 開始編譯:用上一步得到的引數初始化 Compiler 物件,載入所有配置的外掛,執行物件的 run 方法開始執行編譯;
  3. 確定入口:根據配置中的 entry 找出所有的入口檔案
  4. 編譯模組:從入口檔案出發,呼叫所有配置的 Loader 對模組進行翻譯,再找出該模組依賴的模組,再遞迴本步驟直到所有入口依賴的檔案都經過了本步驟的處理;
  5. 完成模組編譯:在經過第4步使用 Loader 翻譯完所有模組後,得到了每個模組被翻譯後的最終內容以及它們之間的依賴關係;
  6. 輸出資源:根據入口和模組之間的依賴關係,組裝成一個個包含多個模組的 Chunk,再把每個 Chunk 轉換成一個單獨的檔案加入到輸出列表,這步是可以修改輸出內容的最後機會;
  7. 輸出完成:在確定好輸出內容後,根據配置確定輸出的路徑和檔名,把檔案內容寫入到檔案系統。 在以上過程中,webpack 會在特定的時間點廣播出特定的事件,外掛在監聽到感興趣的事件後會執行特定的邏輯,並且外掛可以呼叫 webpack 提供的 API 改變 webpack 的執行結果。

webpack 中比較核心的兩個物件

  • Compile 物件:負責檔案監聽和啟動編譯。Compiler 例項中包含了完整的 webpack 配置,全域性只有一個 Compiler 例項。
  • compilation 物件:當 webpack 以開發模式執行時,每當檢測到檔案變化,一次新的 Compilation 將被建立。一個 Compilation 物件包含了當前的模組資源、編譯生成資源、變化的檔案等。Compilation 物件也提供了很多事件回撥供外掛做擴充套件。
  • 這兩個物件都繼承自 Tapable。以Compile為例
const {
	Tapable,
	SyncHook,
	SyncBailHook,
	AsyncParallelHook,
	AsyncSeriesHook
} = require("tapable");

class Compiler extends Tapable {
	constructor(context) {
		super();
		this.hooks = {
			/** @type {SyncBailHook<Compilation>} */
			//所有需要輸出的檔案已經生成好,詢問外掛哪些檔案需要輸出,哪些不需要。
			shouldEmit: new SyncBailHook(["compilation"]),
			/** @type {AsyncSeriesHook<Stats>} */
			//成功完成一次完成的編譯和輸出流程。
			done: new AsyncSeriesHook(["stats"]),
			/** @type {AsyncSeriesHook<>} */
			additionalPass: new AsyncSeriesHook([]),
			/** @type {AsyncSeriesHook<Compiler>} */
			beforeRun: new AsyncSeriesHook(["compiler"]),
			/** @type {AsyncSeriesHook<Compiler>} */
			//啟動一次新的編譯
			run: new AsyncSeriesHook(["compiler"]),
			/** @type {AsyncSeriesHook<Compilation>} */
			// 確定好要輸出哪些檔案後,執行檔案輸出,可以在這裡獲取和修改輸出內容。
			emit: new AsyncSeriesHook(["compilation"]),
			/** @type {AsyncSeriesHook<Compilation>} */
			// 輸出完畢
			afterEmit: new AsyncSeriesHook(["compilation"]),
                         // 以上幾個事件(除了run,beforerun為編譯階段)其餘為輸出階段的事件
			/** @type {SyncHook<Compilation, CompilationParams>} */
			// compilation 建立之前掛載外掛的過程
			thisCompilation: new SyncHook(["compilation", "params"]),
			/** @type {SyncHook<Compilation, CompilationParams>} */
			// 建立compilation物件
			compilation: new SyncHook(["compilation", "params"]),
			/** @type {SyncHook<NormalModuleFactory>} */
		    // 初始化階段:初始化compilation引數
			normalModuleFactory: new SyncHook(["normalModuleFactory"]),
			/** @type {SyncHook<ContextModuleFactory>}  */
		    // 初始化階段:初始化compilation引數
			contextModuleFactory: new SyncHook(["contextModulefactory"]),

			/** @type {AsyncSeriesHook<CompilationParams>} */
			beforeCompile: new AsyncSeriesHook(["params"]),
			/** @type {SyncHook<CompilationParams>} */
			// 該事件是為了告訴外掛一次新的編譯將要啟動,同時會給外掛帶上 compiler 物件
			compile: new SyncHook(["params"]),
			/** @type {AsyncParallelHook<Compilation>} */
			//一個新的 Compilation 建立完畢,即將從 Entry 開始讀取檔案,根據檔案型別和配置的 Loader 對檔案進行編譯,編譯完後再找出該檔案依賴的檔案,遞迴的編譯和解析。
			make: new AsyncParallelHook(["compilation"]),
			/** @type {AsyncSeriesHook<Compilation>} */
		    // 一次Compilation執行完成
			afterCompile: new AsyncSeriesHook(["compilation"]),

			/** @type {AsyncSeriesHook<Compiler>} */
			//監聽模式下啟動編譯(常用於開發階段)
			watchRun: new AsyncSeriesHook(["compiler"]),
			/** @type {SyncHook<Error>} */
			failed: new SyncHook(["error"]),
			/** @type {SyncHook<string, string>} */
			invalid: new SyncHook(["filename", "changeTime"]),
			/** @type {SyncHook} */
			// 如名字所述
			watchClose: new SyncHook([]),

			// TODO the following hooks are weirdly located here
			// TODO move them for webpack 5
			/** @type {SyncHook} */
			//初始化階段:開始應用 Node.js 風格的檔案系統到compiler 物件,以方便後續的檔案尋找和讀取。
			environment: new SyncHook([]),
			/** @type {SyncHook} */
			// 參照上文
			afterEnvironment: new SyncHook([]),
			/** @type {SyncHook<Compiler>} */
			// 呼叫完內建外掛以及配置引入外掛的apply方法,完成了事件訂閱
			afterPlugins: new SyncHook(["compiler"]),
			/** @type {SyncHook<Compiler>} */
			afterResolvers: new SyncHook(["compiler"]),
			/** @type {SyncBailHook<string, EntryOptions>} */
			// 讀取配置的 Entrys,為每個 Entry 例項化一個對應的 EntryPlugin,為後面該 Entry 的遞迴解析工作做準備。
			entryOption: new SyncBailHook(["context", "entry"])
		};
複製程式碼

webpack 執行的過程中,會按順序廣播一系列事件--this.hooks中的一系列事件(類似於我們常用框架中的生命週期),而這些事件的訂閱者該按照怎樣的順序來組織,來執行,來進行引數傳遞... 這就是 Tapable 要做的事情。
關於 Tapable 給大家推薦一篇比較好(但是閱讀量點贊評論都不多2333)的科普文

流程細節

流程細節參照我在引用的Compile物件中的註釋,有一點需要注意,作者hooks的書寫順序並不是呼叫順序。 有些沒註釋的有幾種情況:

  1. 不那麼重要,或參照事件名稱和上下文可知
  2. 主要是暫時還不知道(2333,後面有新的理解再補充,逃...)
  3. 當然最重要的事件基本涵蓋到了 這裡補充一個大從參考文章裡面找來的圖

compilation 過程簡介

compilation 實際上就是呼叫相應的 loader 處理檔案生成 chunks並對這些 chunks 做優化的過程。幾個關鍵的事件(Compilation物件this.hooks中):

  1. buildModule 使用對應的 Loader 去轉換一個模組;
  2. normalModuleLoader 在用 Loader 對一個模組轉換完後,使用 acorn 解析轉換後的內容,輸出對應的抽象語法樹(AST),以方便 webpack 後面對程式碼的分析。
  3. seal 所有模組及其依賴的模組都通過 Loader 轉換完成後,根據依賴關係開始生成 Chunk

最後從參考文章中摘了一張圖片以便於對整個過程有更清晰的認知

image

參考

  1. taobaofed.org/blog/2016/0…
  2. imweb.io/topic/5baca…
  3. 《深入淺出webpack》

廣而告之

本文釋出於薄荷前端週刊,歡迎Watch & Star ★,轉載請註明出處。

歡迎討論,點個贊再走吧 。◕‿◕。 ~

相關文章