Kurs Vue.js · Moduł 11: Pinia

Plugins i Persistence

2 min czytania
W tej lekcji6

System backupów NOVA LAB zapewnia, że krytyczne dane przetrwają nawet restart stacji. W Pinia tę rolę pełnią plugins - rozszerzenia dodające nowe możliwości do stores. Najważniejszy z nich to persistence - zapisywanie state do localStorage.

$subscribe - nasłuchiwanie zmian

$subscribe pozwala reagować na każdą zmianę state:

1const store = useResourceStore()
2
3store.$subscribe((mutation, state) => {
4  console.log('Store changed:', mutation.storeId)
5  console.log('Type:', mutation.type) // 'direct' | '$patch'
6  console.log('New state:', state)
7
8  // Automatyczny zapis do localStorage
9  localStorage.setItem('resources', JSON.stringify(state))
10})

Opcje subscribe:

1store.$subscribe(
2  (mutation, state) => { /* ... */ },
3  { detached: true } // Przetrwa unmount komponentu
4)

$onAction - obserwowanie akcji

1const store = useResourceStore()
2
3store.$onAction(({ name, args, after, onError }) => {
4  console.log(`Action started: ${name}`, args)
5
6  after((result) => {
7    console.log(`Action ${name} completed`, result)
8  })
9
10  onError((error) => {
11    console.error(`Action ${name} failed`, error)
12  })
13})

Plugin persistence

Najpopularniejszy plugin - pinia-plugin-persistedstate:

1npm install pinia-plugin-persistedstate

Konfiguracja:

1// main.js
2import { createPinia } from 'pinia'
3import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
4
5const pinia = createPinia()
6pinia.use(piniaPluginPersistedstate)

Użycie w store:

1export const useSettingsStore = defineStore('settings', () => {
2  const theme = ref('dark')
3  const language = ref('pl')
4
5  return { theme, language }
6}, {
7  persist: true // Cały state do localStorage
8})

Zaawansowana konfiguracja persist

1export const useAuthStore = defineStore('auth', () => {
2  const token = ref(null)
3  const user = ref(null)
4  const tempData = ref(null) // Nie chcemy tego zapisywać
5
6  return { token, user, tempData }
7}, {
8  persist: {
9    // Tylko wybrane pola
10    paths: ['token'],
11
12    // Custom storage
13    storage: sessionStorage,
14
15    // Custom key
16    key: 'nova-lab-auth'
17  }
18})

Tworzenie custom pluginu

Plugin Pinia to funkcja przyjmująca context ze store:

1// plugins/logger.js
2export function loggerPlugin({ store }) {
3  store.$subscribe((mutation, state) => {
4    console.log(`[${mutation.storeId}] ${mutation.type}`)
5  })
6
7  store.$onAction(({ name, store: s, args }) => {
8    console.log(`[${s.$id}] Action: ${name}`, args)
9  })
10}
11
12// main.js
13const pinia = createPinia()
14pinia.use(loggerPlugin)

Plugin dodający właściwości

1function timestampPlugin({ store }) {
2  // Dodaj lastUpdated do każdego store
3  store.lastUpdated = ref(null)
4
5  store.$subscribe(() => {
6    store.lastUpdated = new Date()
7  })
8}
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, computed, watch } from 'vue'
3
4// Symulacja store z $subscribe i pluginami
5const settings = ref({
6  theme: 'dark',
7  language: 'pl',
8  fontSize: 14
9})
10
11const changeLog = ref([])
12const actionLog = ref([])
13
14// Symulacja $subscribe
15watch(settings, (newVal) => {
16  changeLog.value.unshift({
17    time: new Date().toLocaleTimeString(),
18    change: JSON.stringify(newVal)
19  })
20  if (changeLog.value.length > 6) changeLog.value.pop()
21
22  // Symulacja persist - zapis do localStorage
23  try {
24    localStorage.setItem('nova-settings', JSON.stringify(newVal))
25  } catch (e) {}
26}, { deep: true })
27
28// Symulacja $onAction (plugin logujący)
29function logAction(name, args = []) {
30  actionLog.value.unshift({
31    time: new Date().toLocaleTimeString(),
32    action: name,
33    args: args.length ? JSON.stringify(args) : '-'
34  })
35  if (actionLog.value.length > 6) actionLog.value.pop()
36}
37
38// Actions
39function toggleTheme() {
40  settings.value.theme = settings.value.theme === 'dark' ? 'light' : 'dark'
41  logAction('toggleTheme')
42}
43
44function setLanguage(lang) {
45  settings.value.language = lang
46  logAction('setLanguage', [lang])
47}
48
49function changeFontSize(delta) {
50  settings.value.fontSize = Math.max(10, Math.min(24, settings.value.fontSize + delta))
51  logAction('changeFontSize', [delta])
52}
53
54function resetSettings() {
55  settings.value = { theme: 'dark', language: 'pl', fontSize: 14 }
56  logAction('resetSettings')
57}
58
59const persistedData = computed(() => {
60  try {
61    return localStorage.getItem('nova-settings') || 'Brak danych'
62  } catch (e) {
63    return 'Pamięć lokalna niedostępna'
64  }
65})
66</script>
67
68<template>
69  <div class="nova-lab" :class="settings.theme" :style="{ fontSize: settings.fontSize + 'px' }">
70    <h1>NOVA LAB - pluginy i persistence</h1>
71
72    <div class="grid">
73      <div class="panel">
74        <h2>Store ustawień</h2>
75        <div class="setting">
76          <span>Motyw:</span>
77          <button @click="toggleTheme" class="btn">{{ settings.theme }}</button>
78        </div>
79        <div class="setting">
80          <span>Język:</span>
81          <button @click="setLanguage('pl')" class="btn" :class="{ active: settings.language === 'pl' }">PL</button>
82          <button @click="setLanguage('en')" class="btn" :class="{ active: settings.language === 'en' }">EN</button>
83        </div>
84        <div class="setting">
85          <span>Czcionka:</span>
86          <button @click="changeFontSize(-2)" class="btn">A-</button>
87          <span>{{ settings.fontSize }}px</span>
88          <button @click="changeFontSize(2)" class="btn">A+</button>
89        </div>
90        <button @click="resetSettings" class="btn btn-reset">Resetuj wszystko</button>
91      </div>
92
93      <div class="panel">
94        <h2>Dziennik $subscribe</h2>
95        <div v-for="(entry, i) in changeLog" :key="i" class="log-entry">
96          <span class="time">{{ entry.time }}</span>
97          <span class="change">{{ entry.change }}</span>
98        </div>
99        <p v-if="!changeLog.length" class="empty">Brak zmian</p>
100      </div>
101
102      <div class="panel">
103        <h2>Dziennik $onAction (plugin)</h2>
104        <div v-for="(entry, i) in actionLog" :key="i" class="log-entry">
105          <span class="time">{{ entry.time }}</span>
106          <span class="action-name">{{ entry.action }}</span>
107          <span class="args">{{ entry.args }}</span>
108        </div>
109        <p v-if="!actionLog.length" class="empty">Brak akcji</p>
110      </div>
111
112      <div class="panel">
113        <h2>Zapisane (localStorage)</h2>
114        <pre class="persisted">{{ persistedData }}</pre>
115      </div>
116    </div>
117  </div>
118</template>
119
120<style scoped>
121.nova-lab {
122  min-height: 100vh;
123  padding: 20px;
124  font-family: monospace;
125  transition: all 0.3s;
126}
127.nova-lab.dark { background: #0a0e27; color: #00ff88; }
128.nova-lab.light { background: #f0f4f8; color: #1a1e3f; }
129h1 { margin-bottom: 20px; text-align: center; }
130h2 { color: #00b4d8; margin-bottom: 10px; font-size: 1em; }
131.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; max-width: 700px; margin: 0 auto; }
132.panel {
133  background: rgba(0, 180, 216, 0.1);
134  border: 2px solid #00b4d8;
135  padding: 12px;
136}
137.setting { display: flex; align-items: center; gap: 8px; margin: 8px 0; flex-wrap: wrap; }
138.btn {
139  background: #00b4d8;
140  color: #0a0e27;
141  border: none;
142  padding: 4px 12px;
143  cursor: pointer;
144  font-family: monospace;
145}
146.btn:hover { background: #00ff88; }
147.btn.active { background: #00ff88; }
148.btn-reset { background: #ff9900; margin-top: 10px; }
149.log-entry {
150  padding: 4px 0;
151  border-bottom: 1px solid rgba(0, 180, 216, 0.2);
152  font-size: 0.8em;
153  display: flex;
154  gap: 8px;
155  flex-wrap: wrap;
156}
157.time { color: #00b4d8; }
158.change { color: #00ff88; word-break: break-all; }
159.action-name { color: #ff9900; }
160.args { color: #666; }
161.empty { color: #666; font-style: italic; font-size: 0.85em; }
162.persisted {
163  background: #0a0e27;
164  color: #00ff88;
165  padding: 8px;
166  font-size: 0.75em;
167  word-break: break-all;
168  white-space: pre-wrap;
169}
170</style>

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Do czego służy $subscribe() w Pinia?

  2. 2. Jak dodać persistence (zapis do localStorage) do Pinia store?

To 2 z 6 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Stwórz log store z addLog, clearLogs i plugin logujący zmiany do konsoli.

  • Układanie w pionie

    Ułóż kroki dodania persistence do store:

  • Układanie w pionie

    Uporządkuj przepływ nasłuchiwania zmian store:

  • Edytor kodu

    Stwórz resource store z alertami i statusLevel getter - consume tworzy alerty.

  • Klikanie w kolejności

    Ułóż kod tworzenia custom pluginu Pinia:

  • Układanie w pionie

    Uporządkuj opcje persist od najprostszej do najbardziej szczegółowej:

  • Układanie w poziomie

    Ułóż składnię dostępu do state w komponencie:

  • Układanie w pionie

    Uporządkuj pełny przepływ danych w aplikacji z Pinia:

  • Układanie w poziomie

    Ułóż składnię $patch z funkcją callback:

  • Klikanie w kolejności

    Ułóż pełny lifecycle store w aplikacji:

  • Układanie w pionie

    Uporządkuj scenariusze od prostszych (provide) do złożonych (Pinia):

  • Układanie w pionie

    Ułóż kod rejestracji pluginu w main.js:

Przydatne artykuły