Vite 熱更新的主要流程
熱更新的英文全稱為Hot Module Replacement
,簡寫為 HRM。當修改程式碼時,HRM 能夠在不重新整理頁面的情況下,把頁面中發生變化的模組,替換成新的模組,同時不影響其他模組的正常運作
本文講的會講述熱更新的每個流程,主要的作用是什麼,還有這些流程是怎麼串起來的,目的是幫助大家對熱更新的流程有個基本的瞭解。
由於篇幅原因,本文不會非常深入的每個流程的細節。
本文的用到的程式碼放在 GitHub,裡邊有兩個專案,一個是純 ts 的熱更新專案,一個是普通的 vue 專案
熱更新流程
在介紹熱更新的主要流程前,我們先來看看這個問題
把一頭大象裝進冰箱,需要幾步?
這個問題相信大家都非常的熟悉,只需要三步:
- 開啟冰箱門
- 把大象裝進冰箱
- 把冰箱門關起來
這個問題本身不是考驗人的的邏輯能力,而是考驗抽象解決方案關鍵步驟的能力
熱更新的流程非常大,且很複雜,我們要把複雜問題簡單化,只關注核心的流程,將次要的問題抽象化,從而對整個熱更新的過程有所理解
在這個問題中,核心流程就是這三個步驟,然後我們可以進一步細化我們需要關注的步驟,其他步驟可以暫且忽略
既然只關心核心的流程,那麼你覺得,熱更新的有哪些核心流程?
從修改程式碼,到介面更新,這個過程發生了什麼?
這是我在給小夥伴分享時,他們提出的:
- 修改程式碼
- 重新編譯(怎麼編譯,編譯產物是什麼,先不管)
- 告訴前端要熱更新了(怎麼告訴,先不管)
- 前端執行熱更新程式碼進行熱更新(怎麼更新,先不管)
實際上,也就是這麼幾個過程
下面是我畫的熱更新的主要流程的時序圖,大家一開始可能是看不懂的,這不重要,後面會逐一細講,只要大概清晰各個部分的時序關係即可
vite server:指 vite 在開發時啟動的 server
vite client:vite dev server 會在 index.html
中,注入路徑為 @vite/client
的指令碼,這個指令碼是執行在瀏覽器的
暫時先記住這個核心流程:
- 修改程式碼,vite server 監聽到程式碼被修改
- vite 計算出熱更新的邊界(即受到影響,需要進行更新的模組)
- vite server 通過 websocket 告訴 vite client 需要進行熱更新
- 瀏覽器拉取修改後的模組
- 執行熱更新的程式碼
我們先從離我們最近的瀏覽器端,開始介紹
熱更新 API 簡介
該小節主要講這兩部分:
這裡主要涉及到兩個 API:
這兩個 API 定義了拉取到新的程式碼之後,如何進行老程式碼的退出,和新程式碼的更新
我們先來看看,沒有使用熱更新 API 的程式碼被修改時,會發生什麼?
不使用熱更新 API
該小節對應的專案程式碼在 /package/ts-file-test,對應的檔案為 no-hrm.ts
下圖主要是一個 ts 檔案,直接獲取到一個 DOM,並替換其 innerHTML
我們可以看到,該檔案沒有定義熱更新,當檔案被修改時,整個頁面都重新重新整理了。因為 vite 不知道如何進行熱更新,所以只能重新整理頁面
使用 hot.accept API
該小節對應的專案程式碼在 /package/ts-file-test,對應的檔案為 accept.ts
import.meta.hot.accept
API 用於傳入一個回撥函式,來定義該模組修改後,需要怎麼去熱更新
``typescript
// src/accept.ts
export const render = () => {
const el = document.querySelector<HTMLDivElement>('#accept')!;
el.innerHTML =
Project: ts-file-test
File: accept.ts
accept test
`; };if (import.meta.hot) { // 呼叫的時候,呼叫的是老的模組的 accept 回撥 import.meta.hot.accept((mod) => { // 老的模組的 accept 回撥拿到的是新的模組 console.log('mod', mod); console.log('mod.render', mod.render); mod.render(); }); } ```
當我們將修改該檔案時(將 <p>accept test</p>
改成 <p>accept test2</p>
),之前老的模組註冊的 accept 的回撥就會被執行
mod 就是修改後的模組物件,在該檔案中,mod 就是一個匯出了 render 函式的物件
當模組被修改時,重新執行 render 函式,設定 innerHTML 更新介面。
這時候我們定義瞭如何進行熱更新,vite 就不會重新整理頁面了(重新整理頁面會清空所有請求,而下圖沒有清空請求)
dispose 類似 hot,只是 dispose 定義的是老模組如何退出,而 hot 定義的是新模組如何更新
什麼時候老模組需要退出?
假如你的頁面有個定時器,就要在老模組退出時,將定時器清除,否則每次修改,頁面會新增一個定時器,頁面上的定時器會越來越多,造成記憶體洩露
dispose 主要用來做一些模組的退出工作
寫熱更新程式碼非常麻煩,應該沒有人會在業務中寫?
熱更新程式碼的確很麻煩,業務中基本上也不會有人寫,但我們在寫 vue 程式碼時,確實有熱更新的。
那是因為, vite 的 vite-plugin
外掛,在編譯模組時加入了 vue 熱更新的程式碼。
vite 本身只提供熱更新 API,不提供具體的熱更新邏輯,具體的熱更新行為,由 vue、react 這些框架提供
熱更新邊界
該小節主要講這一部分
什麼是熱更新邊界?作用是什麼?
假設有兩個檔案,關係如下
從上一小節,我們可以知道,vue 自帶了熱更新邏輯,而我們寫的 ts 檔案,沒有熱更新邏輯
當 useData.ts
被修改時,這時候是會重新整理頁面嗎?
答案是不會的。vue 元件依賴的 ts 檔案被修改,可以對這個 vue 檔案進行熱更新,重新載入元件。如果重新整理頁面,那開發體驗就不太好了。
這時候,index.vue
就被稱為熱更新邊界——最近的可接受熱更新的模組
沿著依賴樹,往上找到最近的一個可以熱更新的模組,即熱更新邊界,對其進行熱更新即可
為什麼有時候修改程式碼可以熱更新,有時候卻是重新整理頁面?例如在 vue 專案中修改 main.ts
修改 main.ts
時,因為往上找不到可以熱更新的模組了,vite 不知道如何進行熱更新,因此只能重新整理頁面
如果其他 ts 檔案,能找到熱更新邊界,就可以直接進行熱更新
檔案跟模組不是一一對應的嗎?為什麼需要遍歷檔案對應的模組?
在 vite 中,檔案跟模組不是一一對應
因為 vite 可以加入查詢引數,可檢視 vite 文件【更改資源被引入的方式】
```ts // 顯式載入資源為一個 URL import assetAsURL from './asset.js?url'
// 以字串形式載入資源 import assetAsString from './shader.glsl?raw'
// 載入為 Web Worker import Worker from './worker.js?worker'
// 在構建時 Web Worker 內聯為 base64 字串 import InlineWorker from './worker.js?worker&inline' ```
同一個檔案,可能作為多個模組,例如 raw 時的編譯產出的模組跟 worker 時編譯產出的模組就是兩個不同的模組
因為,一個檔案,是對應多個模組的。這些模組都需要找到他們的熱更新邊界,並進行熱更新
瀏覽器接收熱更新訊號
該小節主要講這一部分
websocket 是什麼建立的?
vite dev server 會在 index.html
中,注入路徑為 @vite/client
的指令碼,當訪問 index.html
時,就會拉取該指令碼
client.ts 在載入時,會建立 websocket 並監聽 message 事件
handleMessage 負責處理各種訊號,由於篇幅有限,我們不會展開講細節
typescript
async function handleMessage(payload: HMRPayload) {
switch (payload.type) {
case 'connected':
// 連線訊號
console.log(`[vite] connected.`)
setInterval(() => socket.send('ping'), __HMR_TIMEOUT__)
break
case 'update':
// 模組更新訊號
break
case 'custom': {
// 自定義訊號
break
}
case 'full-reload':
// 頁面重新整理訊號
break
case 'prune':
// 模組刪除訊號
break
case 'error': {
// 錯誤訊號
break
}
}
}
我們可以通過抓包的方式,看到 vite dev server 跟 client 之前的通訊
server 模組轉換
該小節主要講這一部分
模組程式碼轉換 vite 的核心,這部分足以開一個大的主題去講,同樣的,本文只會介紹個大概,只需要知道 vite 會轉換程式碼即可,轉換細節暫時可以不關注,把 vite server 當做一個黑箱
之前說的到,vite 的 plugin-vue
外掛,將熱更新程式碼注入到模組中,就是在編譯轉換模組的過程中處理
從圖中可以看出,index.vue
經過編譯後,內容是 js 程式碼,其中還能看到 import.meta.hot.accept
定義熱更新的回撥
時序圖中,有個迴圈條件,直到動態 import 的模組沒有模組依賴,是什麼意思?
假如有以下兩個檔案:
text
index.vue
- useData.ts
index.vue
依賴(import)了 useData.ts
當修改 useData.ts
時,會執行以下的步驟:
- vite 沿著依賴樹,往上找到
index.vue
,作為熱更新邊界 - server 將熱更新邊界資訊,通過 websocket 傳遞到 client
- client 執行老的
index.vue
的import.meta.hot.dispose
回撥 - client 動態
import(index.vue)
,vite 會重新編譯index.vue
- 執行
index.vue
的程式碼(此時請求到index.vue
雖然是 vue 字尾,但是它的內容經過編譯後,是 js 程式碼),執行過程中遇到 importuseData.ts
- 動態拉取
useData.ts
模組,vite 會重新編譯useData.ts
- 執行
useData.ts
的程式碼 - client 執行新的
index.vue
的import.meta.hot.accept
回撥
因為熱更新邊界的模組,可能會存在依賴,import 了其他模組,這些模組都需要 import 拉取,直到動態 import 的模組沒有模組依賴