天天看點

webpack-dev-middleware 源碼解讀

 這是第 42 篇不摻水的原創

本文首發于政采雲前端團隊部落格:webpack-dev-middleware 源碼解讀 https://www.zoo.team/article/webpack-dev-middleware
webpack-dev-middleware 源碼解讀

Webpack 的使用目前已經是前端開發工程師必備技能之一。若是想在本地環境啟動一個開發服務,大家隻需在 Webpack 的配置中,增加 devServer (https://www.webpackjs.com/configuration/dev-server/) 的配置來啟動。devServer 配置的本質是 webpack-dev-server 這個包提供的功能,而 webpack-dev-middleware 則是這個包的底層依賴。

截至本文發表前,webpack-dev-middleware 的最新版本為 <code>[email protected]</code>,本文的源碼來自于此版本。本文會講解 webpack-dev-middleware 的核心子產品實作,相信大家把這篇文章看完,再去閱讀源碼,會容易了解很多。

要回答這個問題,我們先來看看如何使用這個包:

通過啟動一個 Express (http://www.expressjs.com.cn/) 服務,将 <code>wdm(compiler)</code> 的結果通過 <code>app.use</code> 方法注冊為 Express 服務的中間函數。從這裡,我們不難看出 <code>wdm(compiler)</code> 的執行結果傳回的是一個 <code>express</code> 的中間件。它作為一個容器,将 <code>webpack</code> 編譯後的檔案存儲到記憶體中,然後在使用者通路 <code>express</code> 服務時,将記憶體中對應的資源輸出傳回。

熟悉 <code>webpack</code> 的同學都知道,<code>webpack</code> 可以通過 watch mode (https://www.webpackjs.com/configuration/watch/) 方式啟動,那為何我們不直接使用此方式來監聽資源變化呢?答案就是,<code>webpack</code> 的 <code>watch mode</code> 雖然能監聽檔案的變更,并且自動打包,但是每次打包後的結果将會存儲到本地硬碟中,而 IO 操作是非常耗資源時間的,無法滿足本地開發調試需求。

而 webpack-dev-middleware 擁有以下幾點特性:

以 <code>watch mode</code> 啟動 <code>webpack</code>,監聽的資源一旦發生變更,便會自動編譯,生産最新的 <code>bundle</code>

在編譯期間,停止提供舊版的 <code>bundle</code> 并且将請求延遲到最新的編譯結果完成之後

<code>webpack</code> 編譯後的資源會存儲在記憶體中,當使用者請求資源時,直接于記憶體中查找對應資源,減少去硬碟中查找的 IO 操作耗時

本文将主要圍繞這三個特性和主流程邏輯進行分析。

讓我們先來看下 webpack-dev-middleware 的源碼目錄:

其中 <code>lib</code> 目錄下為源代碼,一眼望去有近 10 多個檔案要解讀。但刨除 <code>utils</code> 工具集合目錄,其核心源碼檔案其實隻有兩個 <code>index.js</code>、<code>middleware.js</code>

下面我們就來分析核心檔案 <code>index.js</code>、<code>middleware.js</code> 的源碼實作

從上文我們已經得知 <code>wdm(compiler)</code> 傳回的是一個 <code>express</code> 中間件,是以入口檔案 <code>index.js</code> 則為一個中間件的容器包裝函數。它接收兩個參數,一個為 <code>webpack</code> 的 <code>compiler</code>、另一個為配置對象,經過一系列的處理,最後傳回一個中間件函數。下面我将對 <code>index.js</code> 中的核心代碼進行講解:

<code>index.js</code> 最為核心的是以上 3 個部分的執行,分别完成了我們上文提到的兩點特性:

以監控的方式啟動 <code>webpack</code>

将 <code>webpack</code> 的編譯内容,輸出至記憶體中

此函數的作用是在 <code>compiler</code> 的 <code>invalid</code>、<code>run</code>、<code>done</code>、<code>watchRun</code> 這 4 個編譯生命周期上,注冊對應的處理方法

在 <code>done</code> 生命周期上注冊 <code>done</code> 方法,該方法主要是 <code>report</code> 編譯的資訊以及執行 <code>context.callbacks</code> 回調函數

在 <code>invalid</code>、<code>run</code>、<code>watchRun</code> 等生命周期上注冊 <code>invalid</code> 方法,該方法主要是 <code>report</code> 編譯的狀态資訊

此部分的作用是,調用 <code>compiler</code> 的 watch 方法,之後 <code>webpack</code> 便會監聽檔案變更,一旦檢測到檔案變更,就會重新執行編譯。

其作用是使用 memory-fs 對象替換掉 <code>compiler</code> 的檔案系統對象,讓 <code>webpack</code> 編譯後的檔案輸出到記憶體中。

通過以上 3 個部分的執行,我們以 <code>watch mode</code> 的方式啟動了 <code>webpack</code>,一旦監測的檔案變更,便會重新進行編譯打包,同時我們又将檔案的存儲方法改為了記憶體存儲,提高了檔案的存儲讀取效率。最後,我們隻需要傳回 <code>express</code> 的中間件就可以了,而中間件則是調用 <code>middleware(context)</code> 函數得到的。下面,我們來看看 <code>middleware</code> 是如何實作的。

此檔案傳回的是一個 <code>express</code> 中間件函數的包裝函數,其核心處理邏輯主要針對 <code>request</code> 請求,根據各種條件判斷,最終傳回對應的檔案内容:

首先,<code>middleware</code> 中定義了一個 <code>goNext()</code> 方法,該方法判斷是否是服務端渲染。如果是,則調用 <code>ready()</code> 方法(此方法即為 <code>ready.js</code> 檔案,作用為根據 <code>context.state</code> 狀态判斷直接執行回調還是将回調存儲 <code>callbacks</code> 隊列中)。如果不是,則直接調用 <code>next()</code> 方法,流轉至下一個 <code>express</code> 中間件。

接着,判斷 <code>HTTP</code> 協定的請求的類型,若請求不包含于配置中(預設 <code>GET</code>、<code>HEAD</code> 請求),則直接調用 <code>goNext()</code> 方法處理請求:

然後,根據請求的 <code>req.url</code> 位址,在 <code>compiler</code> 的記憶體檔案系統中查找對應的檔案,若查找不到,則直接調用 <code>goNext()</code> 方法處理請求:

最後,中間件傳回一個 <code>Promise</code> 執行個體,而在執行個體中,先是定義一個 <code>processRequest</code> 方法,此方法的作用是根據上文中找到的 <code>filename</code> 路徑擷取到對應的檔案内容,并構造 <code>response</code> 對象傳回,随後調用 <code>ready(context, processRequest, req)</code> 函數,去執行 <code>processRequest</code> 方法。這裡我們着重看下 <code>ready</code> 方法的内容:

非常簡單的方法,判斷 <code>context.state</code> 的狀态,将直接執行回調函數 <code>fn</code>,或在 <code>context.callbacks</code> 中添加回調函數 <code>fn</code>。這也解釋了上文提到的另一個特性 “在編譯期間,停止提供舊版的 <code>bundle</code> 并且将請求延遲到最新的編譯結果完成之後”。若 <code>webpack</code> 還處于編譯狀态,<code>context.state</code> 會被設定為 <code>false</code>,是以當使用者發起請求時,并不會直接傳回對應的檔案内容,而是會将回調函數 <code>processRequest</code> 添加至 <code>context.callbacks</code> 中,而上文中我們說到在 <code>compile.hooks.done</code> 上注冊了回調函數 <code>done</code>,等編譯完成之後,将會執行這個函數,并循環調用 <code>context.callbacks</code>。

源碼的閱讀是一個非常枯燥的過程,但是它的收益也是巨大的。上文的源碼解讀主要分析的是 <code>webpack-dev-middleware</code> 它是如何實作它所擁有的特性、如何處理使用者的請求等主要功能點,未包括其他分支邏輯處理、容錯。還需讀者在這篇文章基礎之上,再去閱讀詳細的源碼,望這篇文章能對你的閱讀過程起到一定的幫助作用。

webpack-dev-middleware 源碼解讀
webpack-dev-middleware 源碼解讀

繼續閱讀