webpack熱更新原理(面試大概率會問)

語言: CN / TW / HK

搭建webpack環境

建立一個專案

```javascript mkdir dev-erver && cd dev-server npm init -y // 快速建立一個專案配置 npm i webpack webpack-dev-server webpack-cli --save-dev mkdir src // 建立資源目錄 mkdir dist // 輸出目錄 touch webpack.dev.js // 因為是在開發環境需要熱更新,所以直接建立dev配置檔案

```

目錄結構

image.png

webpack版本

這裡說明一下,webpack4和webpack5的配置資訊或者顯示資訊可能有點區別

```javascript "devDependencies": { "webpack": "^5.74.0", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.9.3" }

```

編寫配置檔案

```javascript // webpack.dev.js

'use strict';

const path = require('path');

module.exports = { entry: './src/index.js', // 入口檔案 output: { path: path.resolve(__dirname, 'dist'), // 輸出到哪個資料夾 filename: 'output.js' // 輸出的檔名 }, mode: 'development', // 開發模式 devServer: { // contentBase: path.resolve(__dirname, 'dist') // contentBase是用來指定被訪問html頁面所在目錄的; //但是我本地報錯了,使用下面的語句 static: path.resolve(__dirname, "dist")

}

};

```

新建檔案

```javascript // src/index.js

'use strict'

document.write('hello world~')

```

package.json新增一條命令

```javascript "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "dev": "webpack-dev-server --config webpack.dev.js --open" },

```

npm run dev 執行

image.png

我們看到檔案已經打包完成了,但是在dist目錄裡並沒有看到檔案,這是因為WDS是把編譯好的檔案放在快取中,沒有放在磁碟上,但是我們是可以訪問到的,

output.js 對應你在webpack配置檔案中的輸出檔案,配置的是什麼就訪問什麼

http://localhost:8080/output.js

顯然我們想看效果而不是打包後的程式碼,所以我們在dist目錄裡建立一個html檔案引入即可,參考webpack視訊講解:進入學習

```javascript

```

感受webpack的熱更新

內容出來了,我們接下來修改index.js檔案,來看下是否可以自動重新整理

```javascript 'use strict'

document.write('hello world~byebye world')

```

這確實是熱更新,但是這種是每一次修改會重新重新整理整個頁面,大家可以開啟控制檯檢視。webpack-dev-server 提供了實時重載入的功能,但是不能區域性重新整理。必須配合後兩步的配置才能實現區域性重新整理,這兩步的背後其實是藉助了HotModuleReplacementPlugin。

webpack-dev-server搭配HotModuleReplacementPlugin 實現熱更新

我們需要的是,更新修改的模組,但是不要重新整理頁面。這個時候就需要用到模組熱替換。

模組熱替換(Hot Module ReplacementHMR)是 webpack 提供的最有用的功能之一。它允許在執行時更新各種模組,而無需進行完全重新整理。

特性

模組熱替換(HMR - Hot Module Replacement)功能會在應用程式執行過程中替換、新增或刪除模組,而無需重新載入整個頁面。主要是通過以下幾種方式,來顯著加快開發速度:

  • 保留在完全重新載入頁面時丟失的應用程式狀態。
  • 只更新變更內容,以節省寶貴的開發時間。
  • 調整樣式更加快速 - 幾乎相當於在瀏覽器偵錯程式中更改樣式。

啟用

```javascript // webpack.dev.js

const path = require('path'); const webpack = require('webpack'); // 主要多了這一行

module.exports = { entry: './src/index.js', // 入口檔案 output: { path: path.resolve(__dirname, 'dist'), // 輸出到哪個資料夾 filename: 'output.js' // 輸出的檔名 }, mode: 'development', // 開發模式 devServer: { // contentBase: path.resolve(__dirname, 'dist') // contentBase是用來指定被訪問html頁面所在目錄的;但是我本地報錯了,使用下面的語句 static: path.resolve(__dirname, "dist"), hot: true // 主要多了這一行

},
plugins: [ //  主要多了這一行
    new webpack.HotModuleReplacementPlugin()
]

};

```

我們修改一下檔案,形成引用關係

```javascript //index.js

import { test } from './page1.js'

document.write('hello world~1234')

test()

```

```javascript //page1.js

module.exports = { test: function () { console.log(11111) } }

```

在入口頁index.js面再新增一段

```javascript if (module.hot) { module.hot.accept(); }

```

思考💡:為什麼平時修改程式碼的時候不用監聽module.hot.accept也能實現熱更新?

那是因為我們使用的 loader 已經在幕後幫我們實現了。

接下來執行npm run dev

然後我們修改page1.js,會發現頁面並沒有重新整理,只是更新了部分檔案

這樣我們的熱更新就實現了。

熱更新原理

第一步,在 webpack 的 watch 模式下,檔案系統中某一個檔案發生修改,webpack 監聽到檔案變化,根據配置檔案對模組重新編譯打包,並將打包後的程式碼通過簡單的 JavaScript 物件儲存在記憶體中。

第二步是 webpack-dev-server 和 webpack 之間的介面互動,而在這一步,主要是 dev-server 的中介軟體 webpack-dev-middleware 和 webpack 之間的互動,webpack-dev-middleware 呼叫 webpack 暴露的 API對程式碼變化進行監控,並且告訴 webpack,將程式碼打包到記憶體中。

第三步是 webpack-dev-server 對檔案變化的一個監控,這一步不同於第一步,並不是監控程式碼變化重新打包。當我們在配置檔案中配置了devServer.watchContentBase 為 true 的時候,Server 會監聽這些配置資料夾中靜態檔案的變化,變化後會通知瀏覽器端對應用進行 live reload。注意,這兒是瀏覽器重新整理,和 HMR 是兩個概念。

第四步也是 webpack-dev-server 程式碼的工作,該步驟主要是通過 sockjs(webpack-dev-server 的依賴)在瀏覽器端和服務端之間建立一個 websocket 長連線,將 webpack 編譯打包的各個階段的狀態資訊告知瀏覽器端,同時也包括第三步中 Server 監聽靜態檔案變化的資訊。瀏覽器端根據這些 socket 訊息進行不同的操作。當然服務端傳遞的最主要資訊還是新模組的 hash 值,後面的步驟根據這一 hash 值來進行模組熱替換。

webpack-dev-server/client 端並不能夠請求更新的程式碼,也不會執行熱更模組操作,而把這些工作又交回給了 webpack,webpack/hot/dev-server 的工作就是根據 webpack-dev-server/client 傳給它的資訊以及 dev-server 的配置決定是重新整理瀏覽器呢還是進行模組熱更新。當然如果僅僅是重新整理瀏覽器,也就沒有後面那些步驟了。

HotModuleReplacement.runtime 是客戶端 HMR 的中樞,它接收到上一步傳遞給他的新模組的 hash 值,它通過 JsonpMainTemplate.runtime 向 server 端傳送 Ajax 請求,服務端返回一個 json,該 json 包含了所有要更新的模組的 hash 值,獲取到更新列表後,該模組再次通過 jsonp 請求,獲取到最新的模組程式碼。這就是上圖中 7、8、9 步驟。

而第 10 步是決定 HMR 成功與否的關鍵步驟,在該步驟中,HotModulePlugin 將會對新舊模組進行對比,決定是否更新模組,在決定更新模組後,檢查模組之間的依賴關係,更新模組的同時更新模組間的依賴引用。 最後一步,當 HMR 失敗後,回退到 live reload 操作,也就是進行瀏覽器重新整理來獲取最新打包程式碼。

在初步體會了webpack的熱更新之後,可能需要思考以下的問題

思考💡:為什麼需要熱更新?

Hot Module Replacement(以下簡稱 HMR)是 webpack 發展至今引入的最令人興奮的特性之一 ,當你對程式碼進行修改並儲存後,webpack 將對程式碼重新打包,並將新的模組傳送到瀏覽器端,瀏覽器通過新的模組替換老的模組,這樣在不重新整理瀏覽器的前提下就能夠對應用進行更新。例如,在開發 Web 頁面過程中,當你點選按鈕,出現一個彈窗的時候,發現彈窗標題沒有對齊,這時候你修改 CSS 樣式,然後儲存,在瀏覽器沒有重新整理的前提下,標題樣式發生了改變。感覺就像在 Chrome 的開發者工具中直接修改元素樣式一樣。

思考💡:HMR是怎樣實現自動編譯的?

webpack通過watch可以監聽檔案編譯完成和監聽檔案的變化,webpack-dev-middleware可以呼叫webpack的API監聽程式碼的變化,webpack-dev-middleware利用sockjs和webpack-dev-server/client建立webSocket長連線。將webpack的編譯編譯打包的各個階段告訴瀏覽器端。主要告訴新模組hash的變化,然後webpack-dev-server/client是無法獲取更新的程式碼的,通過webpack/hot/server獲取更新的模組,然後HMR對比更新模組和模組的依賴。

思考💡:模組內容的變更瀏覽器又是如何感知的?

webpack-dev-middleware利用sockjs和webpack-dev-server/client建立webSocket長連線。將webpack的編譯編譯打包的各個階段告訴瀏覽器端。

思考💡:以及新產生的兩個檔案又是幹嘛的?

d04feccfa446b174bc10.hot-update.json

告知瀏覽器新的hash值,並且是哪個chunk發生了改變

main.d04feccfa446b174bc10.hot-update.js

告知瀏覽器,main 程式碼塊中的/src/title.js模組變更的內容

首先是通過XMLHttpRequest的方式,利用上一次儲存的hash值請求hot-update.json檔案。這個描述檔案的作用就是提供了修改的檔案所在的chunkId。

然後通過JSONP的方式,利用hot-update.json返回的chunkId 及 上一次儲存的hash 拼接檔名進而獲取檔案內容。

思考💡:怎麼實現區域性更新的?

當hot-update.js檔案載入好後,就會執行window.webpackHotUpdate,進而呼叫了hotApply。hotApply根據模組ID找到舊模組然後將它刪除,然後執行父模組中註冊的accept回撥,從而實現模組內容的區域性更新。

思考💡:webpack 可以將不同的模組打包成 bundle 檔案或者幾個 chunk 檔案,但是當我通過 webpack HMR 進行開發的過程中,我並沒有在我的 dist 目錄中找到 webpack 打包好的檔案,它們去哪呢?

原來 webpack 將 bundle.js 檔案打包到了記憶體中,不生成檔案的原因就在於訪問記憶體中的程式碼比訪問檔案系統中的檔案更快,而且也減少了程式碼寫入檔案的開銷,這一切都歸功於memory-fs,memory-fs 是 webpack-dev-middleware 的一個依賴庫,webpack-dev-middleware 將 webpack 原本的 outputFileSystem 替換成了MemoryFileSystem 例項,這樣程式碼就將輸出到記憶體中。

思考💡:通過檢視 webpack-dev-server 的 package.json 檔案,我們知道其依賴於 webpack-dev-middleware 庫,那麼 webpack-dev-middleware 在 HMR 過程中扮演什麼角色?

webpack-dev-middleware扮演是中介軟體的角色,一頭可以呼叫webpack暴露的API檢測程式碼的變化,一頭可以通過sockjs和webpack-dev-server/client建立webSocket長連線,將webapck打包編譯的各個階段傳送給瀏覽器端。

思考💡:使用 HMR 的過程中,通過 Chrome 開發者工具我知道瀏覽器是通過 websocket 和 webpack-dev-server 進行通訊的,但是 websocket 的 message 中並沒有發現新模組程式碼。打包後的新模組又是通過什麼方式傳送到瀏覽器端的呢?為什麼新的模組不通過 websocket 隨訊息一起傳送到瀏覽器端呢?

功能塊的解耦,各個模組各司其職,dev-server/client 只負責訊息的傳遞而不負責新模組的獲取,而這些工作應該有 HMR runtime 來完成,HMR runtime 才應該是獲取新程式碼的地方。再就是因為不使用 webpack-dev-server 的前提,使用 webpack-hot-middleware 和 webpack 配合也可以完成模組熱更新流程,在使用 webpack-hot-middleware 中有件有意思的事,它沒有使用 websocket,而是使用的 EventSource。綜上所述,HMR 的工作流中,不應該把新模組程式碼放在 websocket 訊息中。

思考💡:瀏覽器拿到最新的模組程式碼,HMR 又是怎麼將老的模組替換成新的模組,在替換的過程中怎樣處理模組之間的依賴關係?

思考💡:當模組的熱替換過程中,如果替換模組失敗,有什麼回退機制嗎?

模組熱更新的錯誤處理,如果在熱更新過程中出現錯誤,熱更新將回退到重新整理瀏覽器

面試題:說一下webpack的熱更新原理?

webpack通過watch可以監測程式碼的變化;webpack-dev-middleware可以呼叫webpack暴露的API檢測程式碼變化,並且告訴webpack將程式碼儲存到記憶體中;webpack-dev-middleware通過sockjs和webpack-dev-server/client建立webSocket長連線,將webpack打包階段的各個狀態告知瀏覽器端,最重要的是新模組的hash值。webpack-dev-server/client通過webpack/hot/dev-server中的HMR去請求新的更新模組,HMR主要藉助JSONP。先拿到hash的json檔案,然後根據hash拼接出更新的檔案js,然後HotModulePlugin對比新舊模組和模組依賴完成更新。