Desmond

Desmond

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

深入了解 Vue 中的注入

刷新#

库的作者经常使用 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/

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。