Zustandsverwaltung und Datenpersistenz

Im vorherigen Kapitel haben wir die Komponentisierung abgeschlossen. Jetzt stehen wir vor dem letzten praktischen Problem:Beim Aktualisieren der Seite gehen alle Daten verloren.。

Um dieses Problem zu lösen, führen wir die von Vue offiziell empfohlene State-Management-BibliothekPiniaein und kombinierenTailwind v4mit den Styling-Fähigkeiten, um einedauerhafte Speicherung der Daten zu erreichen。


Pinia State-Management und Datenpersistenz

Pinia ist eine leichtgewichtige State-Management-Bibliothek für Vue.js. Sie ermöglicht es Ihnen, den Zustand zwischen Komponenten gemeinsam zu nutzen und zu verwalten. Wir können uns Pinia als globalen Datenspeicher vorstellen; alle Komponenten können Daten daraus abrufen oder Daten dort aktualisieren.

Referenz zu Inhalten rund um Pinia:Pinia Einsteiger-Tutorial。

Warum Pinia verwenden?

Im vorherigen Code waren die Daten alle inApp.vuedort. Wenn das Projekt wächst (z. B. das Hinzufügen einer Statistikseite, eines Benutzerzentrums), wird die Weitergabe von Daten zwischen Komponenten zu einem Albtraum im Matrjoschka-Stil (Props Hell).

  • PiniaSie bietet einen globalen Datenspeicher.
  • Jede Komponente kann direkt Daten aus dem Store abrufen oder Methoden auf dem Store auslösen.

Installation und Initialisierung:

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

In src/main.js registrieren:

Beispiel

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

const app = createApp(App)
app.use(createPinia()) // Plugin-Registrierung
app.mount('#app')

Task-Store erstellen (Store)

Erstelle ein `stores`-Verzeichnis unter dem `src`-Verzeichnis und dann `taskStore.js` unter `src/stores`.

Wir verschieben die gesamte Logik, die sich ursprünglich in App.vue befindet, hierher.

Beispiel

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

export const useTaskStore = defineStore('task-store', () => {
  // --- 1. Zustand ---
  // Versuche, den Anfangswert aus LocalStorage zu lesen
  const savedTasks = localStorage.getItem('my-tasks')
  const tasks = ref(savedTasks ? JSON.parse(savedTasks) : [])
  const filter = ref('all')

  // --- 2. Abgeleiteter Zustand (Getters) ---
  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. Aktionen ---
  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. Persistenz ---
  // Auf Änderungen in tasks achten, sobald eine Änderung eintritt, in LocalStorage schreiben
  watch(tasks, (newVal) => {
    localStorage.setItem('my-tasks', JSON.stringify(newVal))
  }, { deep: true }) // Änderungen an Objekten innerhalb des Arrays tiefgehend beobachten

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

App.vue refaktorieren (Verbindung zum Store)

Jetzt muss App.vue die Daten nicht mehr selbst verwalten; es muss nur noch den Store aufrufen.

Beispiel

<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'

// Den Store initialisieren
const taskStore = useTaskStore()

// Verwende storeToRefs, um die reaktive Destrukturierung der Daten beizubehalten
// Auf diese Weise kannst du filter und filteredTasks direkt im Template verwenden
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>

Ausführliche Erklärung der Wissenspunkte

1. storeToRefsWas ist das?

Wenn du es direkt so schreibst:const { filter } = taskStore, verlierst du die Reaktivität. Wenn dufilteränderst, wird die Seite nicht aktualisiert.

  • Grund: Der Store ist einreactiveverpacktes Objekt; direktes Destrukturieren bricht die Referenz.
  • Lösung: VerwendenstoreToRefs. Es wandelt den Zustand im Store um inref, wodurch sichergestellt wird, dass es nach dem Destrukturieren weiterhin "den ganzen Körper durch Ziehen eines einzelnen Haares bewegen" kann.

2. watchTiefe Überwachung (deep: true)

In der Persistenzlogik ist das, was wir überwachen,tasksdas Array.

  • Wenn du es nicht hinzufügst{ deep: true }, nur wenn das Array ersetzt wird (z. B.,tasks.value = []) wird das Speichern ausgelöst.
  • Nach dem Hinzufügen werden auch Änderungen an Eigenschaften von Objekten innerhalb des Arrays (z. B. eine Aufgabe, die von "pending" zu "completed" wechselt) erfasst und auf der Festplatte gespeichert.

3. Persistenzansatz

Wir verwenden einen geschlossenen Kreislauf aus "Lesen bei der Initialisierung + Schreiben bei Änderung":

  1. Lesen: Wenn der Store erstellt wird, vonlocalStoragefetch data, wodurch die App ein "Gedächtnis" erhält.
  2. Schreiben: Verwendewatchum eine vollautomatische Synchronisierung zu erreichen, müssen Entwickler nicht mehr manuell schreibensetItem。

Letzter Feinschliff mit Tailwind v4

Mit Unterstützung des Stores können wir einigeglobale Feedback-Stilehinzufügen. Zum Beispiel in derTaskHeader.vueden Aufgabenfortschritt anzeigen:

Beispiel

<script setup>
<script setup>
import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useTaskStore } from '@/stores/taskStore'; // Stelle sicher, dass der Pfad korrekt ist

// 1. Store initialisieren
const taskStore = useTaskStore();

// 2. Verwende storeToRefs, um tasks zu destrukturieren, und stelle sicher, dass es reaktiv ist
// Wenn du direkt const { tasks } = taskStore; verwendest, bewegt sich die Fortschrittsleiste nicht, wenn Elemente abgehakt werden
const { tasks } = storeToRefs(taskStore);

// 3. Fortschrittslogik schreiben
const progress = computed(() => {
  const total = tasks.value.length;
if (total === 0) return 0; // Division durch Null verhindern

  const completedCount = tasks.value.filter(t => t.isCompleted).length;
// Prozentsatz berechnen und runden
  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">Fortschritt</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>

Nach der Änderung verfügt die gesamte Oberfläche nun über die Fortschrittsbalken-Funktion:

Andere Erweiterungen