刷新#
库的作者经常使用 provide/inject
API 将复杂的上下文信息传递给他们的组件和组合函数。与 props 不同,props 需要消费者显式传递依赖项,依赖注入可以无缝地传递这些依赖项,而消费者甚至没有意识到。这意味着依赖项对消费者变得不可见,有效地抽象掉了不必要的复杂性。
import { provide, ref } from 'vue';
export function useAppUserContext() {
const user = ref(null);
onMounted(() => {
// 获取已登录用户
user.value = await fetch('https://tes.t/me').json();
});
function logout() {
user.value = null;
}
function update(value) {
user.value = { ...user.value, ...value };
}
const ctx = {
user,
update,
logout
};
provide('AUTH_USER', ctx);
return ctx;
}
这个独立的函数可能会在你的根应用组件的 setup
函数中被调用。这使得所有子组件可以轻松地注入用户上下文并根据需要使用它。这在许多方面类似于商店,但也作为一种机制,使用户能够创建自己强大的迭代版本。
使用注入键#
声明一个根注入键的 injectionKeys.js
文件,包含你在应用中使用的所有注入键,并防止使用内联键值。
export const AUTH_USER_KEY = '已认证用户';
export const CART_KEY = '用户购物车';
符号是唯一的值,可以给出描述,并且一旦创建总是唯一的,它们也可以用作对象键。这意味着我们可以在这里使用它们作为注入键。
export const AUTH_USER_KEY = Symbol('用户');
// 一些注入稍后...
export const CURRENT_USER_KEY = Symbol('用户');
使用 TypeScript#
Vue 暴露了一个类型 InjectionKey<TValue>
,它神奇地让 provide/inject
推断和检查传递的值的类型。以下是一个简单的示例:
// types.ts
interface UserCartContext {
items: CartItem[];
total: number;
}
// injectionKeys.ts
import { InjectionKey } from 'vue';
import { UserCartContext } from '@/types';
const CART_KEY: InjectionKey<UserCartContext> = Symbol(
'用户购物车上下文'
);
这将在提供和注入级别提供类型检查,这是你不应该放弃的。
import { provide, inject, reactive } from 'vue';
import { CART_KEY } from '@/injectionKeys';
// ❌ 类型错误
provide(CART_KEY, {});
const cart = reactive({ items: [], total: 0 });
// ✅
provide(CART_KEY, cart);
const cart = inject(CART_KEY);
// ❌ 类型错误
cart?.map(...);
// ✅
cart?.items.map(...)
值得注意的是,inject
函数生成的解析类型与 undefined
结合在一起。这是因为注入可能未被解析。如何处理这种情况取决于用户。为了消除 undefined
,必须将后备值传递给 inject
函数。有趣的是,后备值也需要通过类型检查。
import { inject } from 'vue';
import { ProductKey } from '@/symbols';
// ⛔️ 类型 'string' 的参数不能分配给 ...
const product = inject(ProductKey, 'nope');
// ✅ 类型检查通过
const product = inject(ProductKey, { name: '', price: 0 });
虽然你可以提供普通值类型,但它们并不是那么有用,因为通常你需要对这些值的变化做出反应。你也可以使用泛型类型创建响应式注入。
对于使用 ref
创建的响应式引用,你可以使用泛型 Ref
类型来为你的 InjectionKey
类型,因此这是一个嵌套的泛型类型:
// types.ts
interface Product {
name: string;
price: number;
}
// symbols.ts
import { InjectionKey, Ref } from 'vue';
import { Product } from '@/types';
const ProductKey: InjectionKey<Ref<Product>> = Symbol('产品');
import { inject } from 'vue';
import { ProductKey } from '@/symbols';
const product = inject(ProductKey); // 类型为 Ref<Product> | undefined
product?.value; // 类型为 Product
需要注入#
默认情况下,如果 Vue 未解析注入,则会显示警告。Vue 本可以选择在找不到注入时抛出错误,但 Vue 不能真正假设注入是 required
还是不是,因此你需要自己理解未解析的注入和 undefined
值。
function injectStrict<T>(key: InjectionKey<T>, fallback?: T) {
const resolved = inject(key, fallback);
if (!resolved) {
throw new Error(`无法解析 ${key.description}`);
}
return resolved;
}
import { injectStrict } from '@/utils';
import { USER_CART_KEY } from '@/injectionKeys';
const cart = injectStrict(USER_CART_KEY);
// 这里可以安全地访问 `cart.items.map`
cart.items.map(...);
注入作用域#
import { getCurrentInstance, inject, InjectionKey } from 'vue';
export function injectWithSelf<T>(
key: InjectionKey<T>
): T | undefined {
const vm = getCurrentInstance() as any;
return vm?.provides[key as any] || inject(key);
}
这个函数的作用是,在尝试使用 inject
解析提供的依赖项之前,先尝试在同一组件提供的依赖项中查找它。否则,它将遵循与 inject
相同的行为。
单例注入#
import { provide } from 'vue';
import { CURRENT_USER_CTX } from '@/injectionKeys';
import { injectWithSelf } from '@/utils';
export function useAppUserContext() {
const existingContext = injectWithSelf(CURRENT_USER_CTX, null);
if (existingContext) {
return existingContext;
}
const user = ref(null);
onMounted(() => {
// 获取已登录用户
user.value = await fetch('https://tes.t/me').json();
});
function logout() {
user.value = null;
}
function update(value) {
user.value = { ...user.value, ...value };
}
const ctx = {
user,
update,
logout
};
provide(CURRENT_USER_CTX, ctx);
return ctx;
}
通过实现这种方法,无论你在何处调用 useAppUserContext
,代码只会运行一次。随后,所有其他调用将使用已经注入的实例。因此,不再需要在组件中使用 inject
。当你遵循这种模式时,可以使用上下文函数来注入自身,这比直接使用 inject
和我们自定义的版本更直观和用户友好。值得注意的是,这个过程确保在单个组件树中只创建一次注入。
参考文献:
https://logaretm.com/blog/making-the-most-out-of-vuejs-injections/
https://logaretm.com/blog/type-safe-provide-inject/