Kurs Vue.js · Moduł 8: Sloty i komponenty dynamiczne
Async Components & Suspense
W tej lekcji5
W NOVA LAB ciężkie moduły cargo nie są ładowane od razu - system ładuje je na żądanie, wyświetlając status na ekranie. Pomyśl o panelu z trójwymiarową mapą kraterów: jeśli jego kod trafi do głównej paczki aplikacji, każdy członek załogi zapłaci za niego czasem startu, nawet jeśli mapy nigdy nie otworzy. Vue oferuje async components - ładowanie komponentów lazy (na żądanie) i Suspense - kontrolę nad stanem ładowania.
defineAsyncComponent
Zamiast importować komponent bezpośrednio, możesz go załadować asynchronicznie. Funkcja defineAsyncComponent przyjmuje funkcję ładującą, która zwraca Promise z komponentem - najczęściej dynamiczny import(), dzięki któremu bundler, na przykład Vite, wydziela komponent do osobnego pliku:
1<script setup>
2import { defineAsyncComponent } from 'vue'
3
4// Prosty lazy import
5const HeavyModule = defineAsyncComponent(
6 () => import('./HeavyModule.vue')
7)
8</script>
9
10<template>
11 <HeavyModule />
12</template>Komponent jest pobierany dopiero wtedy, gdy jest potrzebny, czyli przy pierwszym renderowaniu w template. Dla szablonu HeavyModule wygląda jak zwykły komponent, przyjmuje props i emituje zdarzenia - zmienił się tylko moment pobrania kodu.
Zaawansowana konfiguracja
Zamiast samej funkcji możesz przekazać obiekt opcji. Oprócz loader określasz w nim, co pokazać w trakcie ładowania, po jakim czasie to zrobić i co wyświetlić, gdy transport modułu się nie powiedzie:
1<script setup>
2import { defineAsyncComponent } from 'vue'
3import LoadingSpinner from './LoadingSpinner.vue'
4import ErrorDisplay from './ErrorDisplay.vue'
5
6const HeavyModule = defineAsyncComponent({
7 // Funkcja ładująca
8 loader: () => import('./HeavyModule.vue'),
9
10 // Komponent wyświetlany podczas ładowania
11 loadingComponent: LoadingSpinner,
12
13 // Opóźnienie przed wyświetleniem loading (ms)
14 delay: 200,
15
16 // Komponent wyświetlany gdy ładowanie się nie powiedzie
17 errorComponent: ErrorDisplay,
18
19 // Timeout - po tym czasie wyświetli error (ms)
20 timeout: 10000
21})
22</script>Kolejność zdarzeń wygląda tak: Vue wywołuje loader, odczekuje delay, a jeśli moduł wciąż nie dotarł, pokazuje loadingComponent. Gdy import się zakończy, spinner zostaje zastąpiony właściwym komponentem. Wartość 200 ms to zresztą domyślne delay - na szybkim łączu spinner mignąłby tylko na ułamek sekundy. Domyślny timeout jest nieskończony, więc bez tej opcji errorComponent pojawi się tylko przy błędzie importu.
Suspense
<Suspense> to wbudowany komponent Vue do zarządzania stanem ładowania całego drzewa asynchronicznych komponentów naraz. Uwaga: w Vue 3.5 wciąż jest oznaczony jako funkcja eksperymentalna - jego API może się jeszcze zmienić, a Vue przypomina o tym komunikatem w konsoli. Ma dwa sloty, #default i #fallback:
1<template>
2 <Suspense>
3 <!-- Treść główna (ładowany komponent) -->
4 <template #default>
5 <HeavyModule />
6 </template>
7
8 <!-- Loading state -->
9 <template #fallback>
10 <div class="loading">
11 <p>Loading module...</p>
12 <div class="spinner"></div>
13 </div>
14 </template>
15 </Suspense>
16</template>Przy pierwszym renderze Suspense renderuje treść #default w pamięci. Wtedy async komponent rozpoczyna ładowanie, a Suspense przechodzi w stan oczekiwania i wyświetla #fallback. Gdy wszystkie asynchroniczne zależności się zakończą, przełącza się na #default. Ważny szczegół: wewnątrz Suspense własne opcje loadingComponent, errorComponent, delay i timeout komponentu asynchronicznego są ignorowane, bo stanem ładowania steruje Suspense. Jeśli komponent ma zachować własny spinner, dodaj mu opcję suspensible: false.
Suspense events
Suspense emituje zdarzenia informujące o stanie ładowania: pending przy wejściu w stan oczekiwania, fallback przy wyświetleniu treści zastępczej i resolve, gdy treść główna jest gotowa:
1<template>
2 <Suspense
3 @pending="onPending"
4 @resolve="onResolve"
5 @fallback="onFallback"
6 >
7 <template #default>
8 <AsyncModule />
9 </template>
10 <template #fallback>
11 <LoadingSpinner />
12 </template>
13 </Suspense>
14</template>
15
16<script setup>
17const onPending = () => console.log('Loading started...')
18const onResolve = () => console.log('Loading complete!')
19const onFallback = () => console.log('Fallback displayed')
20</script>Przy pierwszym ładowaniu konsola pokaże kolejno "Loading started...", "Fallback displayed" i "Loading complete!". To dobre miejsce na pomiar czasu ładowania albo wpis do dziennika stacji.
Async setup w komponentach
Komponent z async setup lub top-level await w <script setup> automatycznie staje się asynchroniczną zależnością dla Suspense:
1<!-- AsyncModule.vue -->
2<script setup>
3const data = await fetch('/api/module-data')
4 .then(r => r.json())
5</script>
6
7<template>
8 <div>{{ data.name }}</div>
9</template>Taki komponent musi mieć nad sobą <Suspense>. Bez niego Vue wypisze ostrzeżenie i nie wyrenderuje go wcale. Suspense nie obsługuje też błędów samodzielnie - odrzucony fetch przechwycisz w komponencie nadrzędnym hookiem onErrorCaptured.
Moja rada: w kodzie produkcyjnym zacznij od defineAsyncComponent z loadingComponent i errorComponent, bo to stabilne API. Suspense stosuj tam, gdzie kilka komponentów ładuje się naraz i chcesz jednego wspólnego ekranu oczekiwania, pamiętając o jego eksperymentalnym statusie. W edytorze poniżej przycisk ładuje HeavyModule z symulowanym dwusekundowym opóźnieniem, a po 200 ms pojawia się LoadingSpinner.
Zapamiętaj: ciężki moduł dolatuje na stację dopiero wtedy, gdy ktoś go potrzebuje, a Suspense trzyma ekran oczekiwania, dopóki nie dotrze cały ładunek.
Kod do tej lekcji: App.vue
1<script setup>
2import { defineAsyncComponent, ref } from 'vue'
3import LoadingSpinner from './LoadingSpinner.vue'
4
5// Symulacja async component (w prawdziwej apce: import('./Heavy.vue'))
6const HeavyModule = defineAsyncComponent({
7 loader: () => new Promise((resolve) => {
8 setTimeout(() => {
9 resolve({
10 template: `
11 <div class="heavy-module">
12 <h2>Ciężki moduł załadowany!</h2>
13 <p>Złożona wizualizacja danych gotowa</p>
14 <div class="chart">
15 <div v-for="h in [80, 60, 90, 45, 75]" :key="h"
16 class="bar" :style="{ height: h + 'px' }">
17 </div>
18 </div>
19 </div>
20 `
21 })
22 }, 2000)
23 }),
24 loadingComponent: LoadingSpinner,
25 delay: 200
26})
27
28const showModule = ref(false)
29</script>
30
31<template>
32 <div class="nova-lab">
33 <h1>Async Components & Suspense</h1>
34
35 <button @click="showModule = !showModule" class="btn">
36 {{ showModule ? 'Ukryj' : 'Załaduj' }} ciężki moduł
37 </button>
38
39 <div v-if="showModule" class="container">
40 <HeavyModule />
41 </div>
42 </div>
43</template>
44
45<style scoped>
46.nova-lab {
47 background: #0a0e27;
48 color: #00ff88;
49 min-height: 100vh;
50 padding: 20px;
51 font-family: monospace;
52}
53h1 { color: #00ff88; margin-bottom: 20px; }
54.btn {
55 background: #00b4d8;
56 color: #0a0e27;
57 border: none;
58 padding: 12px 24px;
59 cursor: pointer;
60 font-family: monospace;
61 font-weight: bold;
62 font-size: 14px;
63}
64.container {
65 margin-top: 20px;
66 border: 2px solid #00b4d8;
67 padding: 20px;
68}
69</style>
70
71<style>
72.heavy-module h2 { color: #00ff88; margin-bottom: 10px; }
73.heavy-module .chart {
74 display: flex;
75 align-items: flex-end;
76 gap: 8px;
77 margin-top: 15px;
78 height: 100px;
79}
80.heavy-module .bar {
81 width: 30px;
82 background: #00b4d8;
83}
84</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 defineAsyncComponent w Vue?
2. Który slot w <Suspense> wyświetla treść podczas ładowania?
Zadania praktyczne w grze
- Układanie w pionie
Uporządkuj etapy działania Suspense:
- Edytor kodu
Użyj defineAsyncComponent do lazy loadingu komponentu HeavyModule. Wyświetl 'Loading...' podczas ładowania.
- Układanie w poziomie
Ułóż kod definicji async komponentu:
- Klikanie w kolejności
Ułóż kod Suspense z async komponentem:
- Edytor kodu
Użyj <Suspense> z #default i #fallback slotami do wyświetlenia async komponentu z loading spinnerem.