Kurs Vue.js · Moduł 8: Sloty i komponenty dynamiczne
Dynamic Components
W tej lekcji6
System transportu modułów w NOVA LAB pozwala podłączać różne panele do tego samego gniazda stacji - panel tlenu, panel wody, panel energii. Operator przełącza je jednym przyciskiem. Mógłbyś napisać łańcuch v-if i v-else-if z osobnym warunkiem dla każdego panelu, ale każdy nowy panel oznaczałby edycję szablonu, a łańcuch rósłby bez końca. Vue ma na to mechanizm dynamic components - specjalny element <component> z atrybutem :is, który renderuje ten komponent, na który akurat wskazujesz.
Podstawowa składnia
Implementacja przebiega w czterech krokach: importujesz wszystkie panele, tworzysz zmienną wskazującą aktywny panel, renderujesz ją przez <component :is> i dodajesz przyciski, które tę zmienną zmieniają. Do przechowania komponentu użyjemy shallowRef, czyli refa reagującego tylko na podmianę całej wartości:
1<script setup>
2import { ref, shallowRef } from 'vue'
3import OxygenPanel from './OxygenPanel.vue'
4import WaterPanel from './WaterPanel.vue'
5import SolarPanel from './SolarPanel.vue'
6
7const tabs = { OxygenPanel, WaterPanel, SolarPanel }
8const currentTab = shallowRef(OxygenPanel)
9</script>
10
11<template>
12 <div class="station">
13 <nav>
14 <button
15 v-for="(comp, name) in tabs"
16 :key="name"
17 :class="{ active: currentTab === comp }"
18 @click="currentTab = comp"
19 >
20 {{ name }}
21 </button>
22 </nav>
23
24 <component :is="currentTab" />
25 </div>
26</template>Pętla v-for po obiekcie tabs daje wartość, czyli komponent, i klucz, czyli jego nazwę na przycisku. Kliknięcie podmienia currentTab, a <component> renderuje nowy panel w tym samym miejscu; klasa active podświetla aktywny przycisk. Używamy shallowRef zamiast ref, bo nie potrzebujemy głębokiej reaktywności na obiekcie komponentu - Vue ostrzegłby nawet, że niepotrzebnie robi z komponentu obiekt reaktywny.
Co przyjmuje :is
Do :is możesz przekazać zaimportowany obiekt komponentu, jak wyżej, albo napis: nazwę komponentu zarejestrowanego globalnie przez app.component() lub nazwę zwykłego znacznika HTML. W poniższym przykładzie zmienna isLink decyduje, czy powstanie link, czy przycisk:
1<template>
2 <!-- Zaimportowany komponent -->
3 <component :is="OxygenPanel" />
4
5 <!-- Nazwa komponentu zarejestrowanego globalnie -->
6 <component is="WaterPanel" />
7
8 <!-- Zwykły znacznik HTML -->
9 <component :is="isLink ? 'a' : 'button'">Open airlock</component>
10</template>W <script setup> zaimportowane komponenty są zmiennymi, a nie zarejestrowanymi nazwami, dlatego tam zawsze przekazuj obiekt przez :is. Napis zadziała tylko dla komponentów zarejestrowanych globalnie.
KeepAlive
Domyślnie przełączenie komponentu niszczy stary i tworzy nowy. Kolejność hooków jest przewidywalna: stary panel dostaje onBeforeUnmount, potem onUnmounted, a nowy onMounted - od zera, więc przesunięty suwak czy wpisany tekst przepadają. Jeśli chcesz zachować stan, opakuj komponent w <KeepAlive>:
1<template>
2 <KeepAlive>
3 <component :is="currentTab" />
4 </KeepAlive>
5</template>Teraz komponent jest cachowany - wrócisz do niego z zachowanym stanem. Element panelu znika z dokumentu, ale instancja z całym stanem czeka w pamięci. Sam <KeepAlive> nie renderuje żadnego elementu w DOM.
KeepAlive - include i exclude
Nie każdy panel warto trzymać w pamięci. Atrybuty include i exclude porównują nazwę komponentu z listą, a max ogranicza rozmiar cache:
1<template>
2 <!-- Tylko te komponenty -->
3 <KeepAlive :include="['OxygenPanel', 'WaterPanel']">
4 <component :is="currentTab" />
5 </KeepAlive>
6
7 <!-- Wszystkie oprócz tych -->
8 <KeepAlive :exclude="['HeavyModule']">
9 <component :is="currentTab" />
10 </KeepAlive>
11
12 <!-- Limit cache (LRU) -->
13 <KeepAlive :max="5">
14 <component :is="currentTab" />
15 </KeepAlive>
16</template>Skąd komponent ma nazwę? Plik z <script setup> dostaje ją automatycznie od nazwy pliku (od Vue 3.2.34), więc OxygenPanel.vue nazywa się OxygenPanel. Lista może być też napisem z przecinkami albo wyrażeniem regularnym. Przy max cache działa jak LRU: gdy zabraknie miejsca, Vue niszczy instancję najdawniej używaną. Panel spoza include zachowuje się jak bez KeepAlive.
Lifecycle hooks z KeepAlive
Cachowane komponenty mają dwa dodatkowe hooki, bo zamiast niszczenia są tylko odpinane:
1<script setup>
2import { onActivated, onDeactivated } from 'vue'
3
4onActivated(() => {
5 console.log('Panel aktywowany (widoczny)')
6 // Odśwież dane, wznów timery
7})
8
9onDeactivated(() => {
10 console.log('Panel deaktywowany (ukryty)')
11 // Zatrzymaj timery, zapisz stan
12})
13</script>Przy pierwszym renderze panel dostaje onMounted, a zaraz po nim onActivated, bo ten hook działa także przy montowaniu. Przy przełączeniu stary panel dostaje onDeactivated zamiast onUnmounted, a nowy onActivated. Po powrocie nie ma już onMounted - tylko onActivated.
Props na dynamicznych komponentach
Możesz przekazywać props i nasłuchiwać zdarzeń jak przy zwykłym komponencie; trafią do tego panelu, który jest akurat wyrenderowany:
1<template>
2 <component
3 :is="currentTab"
4 :station="stationData"
5 @alert="handleAlert"
6 />
7</template>Kiedy więc <component :is>, a kiedy v-if? Dwa stałe warianty spokojnie obsłuży v-if. Gdy komponentów jest wiele i chcesz przełączać się między nimi dynamicznie, <component :is> daje czystszy szablon.
Moja rada: trzymaj panele w tablicy obiektów { name, component } - przyciski wygenerujesz jednym v-for, a aktywny podświetlisz porównaniem currentTab === tab.component. W edytorze poniżej tak właśnie zbudowane są trzy panele, a KeepAlive pilnuje, by suwak poziomu tlenu nie wracał do wartości startowej.
Zapamiętaj: <component :is> to uniwersalne gniazdo stacji - wpinasz dowolny panel, a KeepAlive zachowuje jego stan, gdy chwilowo go odpinasz.
Kod do tej lekcji: App.vue
1<script setup>
2import { shallowRef } from 'vue'
3import OxygenPanel from './OxygenPanel.vue'
4import WaterPanel from './WaterPanel.vue'
5import SolarPanel from './SolarPanel.vue'
6
7const tabs = [
8 { name: 'Tlen', component: OxygenPanel },
9 { name: 'Woda', component: WaterPanel },
10 { name: 'Energia', component: SolarPanel }
11]
12
13const currentTab = shallowRef(OxygenPanel)
14</script>
15
16<template>
17 <div class="nova-lab">
18 <h1>Dynamic Components</h1>
19
20 <nav class="tabs">
21 <button
22 v-for="tab in tabs"
23 :key="tab.name"
24 :class="{ active: currentTab === tab.component }"
25 @click="currentTab = tab.component"
26 >
27 {{ tab.name }}
28 </button>
29 </nav>
30
31 <KeepAlive>
32 <component :is="currentTab" />
33 </KeepAlive>
34 </div>
35</template>
36
37<style scoped>
38.nova-lab {
39 background: #0a0e27;
40 color: #00ff88;
41 min-height: 100vh;
42 padding: 20px;
43 font-family: monospace;
44}
45h1 { color: #00ff88; margin-bottom: 20px; }
46.tabs {
47 display: flex;
48 gap: 5px;
49 margin-bottom: 15px;
50}
51.tabs button {
52 background: rgba(0, 180, 216, 0.1);
53 border: 2px solid #00b4d8;
54 color: #00b4d8;
55 padding: 10px 20px;
56 cursor: pointer;
57 font-family: monospace;
58 font-weight: bold;
59}
60.tabs button.active {
61 background: #00b4d8;
62 color: #0a0e27;
63}
64</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. Jak wyrenderować dynamiczny komponent w Vue?
2. Co można przekazać do atrybutu :is w <component>?
Zadania praktyczne w grze
- Edytor kodu
Stwórz system tabów z <component :is> przełączający między trzema komponentami: OxygenPanel, WaterPanel, SolarPanel.
- Układanie w pionie
Uporządkuj kroki implementacji dynamicznego komponentu:
- Klikanie w kolejności
Ułóż kod dynamicznego komponentu z przyciskami:
- Edytor kodu
Stwórz komponent z listą tabów w tablicy i renderuj aktywny komponent przez <component :is>. Podświetl aktywny tab.