现在位置: 首页 > Vue3 教程 > 正文

Pinia — 全局状态管理

本章你将学会用 Pinia 管理跨组件、跨页面的共享数据,并实现「收藏文章」功能。


为什么需要全局状态管理?

到目前为止,数据在组件之间通过 props 和 emit 传递。

这对于父子关系简单的场景够用,但遇到以下情况就变得吃力:

  • 收藏状态需要在首页和详情页之间同步
  • 用户登录状态需要所有页面共享
  • 购物车数据需要在导航栏、商品页、结算页之间传递

props 逐层传递 的痛点:如果 A 组件的数据要传到 D 组件,需要经过 B、C 两层中转,而这些中间组件自己并不需要这些数据。

Pinia 是 Vue3 官方推荐的状态管理库,它提供了一个全局的「数据中心」,任何组件都可以直接读取和修改,不需要层层传递。


安装与注册 Pinia

如果在创建项目时已选择 Pinia,跳过安装步骤。

否则:

$ npm install pinia

在 main.js 中注册:

实例

// 文件路径:src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'

const app = createApp(App)

// 创建 Pinia 实例并注册
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.mount('#app')

defineStore — 定义仓库

Pinia 的核心是 Store(仓库),每个 Store 管理一块独立的状态。

它包含三个部分:

部分作用类比
state存储数据组件的 data / ref
getters派生计算数据组件的 computed
actions修改数据的方法组件的 methods / function

实例

// 文件路径:src/stores/useFavoriteStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// defineStore('仓库名', () => { ... })
// 第二个参数用函数写法,与组合式 API 风格一致
export const useFavoriteStore = defineStore('favorite', () => {
  // ====== state:用 ref 定义状态 ======
  // 用一个数组存储收藏的文章 ID
  const favoriteIds = ref([])

  // 从 localStorage 恢复收藏列表
  const saved = localStorage.getItem('blog-favorites')
  if (saved) {
    favoriteIds.value = JSON.parse(saved)
  }

  // ====== getters:用 computed 定义计算属性 ======
  const favoriteCount = computed(() => favoriteIds.value.length)

  // 判断某篇文章是否已收藏
  function isFavorite(articleId) {
    return favoriteIds.value.includes(articleId)
  }

  // ====== actions:用普通函数定义修改方法 ======
  // 切换收藏状态(收藏 → 取消,未收藏 → 收藏)
  function toggleFavorite(articleId) {
    const index = favoriteIds.value.indexOf(articleId)
    if (index === -1) {
      favoriteIds.value.push(articleId)     // 添加收藏
    } else {
      favoriteIds.value.splice(index, 1)    // 取消收藏
    }
    // 同步到 localStorage(持久化保存)
    localStorage.setItem('blog-favorites', JSON.stringify(favoriteIds.value))
  }

  return { favoriteIds, favoriteCount, isFavorite, toggleFavorite }
})

defineStore 的第二个参数有两种写法:Options Store(类似 Vue2 的 data/methods/computed 对象写法)和 Setup Store(用组合式 API 的函数写法)。推荐使用 Setup Store,与组件内 <script setup> 的写法完全一致,学习成本最低。


在组件中使用 Store

任何组件都可以通过 useFavoriteStore() 获取仓库实例,然后直接读写 state 和调用 action。

在 BlogCard 中添加收藏按钮

实例

<!-- 文件路径:src/components/BlogCard.vue -->
<script setup>
import { useFavoriteStore } from '../stores/useFavoriteStore.js'

const props = defineProps({
  id: Number,
  title: String,
  summary: String,
  date: String,
  category: String
})

const favoriteStore = useFavoriteStore()

// 点击收藏按钮时调用
function handleFavorite(e) {
  e.preventDefault()   // 阻止 RouterLink 的跳转
  favoriteStore.toggleFavorite(props.id)
}
</script>

<template>
  <RouterLink :to="{ name: 'post', params: { id } }" class="card-link">
    <div class="card">
      <span class="tag">{{ category }}</span>
      <h3>{{ title }}</h3>
      <p>{{ summary }}</p>
      <div class="card-footer">
        <span class="date">{{ date }}</span>
        <!-- 收藏按钮:已收藏显示实心,未收藏显示空心 -->
        <button class="fav-btn" @click="handleFavorite">
          {{ favoriteStore.isFavorite(id) ? '&#x2665;' : '♡' }}
        </button>
      </div>
    </div>
  </RouterLink>
</template>

<style scoped>
.card-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 10px;
}
.fav-btn {
  background: none;
  border: none;
  font-size: 20px;
  cursor: pointer;
  color: #e74c3c;
  padding: 4px 8px;
}
.fav-btn:hover {
  transform: scale(1.2);
}
</style>

在详情页也添加收藏按钮

实例

<!-- 文件路径:src/views/PostView.vue(部分代码) -->
<script setup>
import { useFavoriteStore } from '../stores/useFavoriteStore.js'

const favoriteStore = useFavoriteStore()
// ... 其他代码
</script>

<template>
  <article v-if="article">
    <div class="post-header">
      <h1>{{ article.title }}</h1>
      <!-- 详情页的收藏按钮:状态与首页完全同步 -->
      <button class="fav-btn" @click="favoriteStore.toggleFavorite(article.id)">
        {{ favoriteStore.isFavorite(article.id) ? '&#x2665; 已收藏' : '♡ 收藏' }}
      </button>
    </div>
    <!-- ... -->
  </article>
</template>

首页和详情页的收藏状态完全同步:在首页收藏一篇文章后,进入详情页自动显示「已收藏」。

这就是全局状态管理的价值:数据只存一份,所有组件读写的是同一个来源。不需要 emit 传递,不需要 props 层层转发。


在 NavBar 中显示收藏数量

在导航栏右侧显示收藏数量,点击可以看到已收藏的文章列表。

实例

<!-- 文件路径:src/components/NavBar.vue -->
<script setup>
import { useFavoriteStore } from '../stores/useFavoriteStore.js'

const favoriteStore = useFavoriteStore()
</script>

<template>
  <header class="navbar">
    <a href="/" class="logo">RUNOOB Blog</a>
    <nav>
      <!-- 收藏数量徽标 -->
      <span class="fav-badge" v-if="favoriteStore.favoriteCount > 0">
        收藏 {{ favoriteStore.favoriteCount }}
      </span>
    </nav>
  </header>
</template>

<style scoped>
.fav-badge {
  background: #e74c3c;
  color: #fff;
  padding: 4px 10px;
  border-radius: 12px;
  font-size: 13px;
}
</style>

NavBar 中的数量会自动与最新的收藏列表保持同步——收藏或取消一篇文章,导航栏的数字立刻变化。


Pinia 三大核心能力总结

能力对应代码在收藏功能中的体现
全局共享任意组件都可 useXxxStore()NavBar、BlogCard、PostView 共享同一份收藏列表
响应式state 变化,所有使用方自动更新收藏后首页按钮变红、导航栏数量+1、详情页同步更新
持久化actions 中写入 localStorage刷新页面后收藏列表依然存在

本章小结

本章你掌握了 Pinia 全局状态管理的完整用法:defineStore 定义仓库(state/getters/actions)、在组件中 useXxxStore 获取实例、以及通过 localStorage 实现数据持久化。

「收藏文章」功能让首页、详情页、导航栏之间的数据同步变得十分自然,所有组件自动保持一致。