极致舒适的Vue【Provide/Inject】食用指南
两个inject
工具函数轻松解决严格注入和Hook返回值透传问题!
痛苦的经历
自从使用了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' });
后代组件中通过inject
获取祖先组件注入的值,示例代码如下:
import { inject } from 'vue';
const message = inject('account');
当只是在项目中小范围的使用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');
用法示例:
Main.vue
:
import { provide } from 'vue';
import { CurAccountKey } from '@/constants/injectionKeys';
const user = reactive({ id: 1, name: 'youth' });
provide(CurAccountKey, user);
ParentView.vue
:
import { provide } from 'vue';
import { AuthAccountKey } from '@/constants/injectionKeys';
const user = reactive({ id: 1, name: 'John Doe' });
provide(AuthAccountKey, user);
DeepChild.vue
:
import { inject } from 'vue';
import { AuthAccountKey, CurAccountKey } from '@/constants/injectionKeys';
const curAccount = inject(CurAccountKey);
const authAccount = inject(AuthAccountKey);
注入提示
但是使用inject(CurAccountKey)
会代码什么样的数据?这就不得不全局查找CurAccountKey
的provide
了。这种的使用体验十分不好,这时Vue官方推荐我们使用TS。
import { inject } from 'vue';
import { AuthAccountKey, CurAccountKey } from '@/constants/injectionKeys';
const curAccount = inject(CurAccountKey);
curAccount.name; // curAccount存在name吗?
实践方案
Vue|为provide / inject 标注类型中提到了InjectionKey
类型,使用TS和InjectionKey
可以有效解决类型提示问题
src/types.ts
:
export interface Account {
name: string;
id: number;
};
src/constants/injection-key.ts
:
import { InjectionKey } from 'vue';
import { Account } from '@/types';
export const CurAccountKey: InjectionKey<Account> = Symbol('account')
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); // 💯
DeepChild.vue
:
const curAccount = inject(CurAccountKey);
curAccount?.age; // ❌
curAccount?.id; // 💯
严格注入
默认情况下,inject
假设传入的注入名会被某个祖先链上的组件提供。如果该注入名的确没有任何组件提供,则会抛出一个运行时警告
const curAccount = inject(CurAccountKey);
curAccount?.id;
当然有时候我们可能并不是要求必须在祖先链上提供,这时候Vue官方推荐我们使用默认值来解决祖先链未提供值的情况,这也仅仅是能解决inject
值不是必要值的情况
但是有些情况下我们又要求祖先链上必须提供需要的inject
,这种情况更常见的是通用型组件开发中。例如:<ElTable>
和<ElTableColumn>
组件,<ElTableColumn>
的祖先链上必须存在<ElTable>
组件。如果单独使用<ElTableColumn>
是不合法的,这时候应该抛出错误❌而不是警告⚠️
要解决上面的严格依赖问题,我们当然可以在子组件中通过判断inject
的值是否为undefined
,如果是则抛出异常。这种代码很简单:
const curAccount = inject(CurAccountKey);
if (!curAccount) {
throw new Error('CurAccountKey必须提供对应的Provide');
}
curAccount.id;
嗯,不错!是解决了问题!如果严格依赖的很多呢?难不成到处都是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;
}
使用injectStrict
重写吧:
const curAccount = injectStrict(CurAccountKey);
curAccount.id;
再谈逐级穿透
在Vue中Provide组件无法使用provide值
这个看着有点绕,直观来看使用情况是这样的:
const user = reactive({ id: 1, name: 'youth' });
provide(CurAccountKey, user);
...
inject(CurAccount); // 这里无法获取👆提供的user
哎?😯这时候有的同学肯定会说,Provide组件使用provide
的值?有没有搞错啊?怎么会有这种操作?
const user = reactive({ id: 1, name: 'youth' });
provide(CurAccountKey, user);
//这里需要user值的时候,直接用不就好了??
user;
逐级透传问题又来了
但是,别忘了自定义hook
的情况啊!!如果provide(CurAccountKey, user);
是在一个自定义的hook
中的呢?
useAccount.ts
:
export const useAccount = async () => {
const user = await fetch('/**/*');
provide(CurAccountKey, user);
return { user };
}
如果是直接调用useAccount
还不是问题,因为useAccount
返回了user
。在调用userAccount
的地方可以直接解构出user
,这样很直观也很方便。
如果useAccount
被其他的hook
再次封装呢?
useApp.ts
:
export const useApp = async () => {
const account = await useAccount();
...
return {
account
}
}
当然,这也不是没有解决方法,可以在useApp
中解构account
再返回
useApp.ts
:
export const useApp = async () => {
const account = await useAccount();
...
return {
...account
}
}
等下?有没有觉得这种情况很熟悉?我们把hook
换成组件,情况是不是就是这样:
不能说相似,只能说一毛一样啊!Provide/Inject
的出现就是为了解决这样的问题,但是当在hook
中出现透传时,却又成了最初的样子啊!
屠龙勇士最终变成恶龙?
你可能会说那没办法呀!Vue无法在当前组件中获取到当前组件的provide
,还能有什么好招?先🐶着吧!
实践方案
解决上面问题的方案也很简单,就是获取当前组件实例,然后从组件实例中找到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);
}
这里我们从当前组件的实例中找到对应key的provide
值,如果不存在就走inject
从祖先链组件中获取。
使用injectWithSelf
重写一下吧:
useAccount.ts
:
export const useAccount = async () => {
const user = await fetch('/**/*');
provide(CurAccountKey, user);
return { user };
}
useApp.ts
:
export const useApp = async () => {
const account = await useAccount();
...
return {
account
}
}
Main.vue
:
useApp();
// 必须在useApp()之后
const user = injectWithSelf(CurAccountKey)
最后
- 使用
Symbol
来创建注入名,来回避取名冲突 - 使用TS和
InjectionKey
可以有效解决类型提示问题 - 使用自定义
injectStrict
可以解决严格注入问题 - 使用自定义
injectWithSelf
可以解决hook
嵌套时的返回值逐级穿透问题
如果你觉得这篇文章对你在开发中有所帮助,麻烦多点赞评论收藏😊
如果...,麻烦多点赞评论收藏😊
转载自:https://juejin.cn/post/7267487352455921723