0. 啥是useId
Vue 3.5中新增的useId
函式主要用於生成唯一的ID,這個ID在同一個Vue應用中是唯一的,並且每次呼叫useId
都會生成不同的ID。這個功能在處理列表渲染、表單元素和無障礙屬性時非常有用,因為它可以確保每個元素都有一個唯一的識別符號。
useId
的實現原理相對簡單。它透過訪問Vue例項的ids
屬性來生成ID,這個屬性是一個數組,其中包含了用於生成ID的字首和自增數字。每次呼叫useId
時,都會取出當前的數字值,然後進行自增操作。這意味著在同一頁面上的多個Vue應用例項可以透過配置app.config.idPrefix
來避免ID衝突,因為每個應用例項都會維護自己的ID生成序列。
1. 實現原始碼
export function useId(): string { const i = getCurrentInstance() if (i) { return (i.appContext.config.idPrefix || 'v') + '-' + i.ids[0] + i.ids[1]++ } else if (__DEV__) { warn( `useId() is called when there is no active component ` + `instance to be associated with.`, ) } return '' }
i.appContext.config.idPrefix
:這是從當前元件例項中獲取的一個配置屬性,用於定義生成ID的字首。如果這個字首存在,它將被使用;如果不存在,預設使用'v'
。i.ids[0]
:這是當前元件例項上的ids
陣列的第一個元素,它是一個字串,通常為空字串,用於生成ID的一部分。i.ids[1]++
:這是ids
陣列的第二個元素,它是一個數字,用於生成ID的自增部分。這裏使用了後置自增運算子++
,這意味著它會返回當前值然後自增。每次呼叫useId
時,這個數字都會增加,確保生成的ID是唯一的。
2.設定ID字首
如果不想使用預設的字首'v'
的話,可以透過app.config.idPrefix
進行設定。
const app = createApp(App) app.config.idPrefix = 'vid'
3.使用場景
3-1. 表單元素的唯一標識
在表單中,<label>
標籤需要透過 for
屬性與對應的 <input>
標籤的 id
屬性相匹配,以實現點選標籤時輸入框獲得焦點的功能。使用 useId
可以為每個 <input>
元素生成一個唯一的 id
,確保這一功能的正常工作。例如:
<label :for="id">Do you like Vue 3.5?</label> <input type="checkbox" :id="id" />
const id = useId()
3-2. 列表渲染中的唯一鍵
在渲染列表時,每一項通常需要一個唯一的鍵(key),以幫助 Vue 追蹤每個節點的身份,從而進行高效的 DOM 更新。如果你的列表資料沒有唯一key的話,那麼useId
可以為列表中的每個專案生成一個唯一的鍵。
<ul> <li v-for="item in items" :key="item.id"> {{ item.text }}({{ item.id }}) </li> </ul>
const items = Array.from({ length: 10}, (v, k) => { return { text: `Text ${k}`, id: useId() } })
上述程式碼渲染結果如下:
3-3. 服務端渲染(SSR)中避免 ID 衝突
在服務端渲染(SSR)的應用中,頁面的HTML內容是在伺服器上生成的,然後傳送給客戶端瀏覽器。在客戶端,瀏覽器會接收到這些HTML內容,並將其轉換成一個可互動的頁面。如果在伺服器端和客戶端生成的HTML中存在相同的ID,那麼在客戶端啟用(hydrate)時,就可能出現問題,因為客戶端可能會嘗試操作一個已經由伺服器端渲染的DOM元素,導致潛在的衝突或錯誤。
下面是一個使用useId
來避免這種ID衝突的實際案例:
服務端程式碼 (server.js)
import { createSSRApp } from 'vue'; import { renderToString } from '@vue/server-renderer'; import App from './App.vue'; const app = createSSRApp(App); // 假設我們在這裏獲取了一些資料 const data = fetchData(); renderToString(app).then(html => { // 將服務端渲染的HTML傳送給客戶端 sendToClient(html); });
客戶端程式碼 (client.js)
import { createSSRApp } from 'vue'; import App from './App.vue'; const app = createSSRApp(App); // 客戶端啟用,將服務端渲染的HTML轉換成可互動的頁面 hydrateApp(app);
在這個案例中,無論是服務端還是客戶端,我們都使用了createSSRApp(App)
來建立應用例項。如果我們在App.vue
中使用了useId
來生成ID,那麼這些ID將在服務端渲染時生成一次,並在客戶端啟用時再次使用相同的ID。
App.vue 元件
<template> <div> <input :id="inputId" type="text" /> <label :for="inputId">Enter text:</label> </div> </template> <script setup> import { useId } from 'vue'; const inputId = useId(); </script>
在App.vue
元件中,我們使用了useId
來為<input>
元素生成一個唯一的ID。這個ID在服務端渲染時生成,幷包含在傳送給客戶端的HTML中。當客戶端接收到這個HTML並開始啟用過程時,由於useId
生成的ID在服務端和客戶端是相同的,所以客戶端可以正確地將<label>
元素關聯到<input>
元素,而不會出現ID衝突的問題。
如果沒有使用useId
,而是使用了Math.random()
或Date.now()
來生成ID,那麼服務端和客戶端可能會生成不同的ID,導致客戶端在啟用時無法正確地將<label>
和<input>
關聯起來,因為它們具有不同的ID。這可能會導致表單元素的行為異常,例如點選<label>
時,<input>
無法獲得焦點。
3-4. 元件庫中的 ID 生成
在使用 Element Plus 等元件庫進行 SSR 開發時,爲了避免 hydration 錯誤,需要確保伺服器端和客戶端生成相同的 ID。透過在 Vue 中注入 ID_injection_key
,可以確保 Element Plus 生成的 ID 在 SSR 中是唯一的。
// src/main.js import { createApp } from 'vue' import { ID_INJECTION_KEY } from 'element-plus' import App from './App.vue' const app = createApp(App) app.provide(ID_INJECTION_KEY, { prefix: 1024, current: 0, })