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

寫在前面的話

在閱讀 webpack4.x 源碼的過程當中,參考了《深刻淺出webpack》一書和衆多大神的文章,結合本身的一點體會,總結以下。前端

總述

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

核心的概念

entryloaderpluginmodulechunk 不論文檔仍是相關的介紹都不少了,不贅述,有疑問的移步文檔。git

構建流程

webpack 的運行流程是一個串行的過程,從啓動到結束會依次執行如下流程:github

  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)的科普文web

流程細節

流程細節參照我在引用的Compile對象中的註釋,有一點須要注意,做者hooks的書寫順序並非調用順序。 有些沒註釋的有幾種狀況:bash

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

compilation 過程簡介

compilation 實際上就是調用相應的 loader 處理文件生成 chunks並對這些 chunks 作優化的過程。幾個關鍵的事件(Compilation對象this.hooks中):app

  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 ★,轉載請註明出處。post

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

相關文章
相關標籤/搜索