如何在 vue3 中提供一个类型安全的 inject

在 vue3 中可以使用 provide / inject 来提供 / 注入一些公共数据,但是被 inject 的组件能不能获取到完全取决于它的祖先组件有没有提供这个值。也就是说,在子组件中无法得知这个值是否真的存在。

而本文就来介绍一下如何类型安全的在 vue3 中使用 provide / inject

前提准备

在很多网站里 用户信息 都是一个典型的全局公共数据,而本文就将以其作为示例。现在假设我们在 @/types 里有如下定义:

/** 用户信息 */
export interface UserInfo {
    username: string
}

/** 设置新的用户信息 可以设置为空 */
export type SetUserInfo = (newInfo: UserInfo | undefined) => void

一个用户信息,以及一个设置用户信息的方法,注意用户信息是可以被设置为空的,为空时就代表用户没有登录。

不安全的示例

首先来看一下不安全的写法是什么样的,首先在 App.vue 中对其进行初始化并 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() {
        // 这两者的类型都为 unknow
        const userInfo = inject('userInfo');
        const setUserInfo = inject('setUserInfo');
    }
})

这样代码可以正常运行么?显然是可以的,因为在我们的设计里 Login 组件永远在 App 组件之下,但是这并不安全,ts 会告诉我们这两者的类型都是 unknow,就算代码可以正常运行,类型推导突然中断也是很难受的,为了解决这个问题,很多人会选择使用 as 断言一下:

const userInfo = inject('userInfo') as UserInfo;
const setUserInfo = inject('setUserInfo') as SetUserInfo;

虽然类型推导回来了,但是这样一把梭也并不是什么好事,如果 provide 的值发生了变化,分散在项目各处的 as 会变成一个个的定时炸弹。

使用 InjectionKey

Provide / Inject Vue3 文档 中提到了可以使用 InjectionKey 在提供者和消费者之间同步注入值的类型。所以我们可以用它来完善一下代码。

首先我们在 @/symbols 里创建注入值索引:

import { InjectionKey, Ref } from 'vue';
import { UserInfo, SetUserInfo } from '@/types';

/** 全局的用户信息 InjectionKey */
export const userInfoKey: InjectionKey<Ref<UserInfo | undefined>> = Symbol();

/** 全局的设置用户信息方法 InjectionKey */
export const setUserInfoKey: InjectionKey<SetUserInfo> = Symbol();

然后在 provide 的时候使用这些 key 来代替字符串索引,这样,在提供时就会进行一次校验,防止提供了错误的值:

// 之前的导入...
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,这样就可以正确的获取到对应的类型。但是这里还有一个问题,这两者的值虽然类型确定了,但是都有可能为 undefined。虽然我可以用短路操作符进行判断,但是写多了还是有点烦的,有方法能避免这个问题么?

// 之前的导入...
import { userInfoKey, setUserInfoKey } from '@/symbols';

export default defineComponent({
    setup() {
        const userInfo = inject(userInfoKey);
        const setUserInfo = inject(setUserInfoKey);

        userInfo.value // ts 报错 > Object is possibly 'undefined'
        userInfo?.value // 不会报错,类型为 Ref<UserInfo | undefined>
    }
})

给 inject 提供默认值

解决上面问题的方法也很简单,给 inject 提供一个初始值即可:

const userInfo = inject(userInfoKey, ref(undefined));
const setUserInfo = inject(setUserInfoKey, () => {});

userInfo.value; // 类型为 Ref<UserInfo | undefined>
setUserInfo({ username: 'abc' }); // 不会报错

现在我们已经获得了完整舒适的 inject 类型推导。但是让我们思考一下,这时候如果真的遇到父组件没有 provide 值的情况会发生什么。

首先 userInfo 为空时使用默认值是没问题的,因为它本身也是有可能为 undefined 的。页面顶多会将其判断为没有登录。但是 setUserInfo 使用默认值时就有大问题了,用户点击了登录按钮,但是 setUserInfo 什么都没有做,也就是说页面不会有任何响应。

这在代码层面时没有问题的,但是却影响了业务,并且由于我们提供了一个默认的空函数,所以我们甚至在控制台里找不到任何报错。

完善默认值

将 setUserInfo 默认值修改为如下形式即可,

const userInfo = inject(userInfoKey, ref(undefined));
// 触发默认函数时进行兜底处理
const setUserInfo = inject(setUserInfoKey, () => {
    message.error('登录失败,请联系管理员');
    throw new Error('setUserInfo 获取失败');
});

虽然这种情况一般不会出现,但是一旦出现就会带来不少的 debug 工作量,特别是面对复杂的注入项。所以我把这一部分单独作为一个小节:不要忘了对 Inject 函数的默认行为进行兜底,最少也要打印些东西让 debug 时可以发现。

下面也是一种兜底方式:

const userInfo = inject(userInfoKey);
const setUserInfo = inject(setUserInfoKey);

if (!userInfo || !setUserInfo) {
    throw new Error('userInfo 获取失败');
}

但是要注意外层要 try / catch 进行处理防止打断 setup 的执行。不然由于 setup 没有正常返回响应式数据,模板里的绑定值实际上是获取不到的,这会导致 ts 检查失败。

这里有一个小坑,上面的这个问题在 Volar 插件安装时是可以正常的在编辑器里提示出红波浪线的。但是如果你偷懒像我一样还在用 Vetur,那么就会发现编辑器里完全没有报错,但打包一直提示 Cannot find name xxx

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容