刷新#
庫的作者經常使用 provide/inject
API 將複雜的上下文信息傳遞給他們的組件和組合函數。與需要消費者明確傳遞依賴的 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
還是 optional
,因此由你來理解未解析的注入和 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/