在 Vue 项目开发中,我们常常面临这样的选择:
“这个数据我应该放在 Pinia 里,还是用 provide/inject 传一下就好?”
选错方式不仅代码臃肿,还容易埋下维护隐患。
今天我们用一句话 + 一张表 + 两个真实例子,彻底搞清楚它们的区别与边界!
一句话区分 provide/inject 和 Pinia
👉 把
provide/inject当成“组件内部的共享通道”,把 Pinia 当成“全局状态中心”。
关键在两个词:作用域 + 数据形态
| 对比维度 | provide/inject | Pinia |
|---|---|---|
| 作用范围 | 组件树内部(祖先 ➜ 子孙) | 全局(任何组件都能用) |
| 数据适用形态 | 通常是静态配置类数据(样式、主题、上下文) | 动态状态数据(用户信息、异步结果) |
| 是否响应式 | 默认不是响应式,需手动用 reactive 处理 | 自动响应式 |
| 数据修改 | 没有状态修改规范,子组件可能随便改 | 有明确 state + action,更可控 |
| 技术复杂度 | 内建,无依赖,轻量 | 引入第三方库,架构复杂度上升 |
举个真实例子:你会怎么做?
场景一:你只需要下发一个主题配置
比如:
// layout.config.ts
export default {
theme: 'dark',
menu: 'left'
}
这就是典型的静态数据、不需要跨模块访问,也不怎么改动 —— 用 provide/inject 完美。不需要搞全局 store、不用担心命名空间、也不用维护 action,一步到位!
场景二:你要管理用户登录态,切换、更新、持久化
你需要:
- 登录/登出接口
- 跨模块访问用户信息
- 页面刷新后持久化用户状态
- 监听用户变动,联动多个 UI 模块
这时你用 provide/inject 基本就是硬写,全靠自己手动处理响应式、全局访问、缓存……
这时 Pinia 才是正确解法,结构清晰、支持响应式、调试友好,一整套生态都在。
所以结论是:
| 如果你遇到的是: | 推荐方案 |
|---|---|
| 页面主题、布局设置、上下文环境 | provide/inject ✅ |
| 用户信息、权限控制、购物车、异步加载结果等全局状态 | Pinia ✅ |
| 插件系统、自定义组件内部通信 | provide/inject ✅ |
| 跨页面共享数据、响应式联动、大量状态管理 | Pinia ✅ |
封装一个全局配置提供器 AppProvider
我们封装一个专用的配置注入组件 AppProvider,将配置传递到整个组件树中:
AppProvider.tsx
import { defineComponent, provide, PropType, reactive } from 'vue'
import config, { ILayoutConfig, layoutConfigKey } from './layout.config'
export default defineComponent({
name: 'AppProvider',
props: {
config: {
type: Object as PropType<ILayoutConfig>,
default: () => config
}
},
setup(props, { slots }) {
const reactiveConfig = reactive(props.config)
provide(layoutConfigKey, reactiveConfig)
return () => slots.default?.()
}
})
layout.config.ts
import { InjectionKey } from 'vue'
export interface ILayoutConfig {
theme: 'dark' | 'light'
showLogo: boolean
}
export const layoutConfigKey: InjectionKey<ILayoutConfig> = Symbol('layout')
export default {
theme: 'dark',
showLogo: true
}
用法示例:
<!-- App.vue -->
<template>
<AppProvider>
<router-view />
</AppProvider>
</template>
// 子组件中
const layoutConfig = inject(layoutConfigKey)
console.log(layoutConfig?.theme)
配置动态修改怎么办?
提供数据(祖先组件):
import { provide, reactive } from 'vue'
const themeConfig = reactive({ theme: 'dark' })
provide('themeConfig', themeConfig)
接收数据(子孙组件):
import { inject } from 'vue'
const themeConfig = inject('themeConfig')
console.log(themeConfig.theme) // 'dark'
但是注意,字符串键会存在命名冲突风险,推荐使用 Symbol 替代:
// config.ts
export const themeKey = Symbol('theme')
// 祖先组件
provide(themeKey, themeConfig)
// 后代组件
const config = inject(themeKey)
useUserStore
// stores/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
name: '',
token: '',
isLoggedIn: false
}),
actions: {
login(name: string, token: string) {
this.name = name
this.token = token
this.isLoggedIn = true
localStorage.setItem('token', token)
},
logout() {
this.name = ''
this.token = ''
this.isLoggedIn = false
localStorage.removeItem('token')
}
}
})
使用方式
// 在组件中使用
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 登录
userStore.login('Jamie', 'mock-token-123')
// 登出
userStore.logout()
// 页面刷新后恢复登录
userStore.restore()
一个设计建议
👉 把
provide/inject当成“组件内部的共享通道”,把 Pinia 当成“全局状态中心”。
它们并不是对立关系,而是 组合拳:
AppProvider用provide/inject注入布局配置;useUserStore用 Pinia 管理用户登录态;usePermissionStore管权限点;provide('modalContext')给弹窗体系传递上下文。
总结
原则上:provide/inject 当成“组件内部的共享通道”,把 Pinia 当成“全局状态中心”。
但在实践中我更倾向这样理解:优先用 Pinia 管状态,结构清晰、响应式天然、扩展性强,适合绝大多数真实业务场景,是我推荐的默认选型。
希望这篇文档对你有所帮助,有所借鉴。