Kurs Vue.js · Moduł 11: Pinia
Kompozycja Stores
W tej lekcji3
W NOVA LAB różne systemy stacji muszą ze sobą współpracować - system tlenu komunikuje się z systemem alarmowym, a system nawigacji z systemem komunikacji. Podobnie w Pinia - stores mogą importować i używać innych stores.
Store używający innego store
1// stores/auth.js
2import { defineStore } from 'pinia'
3import { ref, computed } from 'vue'
4
5export const useAuthStore = defineStore('auth', () => {
6 const user = ref(null)
7 const token = ref(null)
8
9 const isAuthenticated = computed(() => !!token.value)
10 const userName = computed(() => user.value?.name || 'Guest')
11
12 async function login(credentials) {
13 const res = await fetch('/api/login', {
14 method: 'POST',
15 headers: { 'Content-Type': 'application/json' },
16 body: JSON.stringify(credentials)
17 })
18 const data = await res.json()
19 token.value = data.token
20 user.value = data.user
21 }
22
23 function logout() {
24 user.value = null
25 token.value = null
26 }
27
28 return { user, token, isAuthenticated, userName, login, logout }
29})1// stores/missions.js - używa auth store
2import { defineStore } from 'pinia'
3import { ref } from 'vue'
4import { useAuthStore } from './auth'
5
6export const useMissionsStore = defineStore('missions', () => {
7 const missions = ref([])
8
9 async function fetchMissions() {
10 // Użyj auth store do pobrania tokena
11 const authStore = useAuthStore()
12
13 if (!authStore.isAuthenticated) {
14 throw new Error('Must be logged in')
15 }
16
17 const res = await fetch('/api/missions', {
18 headers: {
19 'Authorization': `Bearer ${authStore.token}`
20 }
21 })
22
23 missions.value = await res.json()
24 }
25
26 return { missions, fetchMissions }
27})Wzajemne zależności stores
Stores mogą nawzajem się używać, ale wywołuj useStore() wewnątrz actions (nie na górze setup function), aby uniknąć problemów z cyklicznymi zależnościami:
1// stores/notifications.js
2import { defineStore } from 'pinia'
3import { ref } from 'vue'
4
5export const useNotificationStore = defineStore('notifications', () => {
6 const notifications = ref([])
7
8 function add(message, type = 'info') {
9 notifications.value.push({
10 id: Date.now(),
11 message,
12 type,
13 timestamp: new Date()
14 })
15 }
16
17 function remove(id) {
18 notifications.value = notifications.value.filter(n => n.id !== id)
19 }
20
21 return { notifications, add, remove }
22})
23
24// stores/resources.js - używa notifications
25import { useNotificationStore } from './notifications'
26
27export const useResourceStore = defineStore('resources', () => {
28 const oxygen = ref(100)
29
30 function consumeOxygen(amount) {
31 oxygen.value -= amount
32
33 if (oxygen.value < 20) {
34 // Wywołaj useStore WEWNĄTRZ action
35 const notifStore = useNotificationStore()
36 notifStore.add('Oxygen level critical!', 'danger')
37 }
38 }
39
40 return { oxygen, consumeOxygen }
41})Architektura wielu stores
Zaleca się tworzenie wielu małych, specjalizowanych stores zamiast jednego dużego:
1stores/
2├── auth.js # Autoryzacja użytkownika
3├── resources.js # Zasoby stacji (tlen, woda, energia)
4├── crew.js # Dane załogi
5├── missions.js # Misje i zadania
6├── notifications.js # System powiadomień
7└── settings.js # Ustawienia aplikacjiKażdy store odpowiada za jeden "moduł" aplikacji - tak jak każdy moduł stacji NOVA LAB ma swoją funkcję.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// Kompozycja stores - AuthStore
5const user = ref({ name: 'Komandor Nova', role: 'admin' })
6const token = ref('jwt-token-xyz')
7const isAuthenticated = computed(() => !!token.value)
8
9// Kompozycja stores - ResourceStore (korzysta z AuthStore)
10const resources = ref([
11 { id: 1, name: 'Tlen', level: 92, unit: '%' },
12 { id: 2, name: 'Woda', level: 78, unit: '%' },
13 { id: 3, name: 'Energia', level: 65, unit: '%' }
14])
15
16// Kompozycja stores - NotificationStore
17const notifications = ref([])
18function addNotification(message, type = 'info') {
19 const id = Date.now()
20 notifications.value.push({ id, message, type, time: new Date().toLocaleTimeString() })
21 setTimeout(() => {
22 notifications.value = notifications.value.filter(n => n.id !== id)
23 }, 4000)
24}
25
26// Actions z ResourceStore korzystają z NotificationStore
27function consumeResource(id, amount) {
28 const resource = resources.value.find(r => r.id === id)
29 if (!resource) return
30
31 resource.level = Math.max(0, resource.level - amount)
32
33 if (resource.level < 20) {
34 // Między stores: zasoby -> powiadomienia
35 addNotification(`${resource.name} - krytycznie niski poziom: ${resource.level}${resource.unit}!`, 'danger')
36 } else if (resource.level < 50) {
37 addNotification(`${resource.name} poniżej 50%: ${resource.level}${resource.unit}`, 'warning')
38 }
39}
40
41function refillResource(id) {
42 const resource = resources.value.find(r => r.id === id)
43 if (resource) {
44 resource.level = 100
45 addNotification(`${resource.name}: uzupełniono do 100%`, 'success')
46 }
47}
48
49// Getter łączący stores
50const criticalResources = computed(() =>
51 resources.value.filter(r => r.level < 30)
52)
53</script>
54
55<template>
56 <div class="nova-lab">
57 <h1>NOVA LAB - kompozycja stores</h1>
58
59 <div class="user-bar" v-if="isAuthenticated">
60 Zalogowano: {{ user.name }} ({{ user.role }})
61 </div>
62
63 <div class="resources-grid">
64 <div v-for="r in resources" :key="r.id"
65 class="resource-card" :class="{ critical: r.level < 20, warning: r.level < 50 }">
66 <h3>{{ r.name }}</h3>
67 <div class="level-bar">
68 <div class="level-fill" :style="{ width: r.level + '%' }"></div>
69 </div>
70 <div class="level-text">{{ r.level }}{{ r.unit }}</div>
71 <div class="r-actions">
72 <button @click="consumeResource(r.id, 15)" class="btn">-15</button>
73 <button @click="refillResource(r.id)" class="btn btn-green">Uzupełnij</button>
74 </div>
75 </div>
76 </div>
77
78 <div v-if="criticalResources.length" class="critical-alert">
79 KRYTYCZNE: {{ criticalResources.map(r => r.name).join(', ') }}
80 </div>
81
82 <div class="notifications">
83 <div v-for="n in notifications" :key="n.id" class="notif" :class="n.type">
84 {{ n.time }} - {{ n.message }}
85 </div>
86 </div>
87 </div>
88</template>
89
90<style scoped>
91.nova-lab {
92 background: #0a0e27;
93 color: #00ff88;
94 min-height: 100vh;
95 padding: 20px;
96 font-family: monospace;
97}
98h1 { color: #00ff88; margin-bottom: 15px; text-align: center; }
99.user-bar {
100 text-align: center;
101 padding: 8px;
102 background: rgba(0, 180, 216, 0.1);
103 border: 1px solid #00b4d8;
104 margin-bottom: 15px;
105 color: #00b4d8;
106}
107.resources-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 15px; }
108.resource-card {
109 background: rgba(0, 255, 136, 0.05);
110 border: 2px solid #00ff88;
111 padding: 15px;
112 text-align: center;
113}
114.resource-card.warning { border-color: #ff9900; }
115.resource-card.critical { border-color: #ff4444; background: rgba(255, 68, 68, 0.1); }
116h3 { color: #00b4d8; margin-bottom: 8px; }
117.level-bar { height: 12px; background: #1a1e3f; border: 1px solid #00b4d8; margin: 8px 0; }
118.level-fill { height: 100%; background: #00ff88; transition: width 0.3s; }
119.resource-card.warning .level-fill { background: #ff9900; }
120.resource-card.critical .level-fill { background: #ff4444; }
121.level-text { font-size: 1.2em; font-weight: bold; margin-bottom: 8px; }
122.r-actions { display: flex; gap: 5px; justify-content: center; }
123.btn {
124 background: #00b4d8;
125 color: #0a0e27;
126 border: none;
127 padding: 5px 12px;
128 cursor: pointer;
129 font-family: monospace;
130}
131.btn-green { background: #00ff88; }
132.critical-alert {
133 padding: 10px;
134 background: rgba(255, 68, 68, 0.2);
135 border: 2px solid #ff4444;
136 color: #ff4444;
137 text-align: center;
138 margin-bottom: 15px;
139 font-weight: bold;
140}
141.notifications { max-width: 500px; margin: 0 auto; }
142.notif {
143 padding: 8px;
144 margin: 4px 0;
145 border: 1px solid;
146 font-size: 0.9em;
147}
148.notif.info { border-color: #00b4d8; color: #00b4d8; }
149.notif.warning { border-color: #ff9900; color: #ff9900; }
150.notif.danger { border-color: #ff4444; color: #ff4444; }
151.notif.success { border-color: #00ff88; color: #00ff88; }
152</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. Czy store w Pinia może importować i używać innego store?
2. Gdzie najlepiej wywoływać useStore() drugiego store wewnątrz store?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Stwórz tasks store używający auth store - fetchTasks sprawdza isAuthenticated.
- Układanie w pionie
Uporządkuj stores od najbardziej niezależnego do najbardziej zależnego:
- Klikanie w kolejności
Ułóż kod action wywołującej inny store:
- Edytor kodu
Stwórz resource store z alertami - consume generuje alerty przy niskim poziomie.