Kurs Vue.js · Moduł 11: Pinia
Plugins i Persistence
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-persistedstateKonfiguracja:
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. Do czego służy $subscribe() w Pinia?
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: