在 vue3 中可以使用 provide / inject
來(lái)提供 / 注入一些公共數(shù)據(jù)荣暮,但是被 inject 的組件能不能獲取到完全取決于它的祖先組件有沒(méi)有提供這個(gè)值。也就是說(shuō)彤悔,在子組件中無(wú)法得知這個(gè)值是否真的存在浪蹂。
而本文就來(lái)介紹一下如何類(lèi)型安全的在 vue3 中使用 provide / inject
吼拥。
前提準(zhǔn)備
在很多網(wǎng)站里 用戶(hù)信息 都是一個(gè)典型的全局公共數(shù)據(jù),而本文就將以其作為示例◇ψ纾現(xiàn)在假設(shè)我們?cè)?@/types
里有如下定義:
/** 用戶(hù)信息 */
export interface UserInfo {
username: string
}
/** 設(shè)置新的用戶(hù)信息 可以設(shè)置為空 */
export type SetUserInfo = (newInfo: UserInfo | undefined) => void
一個(gè)用戶(hù)信息,以及一個(gè)設(shè)置用戶(hù)信息的方法状囱,注意用戶(hù)信息是可以被設(shè)置為空的术裸,為空時(shí)就代表用戶(hù)沒(méi)有登錄。
不安全的示例
首先來(lái)看一下不安全的寫(xiě)法是什么樣的亭枷,首先在 App.vue
中對(duì)其進(jìn)行初始化并 provide 出去:
import { ref, defineComponent, provide } from 'vue';
import { UserInfo, SetUserInfo } from '@/types';
export default defineComponent({
setup() {
// 初始化
const userInfo = ref<UserInfo | undefined>(undefined);
const setUserInfo: SetUserInfo = newInfo => userInfo.value = newInfo;
// 提供出去
provide('userInfo', userInfo)
provide('setUserInfo', setUserInfo)
}
})
然后在其子組件 Login.vue
中注入這兩者:
import { defineComponent, inject } from 'vue';
export default defineComponent({
setup() {
// 這兩者的類(lèi)型都為 unknow
const userInfo = inject('userInfo');
const setUserInfo = inject('setUserInfo');
}
})
這樣代碼可以正常運(yùn)行么袭艺?顯然是可以的,因?yàn)樵谖覀兊脑O(shè)計(jì)里 Login 組件永遠(yuǎn)在 App 組件之下叨粘,但是這并不安全猾编,ts 會(huì)告訴我們這兩者的類(lèi)型都是 unknow
,就算代碼可以正常運(yùn)行升敲,類(lèi)型推導(dǎo)突然中斷也是很難受的答倡,為了解決這個(gè)問(wèn)題,很多人會(huì)選擇使用 as
斷言一下:
const userInfo = inject('userInfo') as UserInfo;
const setUserInfo = inject('setUserInfo') as SetUserInfo;
雖然類(lèi)型推導(dǎo)回來(lái)了驴党,但是這樣一把梭也并不是什么好事瘪撇,如果 provide 的值發(fā)生了變化,分散在項(xiàng)目各處的 as 會(huì)變成一個(gè)個(gè)的定時(shí)炸彈港庄。
使用 InjectionKey
在 Provide / Inject Vue3 文檔 中提到了可以使用 InjectionKey 在提供者和消費(fèi)者之間同步注入值的類(lèi)型倔既。所以我們可以用它來(lái)完善一下代碼。
首先我們?cè)?@/symbols
里創(chuàng)建注入值索引:
import { InjectionKey, Ref } from 'vue';
import { UserInfo, SetUserInfo } from '@/types';
/** 全局的用戶(hù)信息 InjectionKey */
export const userInfoKey: InjectionKey<Ref<UserInfo | undefined>> = Symbol();
/** 全局的設(shè)置用戶(hù)信息方法 InjectionKey */
export const setUserInfoKey: InjectionKey<SetUserInfo> = Symbol();
然后在 provide 的時(shí)候使用這些 key 來(lái)代替字符串索引攘轩,這樣叉存,在提供時(shí)就會(huì)進(jìn)行一次校驗(yàn),防止提供了錯(cuò)誤的值:
// 之前的導(dǎo)入...
import { userInfoKey, setUserInfoKey } from '@/symbols';
export default defineComponent({
setup() {
const userInfo = ref<UserInfo | undefined>(undefined);
const setUserInfo: SetUserInfo = newInfo => userInfo.value = newInfo;
// 使用 symbols key 代替字符串 provide 值
provide(userInfoKey, userInfo)
provide(setUserInfoKey, setUserInfo)
}
})
然后度帮,在 inject 里也使用這些 key歼捏,這樣就可以正確的獲取到對(duì)應(yīng)的類(lèi)型。但是這里還有一個(gè)問(wèn)題笨篷,這兩者的值雖然類(lèi)型確定了瞳秽,但是都有可能為 undefined
。雖然我可以用短路操作符進(jìn)行判斷率翅,但是寫(xiě)多了還是有點(diǎn)煩的练俐,有方法能避免這個(gè)問(wèn)題么?
// 之前的導(dǎo)入...
import { userInfoKey, setUserInfoKey } from '@/symbols';
export default defineComponent({
setup() {
const userInfo = inject(userInfoKey);
const setUserInfo = inject(setUserInfoKey);
userInfo.value // ts 報(bào)錯(cuò) > Object is possibly 'undefined'
userInfo?.value // 不會(huì)報(bào)錯(cuò)冕臭,類(lèi)型為 Ref<UserInfo | undefined>
}
})
給 inject 提供默認(rèn)值
解決上面問(wèn)題的方法也很簡(jiǎn)單腺晾,給 inject 提供一個(gè)初始值即可:
const userInfo = inject(userInfoKey, ref(undefined));
const setUserInfo = inject(setUserInfoKey, () => {});
userInfo.value; // 類(lèi)型為 Ref<UserInfo | undefined>
setUserInfo({ username: 'abc' }); // 不會(huì)報(bào)錯(cuò)
現(xiàn)在我們已經(jīng)獲得了完整舒適的 inject 類(lèi)型推導(dǎo)燕锥。但是讓我們思考一下,這時(shí)候如果真的遇到父組件沒(méi)有 provide 值的情況會(huì)發(fā)生什么悯蝉。
首先 userInfo 為空時(shí)使用默認(rèn)值是沒(méi)問(wèn)題的归形,因?yàn)樗旧硪彩怯锌赡転?undefined 的。頁(yè)面頂多會(huì)將其判斷為沒(méi)有登錄鼻由。但是 setUserInfo 使用默認(rèn)值時(shí)就有大問(wèn)題了暇榴,用戶(hù)點(diǎn)擊了登錄按鈕,但是 setUserInfo 什么都沒(méi)有做蕉世,也就是說(shuō)頁(yè)面不會(huì)有任何響應(yīng)蔼紧。
這在代碼層面時(shí)沒(méi)有問(wèn)題的,但是卻影響了業(yè)務(wù)狠轻,并且由于我們提供了一個(gè)默認(rèn)的空函數(shù)奸例,所以我們甚至在控制臺(tái)里找不到任何報(bào)錯(cuò)。
完善默認(rèn)值
將 setUserInfo 默認(rèn)值修改為如下形式即可哈误,
const userInfo = inject(userInfoKey, ref(undefined));
// 觸發(fā)默認(rèn)函數(shù)時(shí)進(jìn)行兜底處理
const setUserInfo = inject(setUserInfoKey, () => {
message.error('登錄失敗哩至,請(qǐng)聯(lián)系管理員');
throw new Error('setUserInfo 獲取失敗');
});
雖然這種情況一般不會(huì)出現(xiàn),但是一旦出現(xiàn)就會(huì)帶來(lái)不少的 debug 工作量蜜自,特別是面對(duì)復(fù)雜的注入項(xiàng)菩貌。所以我把這一部分單獨(dú)作為一個(gè)小節(jié):不要忘了對(duì) Inject 函數(shù)的默認(rèn)行為進(jìn)行兜底,最少也要打印些東西讓 debug 時(shí)可以發(fā)現(xiàn)重荠。
下面也是一種兜底方式:
const userInfo = inject(userInfoKey);
const setUserInfo = inject(setUserInfoKey);
if (!userInfo || !setUserInfo) {
throw new Error('userInfo 獲取失敗');
}
但是要注意外層要 try / catch 進(jìn)行處理防止打斷 setup 的執(zhí)行箭阶。不然由于 setup 沒(méi)有正常返回響應(yīng)式數(shù)據(jù),模板里的綁定值實(shí)際上是獲取不到的戈鲁,這會(huì)導(dǎo)致 ts 檢查失敗仇参。
這里有一個(gè)小坑,上面的這個(gè)問(wèn)題在
Volar
插件安裝時(shí)是可以正常的在編輯器里提示出紅波浪線的婆殿。但是如果你偷懶像我一樣還在用Vetur
诈乒,那么就會(huì)發(fā)現(xiàn)編輯器里完全沒(méi)有報(bào)錯(cuò),但打包一直提示Cannot find name xxx
婆芦。