web前端开发Vue

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

2021-07-02  本文已影响0人  HoPGoldy

在 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

参考

上一篇下一篇

猜你喜欢

热点阅读