跳到主要内容

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 的完整内容:

  1. 基本概念:跨级组件通信
  2. 基本用法:provide 和 inject
  3. 响应式 provide:ref 和 reactive
  4. 实际应用:主题系统、用户认证、多语言
  5. 组合式 API 封装:可复用的 inject
  6. 与 Pinia 结合:状态管理集成

练习

  1. 创建一个 Toast 通知系统
  2. 实现一个配置管理组件
  3. 创建可复用的 useAuth Hook

准备好继续学习了吗?