• Vue3【Provide/Inject】


    前言

    自从使用了Provide/Inject代码的组织方式更加灵活了,但是这个灵活性的增加伴随着代码容错性的降低。我相信只要是真的在项目中引入Provide/Inject的同学,一定一定有过或者正在经历下面的状况:

    • 注入名(Injection key)经常拼错,又或者注入名太多导致注入名取名困难(程序员通病)
    • 为了弄清楚inject()注入的是啥,不得不找到对应provide()
    • 另一种情况是重复provide()同一值,导致Injection覆盖
    • 使用inject()时祖先链上未必存在对应的provide(),不得不做空值处理或默认值处理
    • 在hook中使用provide(),但是调用hook的组件无法inject()这个hook的provide()

    Provide/Inject解决了什么问题?

    依赖注入|Vue.js中提到Provide/Inject这两个API主要是用来解决Prop逐级透传问题(就像下面这样)

    在这里插入图片描述
    引入Provide/Inject后Prop就可以直接传入到后代组件(就像下面这样)
    在这里插入图片描述
    根组件中通过provide提供注入值,示例代码如下:

    import { provide } from 'vue';
    
    provide(/* 注入名 */ 'account', /* 值 */ { name: 'youth' });
    
    • 1
    • 2
    • 3

    后代组件中通过inject获取祖先组件注入的值,示例代码如下:

    import { inject } from 'vue';
    
    const message = inject('account');
    
    • 1
    • 2
    • 3

    当只是在项目中小范围的使用provide和inject时,上面示例的写法没什么问题。但是如果项目工程较大,代码量也多的情况下,就会出现一些问题。

    注入名冲突

    问题是如何保证account不会被其他业务组件覆盖?例如如果某个业务组件也提供了account的信息,就像下面这样:
    在这里插入图片描述

    中间层的ParentView组件可能是一个用户列表组件,也提供了account数据,这里的account可能是列表选中的用户,而Main中提供的是当前用户。在DeepChild组件中可能即需要当前登录用户信息,又需要列表选中的用户信息,而目前DeepChild中只能获取到ParentView提供的选中用户信息。

    当然这种业务场景有很多解决方案,这里先认为只能通过provide/inject解决

    当然我们完全可以在ParentView中将注入名改写为selectAccount来解决这个问题,但是如果中间层还有其他的组件,这些组件也有selectAccount呢?

    实践方案

    在项目中创建一个名为injection-key.ts的文件,我习惯将该文件创建为src/constants/injection-key.ts。这样在该文件中统一管理项目下的注入名,并且使用Symbol来创建注入名,来回避取名冲突.

    export const CurAccountKey = Symbol('account');
    
    export const AuthAccountKey = Symbol('account');
    
    • 1
    • 2
    • 3

    用法示例:

    Main.vue:

    import { provide } from 'vue';
    import { CurAccountKey } from '@/constants/injectionKeys';
    
    const user = reactive({ id: 1, name: 'youth' });
    provide(CurAccountKey, user);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ParentView.vue:

    import { provide } from 'vue';
    import { AuthAccountKey } from '@/constants/injectionKeys';
    
    const user = reactive({ id: 1, name: 'John Doe' });
    provide(AuthAccountKey, user);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    DeepChild.vue:

    import { inject } from 'vue';
    import { AuthAccountKey, CurAccountKey } from '@/constants/injectionKeys';
    
    const curAccount = inject(CurAccountKey);
    const authAccount = inject(AuthAccountKey);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    注入提示

    但是使用inject(CurAccountKey)会代码什么样的数据?这就不得不全局查找CurAccountKey的provide了。这种的使用体验十分不好,这时Vue官方推荐我们使用TS。

    import { inject } from 'vue';
    import { AuthAccountKey, CurAccountKey } from '@/constants/injectionKeys';
    
    const curAccount = inject(CurAccountKey);
    curAccount.name; // curAccount存在name吗?
    
    • 1
    • 2
    • 3
    • 4
    • 5

    实践方案

    Vue|为provide / inject 标注类型中提到了InjectionKey类型,使用TS和InjectionKey可以有效解决类型提示问题

    src/types.ts:

    export interface Account {
      name: string;
      id: number;
    };
    
    • 1
    • 2
    • 3
    • 4

    src/constants/injection-key.ts:

    import { InjectionKey } from 'vue';
    import { Account } from '@/types';
    
    export const CurAccountKey: InjectionKey<Account> = Symbol('account')
    
    • 1
    • 2
    • 3
    • 4

    Main.vue:

    import { provide } from 'vue';
    import { CurAccountKey } from '@/constants/injectionKeys';
    
    const user = reactive({ id: 1, name: 'youth' });
    provide(CurAccountKey, 'name: youth'); // ❌
    provide(CurAccountKey, user); // 💯
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    DeepChild.vue:

    const curAccount = inject(CurAccountKey);
    curAccount?.age; // ❌
    curAccount?.id; // 💯
    
    • 1
    • 2
    • 3

    严格注入

    默认情况下,inject假设传入的注入名会被某个祖先链上的组件提供。如果该注入名的确没有任何组件提供,则会抛出一个运行时警告

    const curAccount = inject(CurAccountKey);
    curAccount?.id;
    
    • 1
    • 2

    当然有时候我们可能并不是要求必须在祖先链上提供,这时候Vue官方推荐我们使用默认值来解决祖先链未提供值的情况,这也仅仅是能解决inject值不是必要值的情况

    但是有些情况下我们又要求祖先链上必须提供需要的inject,这种情况更常见的是通用型组件开发中。例如:和组件,的祖先链上必须存在组件。如果单独使用是不合法的,这时候应该抛出错误❌而不是警告⚠️

    要解决上面的严格依赖问题,我们当然可以在子组件中通过判断inject的值是否为undefined,如果是则抛出异常。这种代码很简单:

    const curAccount = inject(CurAccountKey);
    if (!curAccount) {
      throw new Error('CurAccountKey必须提供对应的Provide');
    }
    curAccount.id;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    嗯,不错!是解决了问题!如果严格依赖的很多呢?难不成到处都是if判断?

    实践方案

    创建一个严格注入工具函数,当对应的注入名没有被提供时抛出异常。

    export const injectStrict = <T>(key: InjectionKey<T>, defaultValue?: T | (() => T), treatDefaultAsFactory?: false): T => {
      const result = inject(key, defaultValue, treatDefaultAsFactory); 
      if (!result) { 
        throw new Error(`Could not resolve ${key.description}`); 
      } 
      return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    使用injectStrict重写吧:

    const curAccount = injectStrict(CurAccountKey);
    curAccount.id;
    
    • 1
    • 2

    再谈逐级穿透

    在Vue中Provide组件无法使用provide值

    这个看着有点绕,直观来看使用情况是这样的:

    const user = reactive({ id: 1, name: 'youth' });
    provide(CurAccountKey, user);
    
    ...
    
    inject(CurAccount); // 这里无法获取👆提供的user
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这时候有的同学肯定会说,Provide组件使用provide的值?有没有搞错啊?怎么会有这种操作?

    const user = reactive({ id: 1, name: 'youth' });
    provide(CurAccountKey, user);
    
    //这里需要user值的时候,直接用不就好了??
    user;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    逐级透传问题又来了
    但是,别忘了自定义hook的情况啊!!如果provide(CurAccountKey, user);是在一个自定义的hook中的呢?

    useAccount.ts:

    export const useAccount = async () => {
      const user = await fetch('/**/*');
      provide(CurAccountKey, user);
      return { user };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果是直接调用useAccount还不是问题,因为useAccount返回了user。在调用userAccount的地方可以直接解构出user,这样很直观也很方便。

    如果useAccount被其他的hook再次封装呢?

    useApp.ts:

    export const useApp = async () => {
      const account = await useAccount();
      ...
      return {
        account
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    当然,这也不是没有解决方法,可以在useApp中解构account再返回

    useApp.ts:

    export const useApp = async () => {
      const account = await useAccount();
      ...
      return {
        ...account
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    有没有觉得这种情况很熟悉?我们把hook换成组件,情况是不是就是这样:
    在这里插入图片描述
    Provide/Inject的出现就是为了解决这样的问题,但是当在hook中出现透传时,却又成了最初的样子啊!

    实践方案

    解决上面问题的方案也很简单,就是获取当前组件实例,然后从组件实例中找到provide的值就好了!

    既然Vue本身无法支持当前组件获取当前组件的provide,那我们自己实现一个吧!

    import { getCurrentInstance, inject, InjectionKey } from 'vue';
    
    export const injectWithSelf = <T>( key: InjectionKey<T>): T | undefined => { 
      const vm = getCurrentInstance() as any; 
      return vm?.provides[key as any] || inject(key);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里我们从当前组件的实例中找到对应key的provide值,如果不存在就走inject从祖先链组件中获取。

    使用injectWithSelf重写一下吧:

    useAccount.ts:

    export const useAccount = async () => {
      const user = await fetch('/**/*');
      provide(CurAccountKey, user);
      return { user };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    useApp.ts:

    export const useApp = async () => {
      const account = await useAccount();
      ...
      return {
        account
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    Main.vue:

    useApp();
    
    // 必须在useApp()之后
    const user = injectWithSelf(CurAccountKey)
    
    • 1
    • 2
    • 3
    • 4

    最后

    • 使用Symbol来创建注入名,来回避取名冲突
    • 使用TS和InjectionKey可以有效解决类型提示问题
    • 使用自定义injectStrict可以解决严格注入问题
    • 使用自定义injectWithSelf可以解决hook嵌套时的返回值逐级穿透问题
  • 相关阅读:
    关于Redis的远程连接 Connection: Disconnect on error 问题
    【路径规划-VRP问题】基于蚁群算法求解带载重和距离约束的车辆路径规划问题附matlab代码
    读《mysql是怎样运行的》有感
    Node.js精进(5)——HTTP
    sm2多端加密解密,java,js,android,ios实战
    运维学习笔记——arthas 线上
    Java之Stream流及方法引用的详细解析二
    Redis从基础到进阶篇(四)----性能调优、分布式锁与缓存问题
    中科驭数DPU芯片K2斩获2023年“中国芯”优秀技术创新产品奖
    如何在macbook上删除文件?Mac删除文件的多种方法
  • 原文地址:https://blog.csdn.net/qq_41961239/article/details/132708554