Vue3 依赖注入深入:provide/inject 的架构设计与最佳实践
Vue3 依赖注入深入:provide/inject 的架构设计与最佳实践
在 Vue 组件树中,数据传递是一个核心问题。props 是最基础的方式,但当组件层级变深时,中间组件被迫传递它们并不关心的 props——这就是臭名昭著的 Props Drilling(属性穿透) 问题。Vue3 的 provide/inject 提供了一种优雅的跨层级通信机制,但很多开发者只停留在基础用法层面。
本文将深入 provide/inject 的实现原理,探讨 Symbol key、类型安全、响应式注入、以及在大型项目中的架构设计模式。
一、Props Drilling 的困境
1.1 一个典型的场景
假设我们有一个主题系统,主题配置需要从 App 根组件传递到深层嵌套的 Button 组件:
App (theme)└── Layout └── Page └── Section └── Card └── CardBody └── Button (需要 theme)用 props 传递:
<template> <Layout :theme="theme" /></template>
<!-- Layout.vue --><template> <Page :theme="theme" /></template><script setup>defineProps(['theme']) // Layout 自己不用 theme</script>
<!-- Page.vue --><template> <Section :theme="theme" /></template><script setup>defineProps(['theme']) // Page 也不用 theme</script>
<!-- ... 每一层都要透传 ... -->
<!-- Button.vue --><template> <button :class="theme.buttonClass">{{ label }}</button></template><script setup>defineProps(['theme', 'label']) // 终于用到了 theme</script>这种方式的问题:
- 中间组件污染:Layout、Page、Section 等组件被迫声明和传递
themeprop - 维护困难:新增一个共享状态需要修改所有中间组件
- 耦合度高:重构组件层级时容易遗漏传递
- 类型冗余:每个中间组件都要定义 prop 类型
1.2 provide/inject 的解决方案
<script setup>import { provide, reactive } from 'vue'
const theme = reactive({ primary: '#1976D2', secondary: '#424242', buttonClass: 'btn-primary'})
provide('theme', theme)</script>
<!-- Button.vue —— 直接注入,无需中间组件传递 --><script setup>import { inject } from 'vue'
const theme = inject('theme')</script>
<template> <button :class="theme.buttonClass"> <slot /> </button></template>中间的 Layout、Page、Section、Card 等组件完全不需要感知 theme 的存在。
二、provide/inject 的实现原理
2.1 原型链机制
Vue3 的 provide/inject 底层使用了 JavaScript 原型链:
// packages/runtime-core/src/apiInject.ts(简化)
export function provide<T>(key: InjectionKey<T> | string, value: T) { let provides = currentInstance!.provides
// 如果当前组件的 provides 和父组件的相同(初始状态) // 则创建一个以父组件 provides 为原型的新对象 const parentProvides = currentInstance!.parent?.provides
if (parentProvides === provides) { provides = currentInstance!.provides = Object.create(parentProvides) }
provides[key as string] = value}
export function inject<T>( key: InjectionKey<T> | string, defaultValue?: T, treatDefaultAsFactory = false): T | undefined { const instance = currentInstance
if (instance) { // 从当前实例的 provides 链上查找 const provides = instance.parent?.provides
if (provides && (key as string) in provides) { return provides[key as string] } else if (arguments.length > 1) { // 有默认值 return treatDefaultAsFactory && typeof defaultValue === 'function' ? defaultValue() : defaultValue } else { // 没有找到且没有默认值 console.warn(`injection "${String(key)}" not found.`) } }}2.2 原型链的妙用
// 组件层级:App -> Parent -> Child
// App 的 providesApp.provides = { theme: 'dark', locale: 'zh-CN' }
// Parent 调用 provide('appName', 'MyApp')// 创建新对象,以 App.provides 为原型Parent.provides = Object.create(App.provides)Parent.provides.appName = 'MyApp'// Parent.provides: { appName: 'MyApp' }// Parent.provides.__proto__: { theme: 'dark', locale: 'zh-CN' }
// Child 调用 inject('theme')// 在 Parent.provides 中没找到 theme// 沿着原型链找到 App.provides 中的 theme// 返回 'dark'
// Child 调用 inject('appName')// 在 Parent.provides 中找到了// 返回 'MyApp'这个设计非常巧妙:
- 就近原则:子组件可以覆盖祖先的 provide 值
- 高效查找:利用 JS 引擎优化的原型链查找
- 内存友好:没有 provide 的组件不会创建新对象
2.3 覆盖机制
<script setup>provide('color', 'red')</script>
<!-- Parent.vue --><script setup>// 覆盖祖先的 provideprovide('color', 'blue')</script>
<!-- Child.vue --><script setup>const color = inject('color') // 'blue'(就近原则)</script>三、Symbol Key 与类型安全
3.1 字符串 Key 的问题
使用字符串作为 key 存在两个问题:
- 命名冲突:不同的库或模块可能使用相同的字符串 key
- 类型不安全:inject 返回
unknown,需要手动断言
// ❌ 字符串 key —— 不安全provide('user', userObj)
// inject 返回 unknownconst user = inject('user') // unknown 类型// 需要手动断言const user = inject('user') as User // 不安全3.2 InjectionKey 类型
Vue3 提供了 InjectionKey<T> 类型,配合 Symbol 使用:
import { type InjectionKey } from 'vue'
// 定义类型安全的注入 Keyexport const UserKey: InjectionKey<User> = Symbol('user')export const ThemeKey: InjectionKey<ThemeConfig> = Symbol('theme')export const ApiClientKey: InjectionKey<ApiClient> = Symbol('apiClient')import { provide, inject } from 'vue'import { UserKey } from './injection-keys'
// provide 时有类型检查provide(UserKey, { id: 1, name: 'Alice' }) // ✅provide(UserKey, 'not a user') // ❌ 类型错误
// inject 时自动推导类型const user = inject(UserKey) // User | undefinedconst user = inject(UserKey, { id: 0, name: 'Guest' }) // User(有默认值时不会是 undefined)3.3 统一管理 Injection Keys
在大型项目中,建议统一管理所有的 Injection Key:
import type { InjectionKey, Ref } from 'vue'
// ===== 类型定义 =====export interface ThemeConfig { mode: 'light' | 'dark' primary: string secondary: string fontFamily: string}
export interface AuthContext { user: Ref<User | null> isAuthenticated: Ref<boolean> login: (credentials: Credentials) => Promise<void> logout: () => Promise<void> token: Ref<string | null>}
export interface I18nContext { locale: Ref<string> t: (key: string, params?: Record<string, any>) => string setLocale: (locale: string) => Promise<void> availableLocales: string[]}
export interface ToastContext { success: (message: string) => void error: (message: string) => void warning: (message: string) => void info: (message: string) => void}
// ===== Injection Keys =====export const ThemeKey: InjectionKey<ThemeConfig> = Symbol('theme')export const AuthKey: InjectionKey<AuthContext> = Symbol('auth')export const I18nKey: InjectionKey<I18nContext> = Symbol('i18n')export const ToastKey: InjectionKey<ToastContext> = Symbol('toast')export const ApiKey: InjectionKey<AxiosInstance> = Symbol('api')四、响应式注入
4.1 provide 响应式数据
provide 注入的值不会自动变成响应式的。你需要显式地提供响应式数据:
<script setup>import { ref, reactive, provide, readonly } from 'vue'
// ✅ 提供 refconst count = ref(0)provide('count', count) // 子组件可以读取到最新值
// ✅ 提供 reactiveconst state = reactive({ name: 'Alice', age: 25 })provide('state', state) // 子组件可以读取到最新值
// ⚠️ 提供普通值 —— 不具备响应性provide('staticValue', 42) // 子组件始终获取到 42
// ✅ 推荐:提供 readonly 版本provide('readonlyCount', readonly(count))provide('readonlyState', readonly(state))</script>4.2 单向数据流原则
为了维护数据流的清晰性,注入的数据应该由提供者修改,而非消费者:
// ❌ 不推荐:消费者直接修改注入的数据const count = inject(CountKey)!count.value++ // 破坏了数据流向
// ✅ 推荐:提供者提供修改方法// Providerconst count = ref(0)function increment() { count.value++}provide(CountKey, { count: readonly(count), increment})
// Consumerconst { count, increment } = inject(CountKey)!increment() // 通过提供者的方法修改4.3 完整的响应式注入模式
import { ref, reactive, readonly, provide, inject, computed, watch } from 'vue'import type { InjectionKey } from 'vue'
interface Theme { mode: 'light' | 'dark' colors: { primary: string secondary: string background: string text: string border: string }}
interface ThemeContext { theme: Readonly<Theme> isDark: Readonly<Ref<boolean>> toggleTheme: () => void setTheme: (mode: 'light' | 'dark') => void setPrimary: (color: string) => void}
const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')
const lightColors = { primary: '#1976D2', secondary: '#424242', background: '#ffffff', text: '#333333', border: '#e0e0e0'}
const darkColors = { primary: '#90CAF9', secondary: '#B0BEC5', background: '#121212', text: '#E0E0E0', border: '#333333'}
export function provideTheme(initialMode: 'light' | 'dark' = 'light') { const theme = reactive<Theme>({ mode: initialMode, colors: initialMode === 'light' ? { ...lightColors } : { ...darkColors } })
const isDark = computed(() => theme.mode === 'dark')
function setTheme(mode: 'light' | 'dark') { theme.mode = mode Object.assign(theme.colors, mode === 'light' ? lightColors : darkColors) }
function toggleTheme() { setTheme(theme.mode === 'light' ? 'dark' : 'light') }
function setPrimary(color: string) { theme.colors.primary = color }
// 同步到 CSS 变量 watch(() => theme.colors, (colors) => { const root = document.documentElement Object.entries(colors).forEach(([key, value]) => { root.style.setProperty(`--color-${key}`, value) }) }, { immediate: true, deep: true })
// 同步 dark class watch(isDark, (dark) => { document.documentElement.classList.toggle('dark', dark) }, { immediate: true })
const context: ThemeContext = { theme: readonly(theme) as Readonly<Theme>, isDark: readonly(isDark), toggleTheme, setTheme, setPrimary }
provide(ThemeKey, context)
return context}
export function useTheme(): ThemeContext { const context = inject(ThemeKey) if (!context) { throw new Error( 'useTheme() is called without provideTheme(). ' + 'Did you forget to call provideTheme() in a parent component?' ) } return context}使用:
<script setup>import { provideTheme } from '@/composables/useTheme'
const { isDark, toggleTheme } = provideTheme('light')</script>
<template> <div :class="{ dark: isDark }"> <button @click="toggleTheme"> {{ isDark ? '🌙' : '☀️' }} 切换主题 </button> <router-view /> </div></template>
<!-- 任何子组件 --><script setup>import { useTheme } from '@/composables/useTheme'
const { theme, isDark } = useTheme()</script>
<template> <div :style="{ color: theme.colors.text, backgroundColor: theme.colors.background }"> 当前主题: {{ isDark ? '暗色' : '亮色' }} </div></template>五、高级架构模式
5.1 插件式注入
利用 Vue 的 app.provide 在应用级别注入:
import type { App } from 'vue'import axios from 'axios'import { ApiKey } from '@/injection-keys'
export function createApiPlugin(baseURL: string) { return { install(app: App) { const apiClient = axios.create({ baseURL, timeout: 10000, headers: { 'Content-Type': 'application/json' } })
// 请求拦截器 apiClient.interceptors.request.use((config) => { const token = localStorage.getItem('auth_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config })
// 响应拦截器 apiClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { // 处理认证过期 window.location.href = '/login' } return Promise.reject(error) } )
// 应用级别的 provide app.provide(ApiKey, apiClient) } }}import { createApp } from 'vue'import App from './App.vue'import { createApiPlugin } from './plugins/api'
const app = createApp(App)app.use(createApiPlugin('https://api.example.com'))app.mount('#app')<!-- 任何组件中 --><script setup>import { inject } from 'vue'import { ApiKey } from '@/injection-keys'
const api = inject(ApiKey)!
const users = ref([])onMounted(async () => { const { data } = await api.get('/users') users.value = data})</script>5.2 服务定位器模式
import { type InjectionKey, provide, inject } from 'vue'
// 通用服务容器const ServiceContainerKey: InjectionKey<Map<symbol, any>> = Symbol('ServiceContainer')
export function provideServiceContainer() { const container = new Map<symbol, any>() provide(ServiceContainerKey, container) return container}
export function registerService<T>(key: InjectionKey<T>, factory: () => T) { const container = inject(ServiceContainerKey) if (!container) { throw new Error('ServiceContainer not provided') }
// 懒加载:只在第一次获取时创建 let instance: T | null = null const proxy = new Proxy({} as any, { get(_, prop) { if (!instance) instance = factory() return (instance as any)[prop] } })
container.set(key as unknown as symbol, { factory, instance: null }) provide(key, proxy)}
export function getService<T>(key: InjectionKey<T>): T { const service = inject(key) if (!service) { throw new Error(`Service not registered: ${String(key)}`) } return service}5.3 组合式 Store 模式
provide/inject 可以用来实现一个轻量级的状态管理方案,无需引入 Pinia 或 Vuex:
import { ref, computed, readonly, provide, inject, type InjectionKey } from 'vue'
interface CartItem { id: string name: string price: number quantity: number}
interface CartStore { items: Readonly<Ref<CartItem[]>> totalItems: Readonly<Ref<number>> totalPrice: Readonly<Ref<number>> addItem: (item: Omit<CartItem, 'quantity'>) => void removeItem: (id: string) => void updateQuantity: (id: string, quantity: number) => void clearCart: () => void}
const CartStoreKey: InjectionKey<CartStore> = Symbol('CartStore')
export function provideCartStore(): CartStore { const items = ref<CartItem[]>([])
const totalItems = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0) )
const totalPrice = computed(() => items.value.reduce((sum, item) => sum + item.price * item.quantity, 0) )
function addItem(newItem: Omit<CartItem, 'quantity'>) { const existing = items.value.find(item => item.id === newItem.id) if (existing) { existing.quantity++ } else { items.value.push({ ...newItem, quantity: 1 }) } }
function removeItem(id: string) { const index = items.value.findIndex(item => item.id === id) if (index !== -1) { items.value.splice(index, 1) } }
function updateQuantity(id: string, quantity: number) { const item = items.value.find(item => item.id === id) if (item) { if (quantity <= 0) { removeItem(id) } else { item.quantity = quantity } } }
function clearCart() { items.value = [] }
const store: CartStore = { items: readonly(items), totalItems, totalPrice, addItem, removeItem, updateQuantity, clearCart }
provide(CartStoreKey, store) return store}
export function useCartStore(): CartStore { const store = inject(CartStoreKey) if (!store) { throw new Error('useCartStore() requires provideCartStore() in a parent component') } return store}使用:
<script setup>import { provideCartStore } from '@/stores/useCartStore'provideCartStore()</script>
<!-- ProductCard.vue --><script setup>import { useCartStore } from '@/stores/useCartStore'
const { addItem } = useCartStore()
const props = defineProps<{ product: { id: string; name: string; price: number }}>()
function handleAddToCart() { addItem({ id: props.product.id, name: props.product.name, price: props.product.price })}</script>
<template> <div class="product-card"> <h3>{{ product.name }}</h3> <p>¥{{ product.price }}</p> <button @click="handleAddToCart">加入购物车</button> </div></template>
<!-- CartWidget.vue --><script setup>import { useCartStore } from '@/stores/useCartStore'
const { items, totalItems, totalPrice, removeItem, updateQuantity } = useCartStore()</script>
<template> <div class="cart-widget"> <h3>购物车 ({{ totalItems }})</h3> <div v-for="item in items" :key="item.id" class="cart-item"> <span>{{ item.name }}</span> <input type="number" :value="item.quantity" @input="updateQuantity(item.id, +$event.target.value)" min="0" /> <span>¥{{ item.price * item.quantity }}</span> <button @click="removeItem(item.id)">✕</button> </div> <div class="cart-total"> 总计: ¥{{ totalPrice.toFixed(2) }} </div> </div></template>六、provide/inject vs 其他方案对比
6.1 与 Props Drilling 对比
| 特性 | Props Drilling | provide/inject |
|---|---|---|
| 数据流向 | 显式、逐层 | 隐式、跨层 |
| 中间组件 | 需要参与传递 | 完全不感知 |
| 可追溯性 | 容易追踪 | 需要约定 |
| 重构难度 | 高(需改每层) | 低 |
| 类型安全 | 天然支持 | 需要 InjectionKey |
| 适用场景 | 1-2 层 | 3+ 层 |
6.2 与全局状态(Pinia)对比
// Pinia Store —— 全局单例export const useUserStore = defineStore('user', () => { const user = ref<User | null>(null) return { user }})
// provide/inject Store —— 组件树作用域export function provideUserStore() { const user = ref<User | null>(null) provide(UserStoreKey, { user: readonly(user) })}| 特性 | Pinia | provide/inject |
|---|---|---|
| 作用域 | 全局 | 组件树 |
| 持久化 | 插件支持 | 手动实现 |
| DevTools | 完整支持 | 有限 |
| SSR | 内置支持 | 需要注意 |
| 复杂度 | 适合大型项目 | 适合中小型场景 |
| 测试 | 需要 mock store | 直接 provide mock |
6.3 何时选择 provide/inject
适合用 provide/inject 的场景:
- 主题/国际化等”环境配置”的传递
- 表单组件库中表单与表单项的通信
- 布局组件与子组件的协调
- 有限范围内的状态共享
- 组件库内部的跨层通信
适合用 Pinia 的场景:
- 全局用户认证状态
- 购物车等需要跨页面持久化的状态
- 需要 DevTools 调试的复杂状态
- 多个不相关的组件树需要共享的状态
七、常见陷阱与注意事项
7.1 避免循环依赖
// ❌ 循环依赖// ComponentA provide → inject from ComponentB// ComponentB provide → inject from ComponentA// 这会导致其中一个 inject 得到 undefined7.2 注意 inject 的调用时机
// ❌ 在 setup 外调用function someHelper() { const theme = inject(ThemeKey) // 无法工作!}
// ✅ 在 setup 中调用,传递给 helperfunction someHelper(theme: ThemeConfig) { // 使用 theme}
// setup 中const theme = inject(ThemeKey)!someHelper(theme)inject 必须在 setup() 或 <script setup> 的同步执行阶段调用,因为它依赖 currentInstance 来确定组件上下文。
7.3 提供有意义的默认值
// ✅ 提供安全的默认值const theme = inject(ThemeKey, { mode: 'light', colors: { primary: '#333', background: '#fff', text: '#000' }} as ThemeConfig)
// ✅ 使用工厂函数避免共享引用const state = inject(StateKey, () => ({ items: [], loading: false}), true) // 第三个参数 true 表示默认值是工厂函数7.4 SSR 注意事项
// 在 SSR 环境中,每个请求应该有独立的 provide 上下文// ❌ 使用模块级别的单例const globalState = reactive({ user: null })
// ✅ 在 createApp 时创建新实例export function createApp() { const app = createSSRApp(App) const state = reactive({ user: null }) app.provide(StateKey, state) return { app, state }}八、实战:表单组件库中的 provide/inject
表单组件是 provide/inject 的经典应用场景。让我们实现一个类型安全的表单系统:
import type { InjectionKey, Ref } from 'vue'
interface FormContext { model: Record<string, any> rules: Record<string, ValidationRule[]> addField: (field: FormFieldContext) => void removeField: (field: FormFieldContext) => void validate: () => Promise<boolean> resetFields: () => void}
interface FormFieldContext { prop: string validate: () => Promise<string | null> resetField: () => void clearValidate: () => void}
interface ValidationRule { required?: boolean message?: string min?: number max?: number pattern?: RegExp validator?: (value: any) => boolean | string | Promise<boolean | string>}
export const FormContextKey: InjectionKey<FormContext> = Symbol('FormContext')export const FormFieldContextKey: InjectionKey<FormFieldContext> = Symbol('FormFieldContext')<script setup lang="ts">import { provide, reactive, toRaw } from 'vue'import { FormContextKey, type FormFieldContext, type ValidationRule } from './types'
const props = defineProps<{ model: Record<string, any> rules?: Record<string, ValidationRule[]>}>()
const emit = defineEmits<{ submit: [values: Record<string, any>] 'validation-error': [errors: Record<string, string>]}>()
const fields: FormFieldContext[] = []
function addField(field: FormFieldContext) { fields.push(field)}
function removeField(field: FormFieldContext) { const index = fields.indexOf(field) if (index !== -1) fields.splice(index, 1)}
async function validate(): Promise<boolean> { const results = await Promise.all( fields.map(field => field.validate()) ) const errors: Record<string, string> = {} results.forEach((error, index) => { if (error) { errors[fields[index].prop] = error } }) if (Object.keys(errors).length > 0) { emit('validation-error', errors) return false } return true}
function resetFields() { fields.forEach(field => field.resetField())}
// 向子组件提供表单上下文provide(FormContextKey, { model: props.model, rules: props.rules || {}, addField, removeField, validate, resetFields})
async function handleSubmit() { const valid = await validate() if (valid) { emit('submit', toRaw(props.model)) }}
defineExpose({ validate, resetFields })</script>
<template> <form @submit.prevent="handleSubmit"> <slot /> </form></template><script setup lang="ts">import { inject, ref, onMounted, onUnmounted, provide, computed, watch } from 'vue'import { FormContextKey, FormFieldContextKey } from './types'
const props = defineProps<{ prop: string label?: string}>()
const formContext = inject(FormContextKey)if (!formContext) { throw new Error('FormItem must be used inside Form component')}
const errorMessage = ref<string | null>(null)const isValidating = ref(false)let initialValue: any
onMounted(() => { initialValue = structuredClone(formContext.model[props.prop])})
async function validate(): Promise<string | null> { const rules = formContext.rules[props.prop] if (!rules || rules.length === 0) return null
isValidating.value = true const value = formContext.model[props.prop]
for (const rule of rules) { if (rule.required && (value === '' || value === null || value === undefined)) { errorMessage.value = rule.message || `${props.label || props.prop} 是必填项` isValidating.value = false return errorMessage.value } if (rule.min !== undefined && typeof value === 'string' && value.length < rule.min) { errorMessage.value = rule.message || `最少 ${rule.min} 个字符` isValidating.value = false return errorMessage.value } if (rule.pattern && !rule.pattern.test(value)) { errorMessage.value = rule.message || '格式不正确' isValidating.value = false return errorMessage.value } if (rule.validator) { const result = await rule.validator(value) if (result !== true) { errorMessage.value = typeof result === 'string' ? result : (rule.message || '验证失败') isValidating.value = false return errorMessage.value } } }
errorMessage.value = null isValidating.value = false return null}
function resetField() { formContext.model[props.prop] = structuredClone(initialValue) errorMessage.value = null}
function clearValidate() { errorMessage.value = null}
const fieldContext = { prop: props.prop, validate, resetField, clearValidate }
// 注册到 FormformContext.addField(fieldContext)onUnmounted(() => formContext.removeField(fieldContext))
// 也可以提供给更深层的组件provide(FormFieldContextKey, fieldContext)</script>
<template> <div class="form-item" :class="{ 'has-error': errorMessage }"> <label v-if="label" class="form-item-label">{{ label }}</label> <div class="form-item-content"> <slot /> </div> <transition name="fade"> <p v-if="errorMessage" class="form-item-error">{{ errorMessage }}</p> </transition> </div></template>使用:
<script setup>import { reactive } from 'vue'import Form from './Form.vue'import FormItem from './FormItem.vue'
const formRef = ref()const model = reactive({ username: '', email: '', password: ''})
const rules = { username: [ { required: true, message: '请输入用户名' }, { min: 3, message: '用户名至少 3 个字符' } ], email: [ { required: true, message: '请输入邮箱' }, { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '邮箱格式不正确' } ], password: [ { required: true, message: '请输入密码' }, { min: 6, message: '密码至少 6 位' } ]}
function handleSubmit(values) { console.log('提交成功:', values)}</script>
<template> <Form ref="formRef" :model="model" :rules="rules" @submit="handleSubmit"> <FormItem prop="username" label="用户名"> <input v-model="model.username" /> </FormItem> <FormItem prop="email" label="邮箱"> <input v-model="model.email" type="email" /> </FormItem> <FormItem prop="password" label="密码"> <input v-model="model.password" type="password" /> </FormItem> <button type="submit">提交</button> <button type="button" @click="formRef?.resetFields()">重置</button> </Form></template>九、总结
Vue3 的 provide/inject 是一个被低估的强大工具。正确使用它可以:
- 消除 Props Drilling:跨层级传递数据,中间组件无需感知
- 实现依赖倒置:消费者不关心数据来源,只关心接口
- 构建组件库:表单、布局等复合组件的内部通信
- 轻量级状态管理:在不需要 Pinia 的场景下共享状态
- 插件系统:通过
app.provide实现应用级服务注入
关键实践要点:
- 始终使用 Symbol + InjectionKey 保证类型安全和避免命名冲突
- 提供 readonly 数据 + 修改方法,维护单向数据流
- 封装为 Composable(
provideXxx+useXxx模式),提升开发体验 - 提供有意义的错误信息,当 inject 失败时抛出清晰的异常
- 注意 SSR 环境,避免全局单例污染
掌握这些模式,你就能在 Vue3 项目中构建出清晰、灵活、可维护的组件架构。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!