Vite 熱更新的主要流程

語言: CN / TW / HK

熱更新的英文全稱為Hot Module Replacement,簡寫為 HRM。當修改程式碼時,HRM 能夠在不重新整理頁面的情況下,把頁面中發生變化的模組,替換成新的模組,同時不影響其他模組的正常運作

本文講的會講述熱更新的每個流程,主要的作用是什麼,還有這些流程是怎麼串起來的,目的是幫助大家對熱更新的流程有個基本的瞭解。

由於篇幅原因,本文不會非常深入的每個流程的細節。

本文的用到的程式碼放在 GitHub,裡邊有兩個專案,一個是純 ts 的熱更新專案,一個是普通的 vue 專案

熱更新流程

在介紹熱更新的主要流程前,我們先來看看這個問題

把一頭大象裝進冰箱,需要幾步?

這個問題相信大家都非常的熟悉,只需要三步:

  1. 開啟冰箱門
  2. 把大象裝進冰箱
  3. 把冰箱門關起來

這個問題本身不是考驗人的的邏輯能力,而是考驗抽象解決方案關鍵步驟的能力

熱更新的流程非常大,且很複雜,我們要把複雜問題簡單化,只關注核心的流程,將次要的問題抽象化,從而對整個熱更新的過程有所理解

在這個問題中,核心流程就是這三個步驟,然後我們可以進一步細化我們需要關注的步驟,其他步驟可以暫且忽略

既然只關心核心的流程,那麼你覺得,熱更新的有哪些核心流程?

從修改程式碼,到介面更新,這個過程發生了什麼?

這是我在給小夥伴分享時,他們提出的:

  1. 修改程式碼
  2. 重新編譯(怎麼編譯,編譯產物是什麼,先不管)
  3. 告訴前端要熱更新了(怎麼告訴,先不管)
  4. 前端執行熱更新程式碼進行熱更新(怎麼更新,先不管)

實際上,也就是這麼幾個過程

下面是我畫的熱更新的主要流程的時序圖,大家一開始可能是看不懂的,這不重要,後面會逐一細講,只要大概清晰各個部分的時序關係即可

image-20220508124910625

vite server:指 vite 在開發時啟動的 server

vite client:vite dev server 會在 index.html 中,注入路徑為 @vite/client 的指令碼,這個指令碼是執行在瀏覽器的

image-20220509204437568

暫時先記住這個核心流程:

  1. 修改程式碼,vite server 監聽到程式碼被修改
  2. vite 計算出熱更新的邊界(即受到影響,需要進行更新的模組)
  3. vite server 通過 websocket 告訴 vite client 需要進行熱更新
  4. 瀏覽器拉取修改後的模組
  5. 執行熱更新的程式碼

我們先從離我們最近的瀏覽器端,開始介紹

熱更新 API 簡介

該小節主要講這兩部分:

image-20220509210519511

這裡主要涉及到兩個 API:

這兩個 API 定義了拉取到新的程式碼之後,如何進行老程式碼的退出,和新程式碼的更新

我們先來看看,沒有使用熱更新 API 的程式碼被修改時,會發生什麼?

不使用熱更新 API

該小節對應的專案程式碼在 /package/ts-file-test,對應的檔案為 no-hrm.ts

下圖主要是一個 ts 檔案,直接獲取到一個 DOM,並替換其 innerHTML

img

我們可以看到,該檔案沒有定義熱更新,當檔案被修改時,整個頁面都重新重新整理了。因為 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 函式的物件

image-20220509214914047

當模組被修改時,重新執行 render 函式,設定 innerHTML 更新介面。

這時候我們定義瞭如何進行熱更新,vite 就不會重新整理頁面了(重新整理頁面會清空所有請求,而下圖沒有清空請求)

accept

dispose 類似 hot,只是 dispose 定義的是老模組如何退出,而 hot 定義的是新模組如何更新

什麼時候老模組需要退出?

假如你的頁面有個定時器,就要在老模組退出時,將定時器清除,否則每次修改,頁面會新增一個定時器,頁面上的定時器會越來越多,造成記憶體洩露

dispose 主要用來做一些模組的退出工作

寫熱更新程式碼非常麻煩,應該沒有人會在業務中寫?

熱更新程式碼的確很麻煩,業務中基本上也不會有人寫,但我們在寫 vue 程式碼時,確實有熱更新的。

那是因為, vite 的 vite-plugin 外掛,在編譯模組時加入了 vue 熱更新的程式碼

vite 本身只提供熱更新 API,不提供具體的熱更新邏輯,具體的熱更新行為,由 vue、react 這些框架提供

熱更新邊界

該小節主要講這一部分

image-20220509210156587

什麼是熱更新邊界?作用是什麼?

假設有兩個檔案,關係如下

image-20220509225027495

從上一小節,我們可以知道,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 時編譯產出的模組就是兩個不同的模組

因為,一個檔案,是對應多個模組的。這些模組都需要找到他們的熱更新邊界,並進行熱更新

瀏覽器接收熱更新訊號

該小節主要講這一部分

image-20220510194729431

websocket 是什麼建立的?

vite dev server 會在 index.html 中,注入路徑為 @vite/client 的指令碼,當訪問 index.html 時,就會拉取該指令碼

client.ts 在載入時,會建立 websocket 並監聽 message 事件

image-20220510195111037

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 之前的通訊

image-20220510203012725

server 模組轉換

該小節主要講這一部分

image-20220510203050804

模組程式碼轉換 vite 的核心,這部分足以開一個大的主題去講,同樣的,本文只會介紹個大概,只需要知道 vite 會轉換程式碼即可,轉換細節暫時可以不關注,把 vite server 當做一個黑箱

之前說的到,vite 的 plugin-vue 外掛,將熱更新程式碼注入到模組中,就是在編譯轉換模組的過程中處理

image-20220510204346277

從圖中可以看出,index.vue 經過編譯後,內容是 js 程式碼,其中還能看到 import.meta.hot.accept 定義熱更新的回撥

時序圖中,有個迴圈條件,直到動態 import 的模組沒有模組依賴,是什麼意思?

假如有以下兩個檔案:

text index.vue - useData.ts

index.vue 依賴(import)了 useData.ts

當修改 useData.ts 時,會執行以下的步驟:

  1. vite 沿著依賴樹,往上找到 index.vue,作為熱更新邊界
  2. server 將熱更新邊界資訊,通過 websocket 傳遞到 client
  3. client 執行老的 index.vueimport.meta.hot.dispose 回撥
  4. client 動態 import(index.vue),vite 會重新編譯 index.vue
  5. 執行 index.vue 的程式碼(此時請求到 index.vue 雖然是 vue 字尾,但是它的內容經過編譯後,是 js 程式碼),執行過程中遇到 import useData.ts

image-20220510213354073

  1. 動態拉取 useData.ts 模組,vite 會重新編譯 useData.ts
  2. 執行 useData.ts 的程式碼
  3. client 執行新的 index.vueimport.meta.hot.accept 回撥

因為熱更新邊界的模組,可能會存在依賴,import 了其他模組,這些模組都需要 import 拉取,直到動態 import 的模組沒有模組依賴

參考資料