Kurs Vue.js · Moduł 7: Props i zdarzenia
Projekt: System Komunikacji NOVA LAB
W tej lekcji4
Finalny projekt Stacji Komunikacji! Czas zastosować wszystkie wzorce komunikacji między komponentami w jednym spójnym projekcie. Zbudujesz system zarządzania modułami stacji NOVA LAB, który łączy wszystkie poznane techniki.
Do tej pory każdy kanał ćwiczyłeś osobno, na dwóch komponentach. Prawdziwy panel stacji to kilka komponentów naraz, a razem z nimi prawdziwe pytania: kto jest właścicielem listy modułów, kto może zmienić status generatora i skąd pasek stanu ma wiedzieć, że filtr wody przeszedł w tryb offline. W tym projekcie odpowiesz na nie, czytając działający system.
Podsumowanie modułu
W tym module poznałeś kompletny zestaw narzędzi do komunikacji między komponentami Vue:
- Props - jednokierunkowe przekazywanie danych z rodzica do dziecka, z typowaniem, walidacją i wartościami domyślnymi. Definiowane przez
defineProps() - Events (Emit) - komunikacja dziecko-rodzic przez emitowanie zdarzeń z opcjonalnym payloadem. Definiowane przez
defineEmits()z walidacją argumentów - v-model na komponentach - dwukierunkowe wiązanie danych z komponentem, wewnętrznie oparte na props
modelValuei emitupdate:modelValue, od Vue 3.4 zapisywane krócej przezdefineModel(). Możliwość wielu v-model z nazwanymi bindings - Provide/Inject - przekazywanie danych przez wiele poziomów drzewa komponentów bez "prop drilling". Wzorzec readonly + actions dla bezpiecznej modyfikacji stanu
- Fallthrough Attributes - automatyczne dziedziczenie atrybutów HTML, kontrolowane przez
inheritAttrsi$attrs - Zaawansowane wzorce - łączenie props, events, provide/inject i v-model w kompleksowych architekturach oraz
defineExposejako złącze serwisowe komponentu
Architektura projektu
Zanim otworzysz pliki, spójrz na mapę połączeń. Każdy wpis w nawiasie mówi, którym kanałem dany komponent dostaje albo wysyła dane:
1App (provide: modules, actions)
2 ├── StatusBar (inject: modules → computed online count)
3 └── ControlPanel (inject: modules)
4 └── ModuleCard[] (props: module, emit: status-change)App jest jedynym właścicielem stanu. StatusBar i ControlPanel stoją obok siebie jako dzieci App i oba wstrzykują listę modułów, a ModuleCard dostaje swój moduł przez props od ControlPanel.
Zadanie
Przeanalizuj kod w edytorze i zrozum jak wszystkie wzorce komunikacji współpracują:
- App.vue - Provider z centralnym stanem modułów w
refi akcjami (provide readonly state + action functions) - StatusBar.vue - Inject stanu modułów + computed, który liczy moduły online na tle wszystkich
- ControlPanel.vue - Inject modułów + renderowanie kart za pomocą v-for, przekazywanie danych przez props
- ModuleCard.vue - Przyjmuje dane przez props, emituje zmiany statusu do rodzica, używa inject do wywoływania akcji
Zacznij od nadajnika. App udostępnia listę modułów tylko do odczytu, a obok niej akcje, które są jedynymi drogami zmiany stanu:
1provide('modules', readonly(modules))
2provide('selectedId', selectedId)
3provide('selectModule', (id) => { selectedId.value = id })
4provide('updateStatus', (id, status) => {
5 const m = modules.value.find(m => m.id === id)
6 if (m) m.status = status
7})Akcja updateStatus zmienia status wewnątrz App, więc StatusBar przelicza swój computed sam, bez żadnego dodatkowego sygnału. Zauważ jeden wyjątek od reguły: selectedId trafia do potomków bez readonly.
Teraz odbiornik na drugim końcu łącza. ModuleCard łączy dwa kanały w jednej funkcji:
1const toggle = () => {
2 const newStatus = props.module.status === 'online' ? 'offline' : 'online'
3 updateStatus(props.module.id, newStatus)
4 emit('status-change', props.module.id, newStatus)
5}Karta niczego nie przypisuje do props.module - prosi App o zmianę przez wstrzykniętą akcję, a swojemu rodzicowi, czyli ControlPanel, wysyła powiadomienie zdarzeniem. W szablonie przycisk ma modyfikator @click.stop, żeby przełączenie statusu nie zaznaczało przy okazji całej karty.
Kluczowe obserwacje:
- Stan jest provide z App i inject w StatusBar oraz ControlPanel - żaden z nich nie dostaje listy przez props
- Akcje
selectModuleiupdateStatustrafiają z App prosto do ModuleCard - przeskakują poziom ControlPanel - ControlPanel przekazuje moduły przez props do ModuleCard - klasyczny wzorzec parent-child
- ModuleCard używa inject do akcji i emit do powiadomień - dwa różne kanały komunikacji
- Wzorzec readonly + actions chroni listę modułów przed bezpośrednią modyfikacją - tylko App może zmieniać dane
Rozbuduj system
Trzy zadania na koniec. Po pierwsze, opakuj selectedId w readonly i sprawdź, że zaznaczanie kart dalej działa, bo idzie przez akcję selectModule. Po drugie, handler @status-change w ControlPanel jest pusty - dopisz dziennik zmian wyświetlany pod kartami. Po trzecie, dodaj do ModuleCard pole notatki technika obsługiwane przez v-model albo walidację propsa module. Rób je po kolei i przy każdym kroku zadaj sobie pytanie, który komponent jest właścicielem zmienianych danych. W kolejnej lokacji, Magazynie Modułów, dołożysz do tego sloty i komponenty dynamiczne.
Zapamiętaj: w dobrze zaprojektowanej stacji każda dana ma jednego właściciela, a pozostałe moduły słuchają jego sygnałów i proszą go o zmiany.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, provide, readonly } from 'vue'
3import ControlPanel from './ControlPanel.vue'
4import StatusBar from './StatusBar.vue'
5
6// Centralny stan stacji
7const modules = ref([
8 { id: 1, name: 'Generator tlenu', status: 'online', temp: -12 },
9 { id: 2, name: 'Odzysk wody', status: 'online', temp: 22 },
10 { id: 3, name: 'Panele słoneczne', status: 'serwis', temp: 85 }
11])
12const selectedId = ref(null)
13
14// Provide: globalny stan
15provide('modules', readonly(modules))
16provide('selectedId', selectedId)
17provide('selectModule', (id) => { selectedId.value = id })
18provide('updateStatus', (id, status) => {
19 const m = modules.value.find(m => m.id === id)
20 if (m) m.status = status
21})
22</script>
23
24<template>
25 <div class="nova-lab">
26 <h1>System komunikacji NOVA LAB</h1>
27 <StatusBar />
28 <ControlPanel />
29 </div>
30</template>
31
32<style scoped>
33.nova-lab {
34 background: #0a0e27;
35 color: #00ff88;
36 min-height: 100vh;
37 padding: 20px;
38 font-family: monospace;
39}
40h1 { color: #00ff88; margin-bottom: 20px; }
41</style>Widzisz błąd w tej lekcji?