Kurs Vue.js · Moduł 8: Sloty i komponenty dynamiczne

Dynamic Components

4 min czytania
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. 1. Jak wyrenderować dynamiczny komponent w Vue?

  2. 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.

Przydatne artykuły