如何在 vue3 中提供一個(gè)類(lèi)型安全的 inject

在 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婆芦。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末怕磨,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子消约,更是在濱河造成了極大的恐慌肠鲫,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件或粮,死亡現(xiàn)場(chǎng)離奇詭異导饲,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)渣锦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)硝岗,“玉大人,你說(shuō)我怎么就攤上這事袋毙”惭龋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵娄猫,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我生闲,道長(zhǎng)媳溺,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任碍讯,我火速辦了婚禮悬蔽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘捉兴。我一直安慰自己蝎困,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布倍啥。 她就那樣靜靜地躺著禾乘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪虽缕。 梳的紋絲不亂的頭發(fā)上始藕,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音氮趋,去河邊找鬼伍派。 笑死,一個(gè)胖子當(dāng)著我的面吹牛剩胁,可吹牛的內(nèi)容都是我干的诉植。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼昵观,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼晾腔!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起索昂,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤建车,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后椒惨,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體缤至,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了领斥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嫉到。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖月洛,靈堂內(nèi)的尸體忽然破棺而出何恶,到底是詐尸還是另有隱情,我是刑警寧澤嚼黔,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布细层,位于F島的核電站,受9級(jí)特大地震影響唬涧,放射性物質(zhì)發(fā)生泄漏疫赎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一碎节、第九天 我趴在偏房一處隱蔽的房頂上張望捧搞。 院中可真熱鬧,春花似錦狮荔、人聲如沸胎撇。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)晚树。三九已至,卻和暖如春受葛,著一層夾襖步出監(jiān)牢的瞬間题涨,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工总滩, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纲堵,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓闰渔,卻偏偏與公主長(zhǎng)得像席函,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子冈涧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容