Pinia — глобальное управление состоянием
В этой главе вы научитесь использовать Pinia для управления общими данными между компонентами и страницами и реализуете функцию "избранные статьи".
Зачем нам нужно глобальное управление состоянием?
До сих пор данные передавались между компонентами через props и emit.
Этого достаточно для простых сценариев «родитель-потомок», но это становится неудобным в следующих ситуациях:
- Состояние избранного должно синхронизироваться между главной страницей и страницей деталей.
- Состояние входа пользователя должно быть общим для всех страниц.
- Данные корзины покупок должны передаваться между панелью навигации, страницей товара и страницей оформления заказа.
Передача props по слоямПроблема: если данные из компонента A нужно передать в компонент D, они должны пройти через B и C как через промежуточные звенья, хотя самим промежуточным компонентам эти данные не нужны.
PiniaPinia — это библиотека управления состоянием, официально рекомендованная Vue 3. Она предоставляет глобальный «центр данных», где любой компонент может напрямую читать и изменять данные без послойной передачи.
Установка и регистрация 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 | Данные Store | Данные / ref компонента |
| геттеры | Производные вычисляемые данные | Вычисляемые свойства компонента |
| actions | Методы для изменения данных | Методы / функции компонента |
Пример
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// defineStore('storeName', () => { ... })
// Второй параметр использует синтаксис функции, соответствующий стилю Composition API.
export const useFavoriteStore = defineStore('favorite', () => {
// ====== состояние: определить состояние с помощью ref ======
// Используйте массив для хранения ID статей, добавленных в избранное
const favoriteIds = ref([])
// Восстановить список избранного из localStorage
const saved = localStorage.getItem('blog-favorites')
if (saved) {
favoriteIds.value = JSON.parse(saved)
}
// ====== геттеры: определить вычисляемые свойства с помощью computed ======
const favoriteCount = computed(() => favoriteIds.value.length)
// Определить, добавлена ли статья в избранное
function isFavorite(articleId) {
return favoriteIds.value.includes(articleId)
}
// ====== действия: определение методов изменения с помощью обычных функций ======
// Переключить статус избранного (в избранном → не в избранном, не в избранном → в избранном)
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(аналогично синтаксису объекта data/methods/computed в Vue 2) иSetup Store(синтаксис функции с использованием Composition API). Рекомендуется использовать Setup Store, который согласуется с
<script setup>синтаксисом, используемым внутри компонентов, и требует наименьших затрат на изучение.
Использование хранилища в компонентах
Любой компонент можетuseFavoriteStore()получить экземпляр хранилища, а затем напрямую читать/записывать состояние и вызывать действия.
Добавление кнопки избранного в 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">ПРИМЕР Блог</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 используют один и тот же список избранного. |
| Реактивность | При изменении состояния все потребители автоматически обновляются. | После добавления в избранное кнопка на главной странице становится красной, счётчик в панели навигации увеличивается на 1, а страница сведений обновляется синхронно. |
| Персистентность | Запись в localStorage в actions | Список избранного по-прежнему существует после обновления страницы. |
Краткое содержание главы
В этой главе вы освоили полное использование Pinia для глобального управления состоянием: определение store с помощью defineStore (state/getters/actions), получение экземпляра с помощью useXxxStore в компонентах и реализацию сохранения данных через localStorage.
Функция «избранные статьи» делает синхронизацию данных между главной страницей, страницей сведений и панелью навигации очень естественной, а все компоненты автоматически остаются согласованными.
Другие расширения