Pinia Anfänger-Tutorial

In Vue3 ist State-Management ein unumgängliches Thema.

Pinia ist eine leichtgewichtige State-Management-Bibliothek für Vue.js, mit der man State zwischen Komponenten teilen und verwalten kann. Wir können uns Pinia als ein globales Datenlager vorstellen, aus dem alle Komponenten Daten abrufen oder aktualisieren können.

In diesem Kapitel stellen wir Pinia vor, die offiziell empfohlene State-Management-Bibliothek für Vue3. Im Vergleich zu Vuex bietet Pinia eine einfachere State-Management-Lösung, die besser zur Denkweise der Vue3 Composition API passt.

Das folgende Diagramm vergleicht die strukturellen Unterschiede zwischen den beiden:

  • Vuex verwendet einen einzigen Store mit mehrstufigen Modulen in einer Baumstruktur, mit fester Hierarchie, setzt auf Mutationen und ist insgesamt schwergewichtiger.
  • Pinia hingegen besteht aus mehreren unabhängigen Stores; es ist flach, leichtgewichtig, hat keine Modultrennung und keinen Namespace-Overhead.

Kernfunktionen von Pinia:

  • Unterstützt Vue2 und Vue3
  • Minimalistisches API-Design
  • Vollständige TypeScript-Unterstützung
  • Unterstützt die Composition API
  • Modulares Design ohne verschachtelte Module

Pinias flache Struktur ermöglicht es Komponenten, sich direkt mit jedem Store zu verbinden und sich zu nehmen, was sie brauchen, ohne über Baummodule oder Namespace-Pfade zu gehen. Der State-Fluss ist einfach und direkt, mit geringer Kopplung.


Installation und Konfiguration

Ein Projekt mit Vite erstellen:

npm create vite@latest vue-pinia-demo --template vue
cd vue-pinia-demo
npm install

Pinia installieren

Installiere zuerst Pinia in deinem Vue3-Projekt:

npm install pinia
# 或者
yarn add pinia

Pinia besteht aus drei Teilen:

  • state: speichert Daten
  • getters: berechnet abgeleitete Daten
  • actions: führt Logik aus, ändert state

Pinia konfigurieren

Pinia in src/main.js oder src/main.ts konfigurieren:

Beispiel

// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

// Pinia-Instanz erstellen
const pinia = createPinia()
// Die Vue-App erstellen
const app = createApp(App)

// Pinia verwenden
app.use(pinia)
app.mount('#app')

Erstelle deinen ersten Store

Ein Store ist der Datenspeicher in Pinia. Lass uns einen einfachen Zähler-Store erstellen.

Einen Store definieren

Erstelle eine neue Datei src/stores/useCounter.js:

Beispiel

// stores/useCounter.js
import { defineStore } from 'pinia'

// Definiere einen Store mit defineStore
// Der erste Parameter ist die eindeutige ID des Stores
// Der zweite Parameter enthält die Konfigurationsoptionen für den Store
export const useCounterStore = defineStore('counter', {
  // state: definiert die Zustandsdaten des Stores
  state: () => ({
    count: 0,
    name: 'My Counter'
  }),

  // getters: definiert berechnete Eigenschaften basierend auf dem State
  Getter: {
    doubleCount: (state) => state.count * 2,
    // Verwende this, um auf andere Getter zuzugreifen
    doubleCountPlusOne() {
      return this.doubleCount + 1
    }
  },

  // actions: definiert Methoden zum Ändern des Zustands
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    // Kann Parameter akzeptieren
    incrementBy(amount) {
      this.count += amount
    },
    // Asynchrone Aktion
    async incrementAsync() {
      // Asynchrone Operation simulieren
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.count++
    }
  }
})

Erklärung der Store-Struktur

Verstehen wir die verschiedenen Teile eines Stores anhand einer Tabelle:

Teil Zweck Beispiel
state Definiert die gespeicherten Daten count: 0
Getter Berechnete Eigenschaften basierend auf dem Zustand doubleCount: state => state.count * 2
actions Methoden zum Ändern des Zustands increment() { this.count++ }

Verwendung des Stores in Komponenten

Grundlegende Verwendung

Der Komponentencode für src/components/CounterComponent.vue lautet wie folgt:

Beispiel

<!-- CounterComponent.vue -->
<template>
  <div class="counter">
    <h3>{{ store.name }}</h3>
    <p>Aktueller Zählerstand: {{ store.count }}</p>
    <p>Doppelter Zählerstand: {{ store.doubleCount }}</p>
    <p>Doppelter Zählerstand plus eins: {{ store.doubleCountPlusOne }}</p>
   
    <button @click="store.increment()">+1</button>
    <button @click="store.decrement()">-1</button>
    <button @click="store.incrementBy(5)">+5</button>
    <button @click="store.incrementAsync()">Asynchron +1</button>
   
    <button @click="reset">Zurücksetzen</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '../stores/useCounter'

// Den Store in setup verwenden
const store = useCounterStore()

// Methode zum Zurücksetzen des Zustands
function reset() {
store.$reset() // Die $reset-Methode kann den Zustand auf seinen Anfangswert zurücksetzen
}
</script>

Ändern Sie src/App.vue wie folgt:

Beispiel

<script setup>
import CounterComponent from './components/CounterComponent.vue'
</script>

<template>
  <CounterComponent />
</template>

Die gesamte Projektstruktur:

Führen Sie den Befehlnpm run devaus, und besuchen Sie im Browserhttp://localhost:5173/, und sehen Sie sich den Effekt an:

Reaktive Destrukturierung

Wenn du State-Eigenschaften direkt im Template verwenden möchtest, kannst dustoreToRefs:

Beispiel

<template>
  <div>
    <p>Anzahl: {{ count }}</p>
    <p>Name: {{ name }}</p>
  </div>
</template>

<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()

// Verwende storeToRefs, um die Reaktivität zu erhalten
const { count, name } = storeToRefs(store)

// Hinweis: Direktes Destrukturieren führt zum Verlust der Reaktivität!
// Falscher Weg: const { count, name } = store
</script>

Store im Composition-API-Stil

Pinia unterstützt auch das Definieren von Stores im Composition-API-Stil:

Beispiel

// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // Zustand
  const user = ref(null)
  const isLoggedIn = ref(false)

  // Getter
  const userName = computed(() => user.value?.name || 'Gast')
  const userAge = computed(() => user.value?.age || 0)

  // Aktionen
  function login(userData) {
    user.value = userData
    isLoggedIn.value = true
  }

  function logout() {
    user.value = null
    isLoggedIn.value = false
  }

  return {
    user,
    isLoggedIn,
    userName,
    userAge,
    login,
    logout
  }
})

Interaktion zwischen Stores

Mehrere Stores können sich gegenseitig aufrufen:

Beispiel

// stores/cart.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),

  actions: {
    addItem(product) {
      this.items.push(product)
     
      // Eine Aktion aus einem anderen Store aufrufen
      const userStore = useUserStore()
      if (userStore.isLoggedIn) {
        // Mit dem Warenkorb des Benutzers synchronisieren
        this.syncToUserCart()
      }
    }
  }
})

Praktisches Beispiel: Warenkorb

Lassen Sie uns ein vollständiges Warenkorb-Beispiel erstellen:

Beispiel

// stores/products.js
export const useProductStore = defineStore('products', {
  state: () => ({
    products: [
      { id: 1, name: 'Laptop', price: 5999, stock: 10 },
      { id: 2, name: 'Smartphone', price: 3999, stock: 20 },
      { id: 3, name: 'Kabellose Ohrhörer', price: 299, stock: 50 }
    ]
  }),

  Getter: {
    // Produkt nach ID abrufen
    getProductById: (state) => (id) => {
      return state.products.find(product => product.id === id)
    },
    // Produkte auf Lager abrufen
    availableProducts: (state) => {
      return state.products.filter(product => product.stock > 0)
    }
  }
})

Beispiel

// stores/cart.js
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [], // { productId, quantity }
    discount: 0
  }),

  Getter: {
    // Gesamtzahl der Artikel im Warenkorb
    totalItems: (state) => {
      return state.items.reduce((total, item) => total + item.quantity, 0)
    },
    // Gesamtmenge des Warenkorbs
    totalPrice: (state) => {
      const productStore = useProductStore()
      return state.items.reduce((total, item) => {
        const product = productStore.getProductById(item.productId)
        return total + (product?.price || 0) * item.quantity
      }, 0)
    },
    // Gesamtpreis nach Rabatt
    finalPrice: (state) => {
      return state.totalPrice * (1 - state.discount / 100)
    }
  },

  actions: {
    // Produkt zum Warenkorb hinzufügen
    addToCart(productId, quantity = 1) {
      const existingItem = this.items.find(item => item.productId === productId)
     
      if (existingItem) {
        existingItem.quantity += quantity
      } else {
        this.items.push({ productId, quantity })
      }
    },

    // Produkt aus dem Warenkorb entfernen
    removeFromCart(productId) {
      this.items = this.items.filter(item => item.productId !== productId)
    },

    // Warenkorb leeren
    clearCart() {
      this.items = []
      this.discount = 0
    },

    // Rabatt festlegen
    setDiscount(percent) {
      this.discount = Math.max(0, Math.min(100, percent))
    }
  }
})

Den Warenkorb in einer Komponente verwenden

Beispiel

<!-- ShoppingCart.vue -->
<template>
  <div class="shopping-cart">
    <h3>Warenkorb ({{ cart.totalItems }} Artikel)</h3>
   
    <div v-if="cart.items.length === 0">
      <p>Der Warenkorb ist leer</p>
    </div>
   
    <div v-else>
      <div v-for="item in cartItems" :key="item.product.id" class="cart-item">
        <span>{{ item.product.name }}</span>
        <span>¥{{ item.product.price }}</span>
        <span>Menge: {{ item.quantity }}</span>
        <span>Zwischensumme: ¥{{ item.product.price * item.quantity }}</span>
        <button @click="cart.removeFromCart(item.product.id)">Löschen</button>
      </div>
     
      <div class="cart-summary">
        <p>Gesamt: ¥{{ cart.totalPrice }}</p>
        <p v-if="cart.discount >0">Rabatt: {{ cart.discount }}%</p>
        <p>Bezahlter Betrag: ¥{{ cart.finalPrice }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useCartStore } from '@/stores/cart'
import { useProductStore } from '@/stores/products'

const cart = useCartStore()
const products = useProductStore()

// Details der Warenkorbartikel berechnen
const cartItems = computed(() => {
  return cart.items.map(item => ({
    product: products.getProductById(item.productId),
    quantity: item.quantity
})).filter(item => item.product) // Produkte herausfiltern, die nicht existieren
})
</script>

Best Practices und Überlegungen

1. Namenskonventionen für Stores

Beispiel

// Gute Benennung
export const useUserStore = defineStore('user', { /* ... */ })
export const useProductStore = defineStore('products', { /* ... */ })

// Zu vermeidende Benennung
export const userStore = defineStore('user', { /* ... */ }) // Fehlendes use-Präfix

2. Zustandsinitialisierung

Beispiel

// Empfohlen: Verwenden Sie eine Funktion, um den Anfangszustand zurückzugeben
state: () => ({
  items: [],
  loading: false,
  error: null
})

// Nicht empfohlen: direkt ein Objekt verwenden
state: {
  items: []  // Dies wird dazu führen, dass alle Instanzen dasselbe Array teilen!
}

3. Umgang mit asynchronen Operationen

Beispiel

actions: {
  async fetchUserData(userId) {
    this.loading = true
    this.error = null
   
    try {
      const response = await api.getUser(userId)
      this.user = response.data
    } catch (error) {
      this.error = error.message
    } finally {
      this.loading = false
    }
  }
}

4. Datenpersistenz

Für Daten, die persistiert werden müssen, kannst du ein Plugin verwenden:

npm install pinia-plugin-persistedstate

Beispiel

import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

export const useUserStore = defineStore('user', {
  state: () => ({
    Token: '',
    userInfo: {}
  }),
 
  // Persistenz aktivieren
  persist: true
})

Pinia API

Kategorie API Beschreibung
Erstellen defineStore Definiere einen unabhängigen Store, der State / Getter / Actions enthält
实例化 createPinia Erstelle eine Pinia-Root-Instanz für app.use()
Verwendung in Komponenten useStore()(Benutzerdefiniert, z. B.useUserStore) Ruft einen Store auf, gibt reaktiven State, Getter und Actions zurück
State state Gibt ein Funktionsobjekt zurück, das zum Definieren von veränderbarem globalen State verwendet wird
State-Operationen store.$state Direktes Lesen/Schreiben des gesamten States (auf Objektebene)
State-Operationen store.$patch() State batchweise ändern, unterstützt sowohl Objekt- als auch Funktionsmodi
State-Ersetzung store.$reset() Zurücksetzen auf den initialen State, nur verfügbar, wenn er mit der setup-Methode definiert wurde
State-Abonnement store.$subscribe() Auf State-Änderungen lauschen, geeignet für lokale Persistenz
Action-Aufruf store.$onAction() Vor und nach Action-Aufrufen lauschen, kann für Logging und Tracking verwendet werden
Getter getters Abgeleitete Daten, automatisch auf Basis des States zwischengespeichert
Plugins pinia.use() Plugins registrieren, um die Fähigkeiten des Stores zu erweitern
Definitionsmethode defineStore(id, options) Options-API-Syntax
Definitionsmethode defineStore(id, () => {...}) Setup-Syntax, gibt State / Getter / Action zurück
Store-Eigenschaften store.$id Eindeutige Kennung des aktuellen Stores
Store-Eigenschaften store.$ready(Einige Versionen) Status-Flag nach abgeschlossener Store-Initialisierung
Persistenz (Plugin) persist Storage-Plugins aktivieren (z. B. localStorage)
Hilfsfunktionen storeToRefs() State / Getter in refs umwandeln, wobei die Reaktivität erhalten bleibt

Für weitere Informationen siehe die offizielle Pinia-Website:https://pinia.vuejs.org/

Weitere Erweiterungen