Vue Provide / Inject
Provide / Inject 是 Vue 提供的一种跨级组件通信方式,适合于需要从祖先组件向所有后代组件传递数据的场景。
基本概念
为什么需要 Provide / Inject?
Props 传递需要层层传递,对于多层嵌套的组件非常不便:
┌─────────────────────────────────────────────────────────┐
│ Props vs Provide │
├─────────────────────────────────────────────────────────┤
│ │
│ Props 层层传递: │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ A │───▶│ B │───▶│ C │───▶│ D │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
│ 需定义 props 需定义 props 需定义 props │
│ │
│ Provide/Inject: │
│ ┌─────┐ │
│ │ A │ ──────────────────────────────────────▶ D │
│ └─────┘ provide │
│ inject │
└─────────────────────────────────────────────────────────┘
基本用法
<!-- 祖先组件 -->
<script setup>
import { provide } from 'vue'
const theme = 'dark'
provide('theme', theme)
</script>
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
console.log(theme) // 'dark'
</script>
详细用法
基本类型
// 祖先组件
import { provide } from 'vue'
provide('appName', 'My App')
provide('version', '1.0.0')
provide('isLoggedIn', true)
对象类型
// 祖先组件
import { provide, reactive } from 'vue'
const userInfo = reactive({
name: '张三',
age: 25,
email: '[email protected]'
})
provide('user', userInfo)
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
const user = inject('user')
console.log(user.name) // '张三'
// 修改会响应式更新
user.age = 30
</script>
带默认值的 inject
// 如果没有找到对应的 provide
const theme = inject('theme', 'light')
const config = inject('config', { default: true })
Symbol 作为 key
// 为了避免命名冲突,可以使用 Symbol
// keys.js
export const THEME_KEY = Symbol()
export const USER_KEY = Symbol()
// 祖先组件
import { THEME_KEY, USER_KEY } from './keys'
provide(THEME_KEY, 'dark')
provide(USER_KEY, userInfo)
// 后代组件
import { THEME_KEY, USER_KEY } from './keys'
const theme = inject(THEME_KEY)
const user = inject(USER_KEY)
响应式 Provide
使用 ref 或 reactive
// 祖先组件
import { provide, ref, reactive } from 'vue'
// ref - 保持 .value 的响应式
const count = ref(0)
provide('count', count)
// reactive - 保持对象的响应式
const user = reactive({ name: '张三', age: 25 })
provide('user', user)
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
const count = inject('count')
console.log(count.value) // 0
// 响应式更新
count.value++
</script>
提供计算属性
import { provide, computed } from 'vue'
const user = reactive({ firstName: '张', lastName: '三' })
provide('fullName', computed(() => user.firstName + user.lastName))
实际应用场景
主题系统
// ThemeProvider.vue
<script setup>
import { provide, ref, computed } from 'vue'
const isDark = ref(false)
const theme = computed(() => isDark.value ? 'dark' : 'light')
function toggleTheme() {
isDark.value = !isDark.value
}
provide('theme', theme)
provide('isDark', isDark)
provide('toggleTheme', toggleTheme)
</script>
<template>
<slot />
</template>
<!-- Button.vue -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
</script>
<template>
<button :class="['btn', `btn-${theme}`]">
<slot />
</button>
</template>
<!-- ToggleButton.vue -->
<script setup>
import { inject } from 'vue'
const isDark = inject('isDark')
const toggleTheme = inject('toggleTheme')
</script>
<template>
<button @click="toggleTheme">
当前: {{ isDark ? '深色' : '浅色' }}模式
</button>
</template>
用户认证
// AuthProvider.vue
<script setup>
import { provide, ref, computed } from 'vue'
const user = ref(null)
const token = ref(localStorage.getItem('token') || '')
const isLoggedIn = computed(() => !!token.value)
async function login(credentials) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
const data = await response.json()
user.value = data.user
token.value = data.token
localStorage.setItem('token', data.token)
}
function logout() {
user.value = null
token.value = ''
localStorage.removeItem('token')
}
provide('user', user)
provide('token', token)
provide('isLoggedIn', isLoggedIn)
provide('login', login)
provide('logout', logout)
</script>
<template>
<slot />
</template>
<!-- UserAvatar.vue -->
<script setup>
import { inject } from 'vue'
const user = inject('user')
</script>
<template>
<div v-if="user">
<img :src="user.avatar" :alt="user.name" />
<span>{{ user.name }}</span>
</div>
<div v-else>未登录</div>
</template>
多语言支持
// I18nProvider.vue
<script setup>
import { provide, ref, computed } from 'vue'
const locale = ref('zh-CN')
const messages = {
'zh-CN': {
hello: '你好',
world: '世界',
welcome: '欢迎 {name}'
},
'en-US': {
hello: 'Hello',
world: 'World',
welcome: 'Welcome {name}'
}
}
const t = (key, params = {}) => {
let message = messages[locale.value]?.[key] || key
Object.entries(params).forEach(([k, v]) => {
message = message.replace(`{${k}}`, v)
})
return message
}
function setLocale(newLocale) {
locale.value = newLocale
}
provide('locale', locale)
provide('t', t)
provide('setLocale', setLocale)
</script>
<template>
<slot />
</template>
<!-- HelloMessage.vue -->
<script setup>
import { inject } from 'vue'
const t = inject('t')
</script>
<template>
<p>{{ t('hello') }} {{ t('world') }}!</p>
<p>{{ t('welcome', { name: '张三' }) }}</p>
</template>
组合式 API 封装
封装 useLocale
// composables/useLocale.js
import { inject, provide, computed } from 'vue'
const LOCALE_KEY = Symbol()
export function provideLocale(locale, messages) {
const currentLocale = ref(locale)
const t = (key, params = {}) => {
const msg = messages[currentLocale.value]?.[key] || key
return msg.replace(/\{(\w+)\}/g, (_, k) => params[k] || `{${k}}`)
}
provide(LOCALE_KEY, { locale: currentLocale, t })
return {
locale: currentLocale,
t
}
}
export function useLocale() {
const context = inject(LOCALE_KEY)
if (!context) {
throw new Error('useLocale() 必须在 LocaleProvider 内使用')
}
return context
}
使用封装
<!-- App.vue -->
<script setup>
import { provideLocale } from './composables/useLocale'
const messages = {
'zh-CN': { hello: '你好' },
'en-US': { hello: 'Hello' }
}
provideLocale('zh-CN', messages)
</script>
<template>
<ChildComponent />
</template>
<!-- ChildComponent.vue -->
<script setup>
import { useLocale } from './composables/useLocale'
const { locale, t } = useLocale()
</script>
<template>
<p>{{ t('hello') }}</p>
</template>
与 Pinia 结合
在 Pinia Store 中使用 Provide
// stores/cart.js
import { defineStore } from 'pinia'
import { provide } from 'vue'
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const count = computed(() => items.value.length)
function addItem(product) {
items.value.push(product)
}
function removeItem(id) {
const index = items.value.findIndex(item => item.id === id)
if (index > -1) {
items.value.splice(index, 1)
}
}
// 在 setup store 中直接 provide
provide('cart', { items, count, addItem, removeItem })
return { items, count, addItem, removeItem }
})
<!-- CartProvider.vue -->
<script setup>
import { useCartStore } from '@/stores/cart'
const cartStore = useCartStore()
</script>
<template>
<slot />
</template>
注意事项
注入来源
// 后代组件可以检查注入的来源
import { inject } from 'vue'
const childMessage = inject('message')
const parentMessage = inject('message', null, true) // 第三个参数表示拒绝注入
调试
// 调试注入
app.config.globalProperties.$debug = {
injected: inject
}
小结
本章我们详细学习了 Vue Provide / Inject 的完整内容:
- 基本概念:跨级组件通信
- 基本用法:provide 和 inject
- 响应式 provide:ref 和 reactive
- 实际应用:主题系统、用户认证、多语言
- 组合式 API 封装:可复用的 inject
- 与 Pinia 结合:状态管理集成
练习
- 创建一个 Toast 通知系统
- 实现一个配置管理组件
- 创建可复用的 useAuth Hook
准备好继续学习了吗?