Pinia — 全局状态管理
本章你将学会用 Pinia 管理跨组件、跨页面的共享数据,并实现「收藏文章」功能。
为什么需要全局状态管理?
到目前为止,数据在组件之间通过 props 和 emit 传递。
这对于父子关系简单的场景够用,但遇到以下情况就变得吃力:
- 收藏状态需要在首页和详情页之间同步
- 用户登录状态需要所有页面共享
- 购物车数据需要在导航栏、商品页、结算页之间传递
props 逐层传递 的痛点:如果 A 组件的数据要传到 D 组件,需要经过 B、C 两层中转,而这些中间组件自己并不需要这些数据。
Pinia 是 Vue3 官方推荐的状态管理库,它提供了一个全局的「数据中心」,任何组件都可以直接读取和修改,不需要层层传递。
安装与注册 Pinia
如果在创建项目时已选择 Pinia,跳过安装步骤。
否则:
$ npm install pinia
在 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 |
实例
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 中添加收藏按钮
实例
<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) ? '♥' : '♡' }}
</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>
在详情页也添加收藏按钮
实例
<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) ? '♥ 已收藏' : '♡ 收藏' }}
</button>
</div>
<!-- ... -->
</article>
</template>
首页和详情页的收藏状态完全同步:在首页收藏一篇文章后,进入详情页自动显示「已收藏」。
这就是全局状态管理的价值:数据只存一份,所有组件读写的是同一个来源。不需要 emit 传递,不需要 props 层层转发。
在 NavBar 中显示收藏数量
在导航栏右侧显示收藏数量,点击可以看到已收藏的文章列表。
实例
<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 实现数据持久化。
「收藏文章」功能让首页、详情页、导航栏之间的数据同步变得十分自然,所有组件自动保持一致。
