第 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 |
动手练习
- 主题切换:用 provide/inject 实现全局主题切换
- 用户认证:实现一个 AuthProvider,提供 user 和 login 方法
- 国际化:实现一个简易 i18n,提供 t 函数
- 表单上下文:实现一个 FormProvider,提供 values、errors、validate
推荐阅读
- 📖 Vue 3 provide/inject — 官方文档
- 🌐 VueUse Injection Pattern — 工具库
- 📖 Vue 3 组合式 Provider 模式 — 模式
下一章:第 125 章:自定义 composables →