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, })