Управление состоянием и сохранение данных

В предыдущей главе мы завершили разбиение на компоненты. Теперь мы сталкиваемся с последней практической проблемой:При обновлении страницы все данные теряются.。

Чтобы решить эту проблему, мы представим официально рекомендуемую Vue библиотеку управления состояниемPinia, и объединимTailwind v4возможности стилизации, чтобы обеспечитьпостоянное хранение данных。


Pinia: управление состоянием и сохранение данных

Pinia — это легковесная библиотека управления состоянием для Vue.js. Она позволяет обмениваться состоянием и управлять им между компонентами. Мы можем рассматривать Pinia как глобальное хранилище данных; все компоненты могут получать из него данные или обновлять данные в нем.

Справочная информация по материалам о Pinia:Учебник по началу работы с Pinia。

Почему стоит использовать Pinia?

В предыдущем коде все данные былиApp.vueтам. Если проект разрастается (например, добавляется страница статистики, центр пользователя), передача данных между компонентами превращается в кошмар в стиле матрёшки (Props Hell).

  • PiniaОна предоставляет глобальное хранилище данных.
  • Любой компонент может напрямую получать данные из хранилища или вызывать методы хранилища.

Установка и инициализация:

npm install pinia
npm install @vue/devtools-kit -D

Зарегистрируйте в src/main.js:

Пример

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import './style.css'

const app = createApp(App)
app.use(createPinia()) // Регистрация плагина
app.mount('#app')

Создание хранилища задач (store)

Создайте каталог `stores` в каталоге `src`, затем создайте `taskStore.js` в `src/stores`.

Мы перенесём сюда всю логику, изначально находившуюся в App.vue.

Пример

import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'

export const useTaskStore = defineStore('task-store', () => {
  // --- 1. Состояние ---
  // Попробуйте прочитать начальное значение из LocalStorage
  const savedTasks = localStorage.getItem('my-tasks')
  const tasks = ref(savedTasks ? JSON.parse(savedTasks) : [])
  const filter = ref('all')

  // --- 2. Производное состояние (геттеры) ---
  const filteredTasks = computed(() => {
    if (filter.value === 'active') return tasks.value.filter(t => !t.isCompleted)
    if (filter.value === 'completed') return tasks.value.filter(t => t.isCompleted)
    return tasks.value
  })

  // --- 3. Действия ---
  const addTask = (title) => {
    tasks.value.unshift({ id: crypto.randomUUID(), title, isCompleted: false })
  }

  const removeTask = (id) => {
    tasks.value = tasks.value.filter(t => t.id !== id)
  }

  const toggleTask = (id) => {
    const task = tasks.value.find(t => t.id === id)
    if (task) task.isCompleted = !task.isCompleted
  }

  // --- 4. Сохранение ---
  // Следите за изменениями в tasks: как только происходит изменение, записывайте в LocalStorage
  watch(tasks, (newVal) => {
    localStorage.setItem('my-tasks', JSON.stringify(newVal))
  }, { deep: true }) // Глубоко отслеживать изменения объектов внутри массива

  return {
    tasks,
    filter,
    filteredTasks,
    addTask,
    removeTask,
    toggleTask
  }
})

Рефакторинг App.vue (Подключение к хранилищу)

Теперь App.vue не нужно управлять данными самостоятельно; ему просто нужно обращаться к хранилищу.

Пример

<script setup>
import { storeToRefs } from 'pinia'
import { useTaskStore } from '@/stores/taskStore'
import TaskHeader from './components/TaskHeader.vue'
import TaskInput from './components/TaskInput.vue'
import TaskFilter from './components/TaskFilter.vue'
import TaskItem from './components/TaskItem.vue'

// Инициализация хранилища
const taskStore = useTaskStore()

// Используйте storeToRefs, чтобы деструктуризация данных оставалась реактивной
// Так вы можете напрямую использовать filter и filteredTasks в шаблоне
const { filter, filteredTasks } = storeToRefs(taskStore)
const { addTask, removeTask, toggleTask } = taskStore
</script>

<template>
  <div class="min-h-screen py-12 px-4">
    <div class="max-w-md mx-auto bg-white rounded-3xl shadow-xl border border-slate-100 overflow-hidden">
      <TaskHeader />
      <main class="p-6">
        <TaskInput @add-task="addTask" />
        <TaskFilter v-model="filter" />

        <ul class="space-y-3">
          <TransitionGroup name="list">
            <TaskItem
             v-for="task in filteredTasks"
             :key="task.id"
             :task="task"
             @toggle="toggleTask"
             @remove="removeTask"
           />
          </TransitionGroup>
        </ul>
      </main>
    </div>
  </div>
</template>

Подробное объяснение ключевых моментов

1. storeToRefsЧто это такое?

Если написать это напрямую так:const { filter } = taskStore, вы потеряете реактивность. Когда вы изменяетеfilter, страница не обновится.

  • Причина: Хранилище — этоreactiveобёрнутый объект; прямая деструктуризация нарушит ссылку.
  • Решение: используйтеstoreToRefs. Он преобразует состояние в хранилище вref, гарантируя, что после деструктуризации всё ещё можно «потянуть за одну волосинку — и сдвинется всё тело».

2. watchГлубокое отслеживание (deep: true)

В логике сохранения мы отслеживаемtasksмассив.

  • Если его не добавить{ deep: true }, только когда массив заменяется (например,tasks.value = []), будет запущено сохранение.
  • После его добавления изменения свойств объектов внутри массива (например, когда задача меняет статус с "pending" на "completed") также будут отслеживаться и сохраняться на диск.

3. Подход к сохранению

Мы используем замкнутый цикл «чтение при инициализации + запись при изменении»:

  1. Чтение: Когда Store создаётся, изlocalStorageполучаем данные, давая приложению «память».
  2. Запись: Используйтеwatchчтобы достичь полностью автоматической синхронизации, разработчикам больше не нужно вручную писатьsetItem。

Финальная полировка с Tailwind v4

С поддержкой хранилища мы можем добавить некоторыеглобальные стили обратной связи. Например, вTaskHeader.vueотобразить прогресс задачи:

Пример

<script setup>
<script setup>
import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useTaskStore } from '@/stores/taskStore'; // Убедитесь, что путь правильный

// 1. Инициализируйте хранилище
const taskStore = useTaskStore();

// 2. Используйте storeToRefs, чтобы деструктурировать tasks и сохранить реактивность
// Если вы напрямую сделаете const { tasks } = taskStore; прогресс-бар не будет двигаться при отметке элементов
const { tasks } = storeToRefs(taskStore);

// 3. Напишите логику прогресса
const progress = computed(() => {
  const total = tasks.value.length;
if (total === 0) return 0; // Предотвратить деление на ноль

  const completedCount = tasks.value.filter(t => t.isCompleted).length;
// Вычислить процент и округлить его
  return Math.round((completedCount / total) * 100);
});
</script>

<template>
  <header class="bg-linear-to-br from-blue-600 to-indigo-700 p-8 text-white">
    <h1 class="text-3xl font-black tracking-tight">TaskHub</h1>
   
    <div class="mt-6">
      <div class="flex justify-between items-end mb-2">
        <p class="text-blue-100/80 text-xs font-bold uppercase tracking-wider">Прогресс выполнения</p>
        <span class="text-2xl font-mono font-black">{{ progress }}%</span>
      </div>
     
      <div class="h-2 w-full bg-white/20 rounded-full overflow-hidden backdrop-blur-sm">
        <div
         class="h-full bg-white shadow-[0_0_15px_rgba(255,255,255,0.5)] transition-all duration-500 ease-out"
         :style="{ width: `${progress}%` }"
       ></div>
      </div>
    </div>
  </header>
</template>

После внесения изменений весь интерфейс теперь имеет функцию индикатора выполнения:

Другие расширения