Skip to content
第 124 / 250 章前端⏱ 10 分钟阅读

第 124 章:provide / inject 依赖注入

学习目标

  • 掌握 provide/inject 的核心用法
  • 理解跨层级组件通信的实现机制
  • 学会类型化的依赖注入
  • 在项目中合理使用 provide/inject

一、为什么需要 provide/inject

1.1 prop drilling 问题

如果 Avatar 需要 user 数据,需要从 App → Layout → Sidebar → Content → Card → Avatar 层层传递,非常繁琐。

1.2 provide/inject 解决方案

App 直接 provide,任意后代 inject,无需逐层传递。

二、基础用法

2.1 provide

vue
<!-- Parent.vue -->
<script setup lang="ts">
import { ref, provide } from 'vue';

const theme = ref('dark');

// 提供响应式数据
provide('theme', theme);
provide('user', { name: 'Tom' });
</script>

<template>
  <slot />
</template>

2.2 inject

vue
<!-- AnyChild.vue (任意层级) -->
<script setup lang="ts">
import { inject } from 'vue';

const theme = inject('theme');          // ref('dark')
const user = inject('user');            // { name: 'Tom' }
</script>

2.3 默认值

typescript
import { inject } from 'vue';

// 默认值
const theme = inject('theme', 'light');

// 默认值为工厂函数
const user = inject('user', () => ({ name: 'Guest' }));

三、类型化注入

3.1 InjectionKey

typescript
// keys.ts
import type { InjectionKey, Ref } from 'vue';

export interface User {
  id: number;
  name: string;
}

export const ThemeKey: InjectionKey<Ref<string>> = Symbol('theme');
export const UserKey: InjectionKey<Ref<User | null>> = Symbol('user');

3.2 提供

vue
<script setup lang="ts">
import { ref, provide } from 'vue';
import { ThemeKey, UserKey } from './keys';

const theme = ref('dark');
const user = ref<User | null>(null);

provide(ThemeKey, theme);
provide(UserKey, user);
</script>

3.3 注入

vue
<script setup lang="ts">
import { inject } from 'vue';
import { ThemeKey, UserKey } from './keys';

// 类型安全,无需断言
const theme = inject(ThemeKey);
const user = inject(UserKey);

// theme.value: string
// user.value: User | null
</script>

四、实战场景

4.1 全局配置

typescript
// composables/useAppConfig.ts
import { provide, inject, ref, type InjectionKey, type Ref } from 'vue';

interface AppConfig {
  apiUrl: string;
  timeout: number;
  theme: 'light' | 'dark';
}

const ConfigKey: InjectionKey<Ref<AppConfig>> = Symbol('app-config');

export function provideAppConfig(config: AppConfig) {
  const configRef = ref(config);
  provide(ConfigKey, configRef);
  return configRef;
}

export function useAppConfig() {
  return inject(ConfigKey, ref({ apiUrl: '', timeout: 5000, theme: 'light' }));
}
vue
<!-- App.vue -->
<script setup lang="ts">
import { provideAppConfig } from '@/composables/useAppConfig';

provideAppConfig({
  apiUrl: 'https://api.example.com',
  timeout: 10000,
  theme: 'dark'
});
</script>
vue
<!-- AnyChild.vue -->
<script setup lang="ts">
import { useAppConfig } from '@/composables/useAppConfig';

const config = useAppConfig();
console.log(config.value.apiUrl);
</script>

4.2 用户认证

typescript
// composables/useAuth.ts
import { provide, inject, ref, computed, type InjectionKey, type Ref } from 'vue';

interface User {
  id: number;
  name: string;
}

const AuthKey: InjectionKey<{
  user: Ref<User | null>;
  isLoggedIn: Ref<boolean>;
  login: (credentials: LoginDTO) => Promise<void>;
  logout: () => void;
}> = Symbol('auth');

export function provideAuth() {
  const user = ref<User | null>(null);
  const isLoggedIn = computed(() => !!user.value);

  async function login(credentials: LoginDTO) {
    const res = await api.login(credentials);
    user.value = res.data.user;
    localStorage.setItem('token', res.data.token);
  }

  function logout() {
    user.value = null;
    localStorage.removeItem('token');
  }

  provide(AuthKey, { user, isLoggedIn, login, logout });
}

export function useAuth() {
  return inject(AuthKey);
}
vue
<!-- App.vue -->
<script setup lang="ts">
import { provideAuth } from '@/composables/useAuth';
provideAuth();
</script>
vue
<!-- UserMenu.vue -->
<script setup lang="ts">
import { useAuth } from '@/composables/useAuth';

const { user, isLoggedIn, logout } = useAuth()!;
</script>

<template>
  <div v-if="isLoggedIn">
    欢迎,{{ user?.name }}
    <button @click="logout">登出</button>
  </div>
</template>

4.3 国际化(简化版)

typescript
// composables/useI18n.ts
import { provide, inject, ref, type InjectionKey, type Ref } from 'vue';

const messages = {
  zh: { hello: '你好', bye: '再见' },
  en: { hello: 'Hello', bye: 'Bye' }
};

type Locale = keyof typeof messages;
const I18nKey: InjectionKey<{
  locale: Ref<Locale>;
  t: (key: keyof typeof messages.zh) => string;
}> = Symbol('i18n');

export function provideI18n(initialLocale: Locale = 'zh') {
  const locale = ref(initialLocale);

  const t = (key: keyof typeof messages.zh) => {
    return messages[locale.value][key] || key;
  };

  provide(I18nKey, { locale, t });
}

export function useI18n() {
  return inject(I18nKey)!;
}
vue
<script setup lang="ts">
import { useI18n } from '@/composables/useI18n';

const { locale, t } = useI18n();
</script>

<template>
  <p>{{ t('hello') }}</p>
  <select v-model="locale">
    <option value="zh">中文</option>
    <option value="en">English</option>
  </select>
</template>

4.4 表单上下文

typescript
// composables/useFormContext.ts
import { provide, inject, ref, type InjectionKey } from 'vue';

const FormKey: InjectionKey<{
  values: Record<string, any>;
  errors: Record<string, string>;
  validate: () => boolean;
}> = Symbol('form-context');

export function provideForm(initial: Record<string, any>) {
  const values = ref(initial);
  const errors = ref<Record<string, string>>({});

  function validate() {
    let valid = true;
    // 验证逻辑
    return valid;
  }

  provide(FormKey, { values, errors, validate });
}

export function useForm() {
  return inject(FormKey);
}

五、provide/inject vs 其他方式

5.1 对比表

方式适用场景优点缺点
Props父子显式、可追踪多层繁琐
provide/inject跨层级无需逐层传递难追踪来源
Pinia全局状态类型友好、DevTools重型
Event Bus临时事件解耦难调试

5.2 选择建议

typescript
// ✅ 适合 provide/inject:
// 1. 主题、语言、配置等全局性数据
// 2. 用户认证信息
// 3. 表单上下文
// 4. UI 组件库内部

// ❌ 不适合:
// 1. 频繁变化的全局状态(用 Pinia)
// 2. 简单父子通信(用 Props)
// 3. 复杂业务数据(用 Pinia)

六、组合式封装

6.1 封装成 composable

typescript
// composables/useTheme.ts
import { ref, provide, inject, watchEffect, type InjectionKey, type Ref } from 'vue';

type Theme = 'light' | 'dark';

const ThemeKey: InjectionKey<Ref<Theme>> = Symbol('theme');

export function provideTheme(initial: Theme = 'light') {
  const theme = ref(initial);

  // 自动应用到 DOM
  watchEffect(() => {
    document.documentElement.dataset.theme = theme.value;
  });

  provide(ThemeKey, theme);
  return theme;
}

export function useTheme() {
  return inject(ThemeKey, ref<Theme>('light'));
}

6.2 使用

vue
<!-- App.vue -->
<script setup lang="ts">
import { provideTheme } from '@/composables/useTheme';

provideTheme('dark');
</script>
vue
<!-- ThemeToggle.vue -->
<script setup lang="ts">
import { useTheme } from '@/composables/useTheme';

const theme = useTheme();

const toggle = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light';
};
</script>

<template>
  <button @click="toggle">
    切换主题(当前:{{ theme }})
  </button>
</template>

七、provide 响应式注意事项

7.1 传递普通值不是响应式

typescript
// ❌ 不是响应式
const user = { name: 'Tom' };
provide('user', user);

// ✅ 用 ref 包装
const user = ref({ name: 'Tom' });
provide('user', user);

7.2 ref.value 修改同步

vue
<!-- Parent -->
<script setup>
const theme = ref('dark');
provide('theme', theme);

// 修改响应
theme.value = 'light';  // 所有后代自动更新
</script>

7.3 不能被解构

typescript
// ❌ 解构会丢失响应式
const { theme } = inject('theme', ref('light'));

// ✅ 用对象形式
const theme = inject('theme', ref('light'));
// theme.value 访问

八、provide/inject 最佳实践

8.1 总是用 Symbol 类型

typescript
// ✅ 类型安全
const ThemeKey: InjectionKey<Ref<string>> = Symbol('theme');
provide(ThemeKey, value);

// ❌ 字符串容易冲突
provide('theme', value);

8.2 封装在 composable 中

typescript
// ✅ 推荐
provideTheme('dark');
const theme = useTheme();

// ❌ 不推荐
provide('theme', 'dark');
const theme = inject('theme');

8.3 提供默认值

typescript
// ✅ 默认值让组件可独立使用
const theme = inject(ThemeKey, ref('light'));

// ❌ 没默认值,组件强依赖 Provider
const theme = inject(ThemeKey);

九、常见错误

9.1 inject 不存在

typescript
// ❌ 不提供默认值时,可能 undefined
const theme = inject('theme');  // Theme | undefined

// ✅ 提供默认值
const theme = inject('theme', 'light');

9.2 组件库内部不要依赖外部 provide

typescript
// ❌ 第三方组件库要求使用者 provide 数据
const config = inject('my-lib-config');

// ✅ 组件库自带 Provider
<MyLibProvider :config="config">
  <MyLibComponent />
</MyLibProvider>

9.3 忘记 provide 就 inject

vue
<!-- A.vue -->
<template>
  <B />
</template>

<!-- B.vue -->
<script setup>
const theme = inject('theme', 'light');  // 用默认值 'light'
</script>

十、本章小结

要点关键
provide在祖先组件提供数据
inject在任意后代组件接收
InjectionKey类型化 key(Symbol)
响应式必须传 ref/reactive
默认值inject(key, default)
封装封装成 useXxx composable

动手练习

  1. 主题切换:用 provide/inject 实现全局主题切换
  2. 用户认证:实现一个 AuthProvider,提供 user 和 login 方法
  3. 国际化:实现一个简易 i18n,提供 t 函数
  4. 表单上下文:实现一个 FormProvider,提供 values、errors、validate

推荐阅读


下一章第 125 章:自定义 composables

本站基于 VitePress 构建 · 由 Codebook 团队维护