还在纠结 provide/inject 还是 Pinia?看完这篇你就不纠结了!

510 阅读3分钟

在 Vue 项目开发中,我们常常面临这样的选择:

“这个数据我应该放在 Pinia 里,还是用 provide/inject 传一下就好?”

选错方式不仅代码臃肿,还容易埋下维护隐患。
今天我们用一句话 + 一张表 + 两个真实例子,彻底搞清楚它们的区别与边界!

一句话区分 provide/inject 和 Pinia

👉 把 provide/inject 当成“组件内部的共享通道”,把 Pinia 当成“全局状态中心”。

关键在两个词:作用域 + 数据形态

对比维度provide/injectPinia
作用范围组件树内部(祖先 ➜ 子孙)全局(任何组件都能用)
数据适用形态通常是静态配置类数据(样式、主题、上下文)动态状态数据(用户信息、异步结果)
是否响应式默认不是响应式,需手动用 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 当成“全局状态中心”。

它们并不是对立关系,而是 组合拳

  • AppProviderprovide/inject 注入布局配置;
  • useUserStore 用 Pinia 管理用户登录态;
  • usePermissionStore 管权限点;
  • provide('modalContext') 给弹窗体系传递上下文。

总结

原则上:provide/inject 当成“组件内部的共享通道”,把 Pinia 当成“全局状态中心”。

但在实践中我更倾向这样理解:优先用 Pinia 管状态,结构清晰、响应式天然、扩展性强,适合绝大多数真实业务场景,是我推荐的默认选型

希望这篇文档对你有所帮助,有所借鉴。