Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

淺入淺出 Vue 中的注入

刷新#

庫的作者經常使用 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/

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。