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對比新舊模塊和模塊依賴完成更新。