Kurs Vue.js · Moduł 6: Komponenty

Composables - Wielokrotne Użycie Logiki

6 min czytania
W tej lekcji7

Dr. Nova wyjaśnia: "W NOVA LAB wiele modułów potrzebuje tych samych systemów - timer misji, odczyty sensorów, połączenie z bazą danych. Zamiast kopiować ten sam kod do każdego modułu, tworzymy współdzielone systemy. W Vue takim systemem jest composable."

Pięć paneli stacji pokazuje czas misji. Jeśli każdy ma własną kopię kodu z setInterval, onMounted i onUnmounted, to poprawka błędu w liczeniu czasu oznacza edycję pięciu plików, a przeoczenie jednego - pięć różnych zegarów na jednej stacji.

Czym Jest Composable?

Composable to funkcja, która enkapsuluje i zwraca reaktywny stan oraz logikę. Pozwala współdzielić logikę między wieloma komponentami bez duplikowania kodu. Dokumentacja Vue opisuje go jako funkcję korzystającą z Composition API, czyli z ref, computed albo hooków cyklu życia.

Konwencja nazewnictwa: composable zawsze zaczyna się od use - np. useTimer, useCounter, useMarsData. Najprostszy przykład to licznik złożony z kontenera, trzech funkcji i obiektu, który je zwraca:

1// composables/useCounter.js
2import { ref } from 'vue'
3
4export function useCounter(initialValue = 0) {
5  const count = ref(initialValue)
6
7  function increment() {
8    count.value++
9  }
10
11  function decrement() {
12    count.value--
13  }
14
15  function reset() {
16    count.value = initialValue
17  }
18
19  // Zwracamy reaktywny stan i funkcje
20  return { count, increment, decrement, reset }
21}

To zwykła funkcja JavaScript eksportowana z pliku .js - bez szablonu i bez stylów. Zwraca zwykły obiekt z kontenerami i funkcjami, więc komponent może go rozpakować, a count pozostanie reaktywny. Parametr initialValue = 0 ma wartość domyślną, więc useCounter() bez argumentu zacznie od zera.

Używanie Composable w Komponencie

W komponencie importujesz funkcję i wywołujesz ją w <script setup>, a zwrócone wartości rozpakowujesz destrukturyzacją:

1<template>
2  <div style="background: #0a0e27; color: #00ff88; padding: 1rem;">
3    <h3>Licznik modułów: {{ count }}</h3>
4    <button @click="increment">+1</button>
5    <button @click="decrement">-1</button>
6    <button @click="reset">Reset</button>
7  </div>
8</template>
9
10<script setup>
11import { useCounter } from './composables/useCounter'
12
13// Destrukturyzacja zwróconych wartości
14const { count, increment, decrement, reset } = useCounter(10)
15</script>

Każdy komponent, który użyje useCounter(), dostaje własną, niezależną instancję stanu. Zmiana count w jednym komponencie nie wpływa na inny. Wywołuj composables synchronicznie, na najwyższym poziomie <script setup>, a nie na przykład w funkcji obsługi kliknięcia - tylko wtedy Vue powiąże je z komponentem.

Organizacja Plików

Composables przechowujemy w dedykowanym folderze, oddzielnie od komponentów, które z nich korzystają:

1src/
2├── composables/
3│   ├── useTimer.js
4│   ├── useCounter.js
5│   ├── useFetch.js
6│   └── useMarsData.js
7├── components/
8│   ├── StatusBar.vue
9│   └── ModuleCard.vue
10└── App.vue

Najczęściej jest to src/composables/ - tak nazywa ten katalog również Nuxt. Czasem spotkasz src/hooks/, nazwę pożyczoną z Reacta. Vue niczego tu nie narzuca, ważne tylko, żeby cały projekt trzymał się jednej konwencji.

Praktyczny Composable: useTimer

Timer misji to typowy composable używany w wielu komponentach stacji, bo łączy stan, wartość wyliczaną i hooki cyklu życia:

1// composables/useTimer.js
2import { ref, computed, onMounted, onUnmounted } from 'vue'
3
4export function useTimer() {
5  const seconds = ref(0)
6  let intervalId = null
7
8  const formatted = computed(() => {
9    const h = String(Math.floor(seconds.value / 3600)).padStart(2, '0')
10    const m = String(Math.floor((seconds.value % 3600) / 60)).padStart(2, '0')
11    const s = String(seconds.value % 60).padStart(2, '0')
12    return `${h}:${m}:${s}`
13  })
14
15  function start() {
16    intervalId = setInterval(() => {
17      seconds.value++
18    }, 1000)
19  }
20
21  function stop() {
22    if (intervalId) {
23      clearInterval(intervalId)
24      intervalId = null
25    }
26  }
27
28  // Automatyczne zarządzanie cyklem życia
29  onMounted(() => start())
30  onUnmounted(() => stop())
31
32  return { seconds, formatted, start, stop }
33}

Hooki wywołane w composable podpinają się pod komponent, który wywołał useTimer(), więc zegar startuje razem z nim i zatrzymuje się przy jego demontażu. Metoda padStart(2, '0') dopisuje zero z przodu, żeby 7 sekund wyglądało jak 07. Zwróć uwagę na import: computed trzeba sprowadzić z vue tak samo jak ref, inaczej kod przerwie się błędem. Komponent, który potrzebuje zegara, bierze z funkcji tylko to, czego potrzebuje:

1<!-- Użycie w dowolnym komponencie -->
2<template>
3  <p>Czas misji: {{ formatted }}</p>
4</template>
5
6<script setup>
7import { useTimer } from './composables/useTimer'
8
9const { formatted } = useTimer()
10</script>

Komponent nie wie nic o setInterval ani o sprzątaniu - dostaje gotowy, sformatowany czas.

Composable: useFetch

Pobieranie danych to kolejny typowy wzorzec: trzy kontenery opisują stan zapytania, a jedna funkcja je wykonuje:

1// composables/useFetch.js
2import { ref } from 'vue'
3
4export function useFetch(url) {
5  const data = ref(null)
6  const error = ref(null)
7  const loading = ref(false)
8
9  async function fetchData() {
10    loading.value = true
11    error.value = null
12
13    try {
14      const response = await fetch(url)
15      data.value = await response.json()
16    } catch (err) {
17      error.value = err.message
18    } finally {
19      loading.value = false
20    }
21  }
22
23  return { data, error, loading, fetchData }
24}

Kontener loading ma wartość true przez cały czas zapytania, error przechowuje komunikat błędu, a blok finally wyłącza ładowanie niezależnie od wyniku. Samo wywołanie useFetch() niczego nie pobiera - robi to dopiero fetchData(). W komponencie wywołujemy ją w onMounted, a szablon wybiera jeden z trzech stanów dyrektywami v-if, v-else-if i v-else:

1<template>
2  <div>
3    <p v-if="loading">Ładowanie danych stacji...</p>
4    <p v-else-if="error">Błąd: {{ error }}</p>
5    <ul v-else-if="data">
6      <li v-for="module in data" :key="module.id">{{ module.name }}</li>
7    </ul>
8  </div>
9</template>
10
11<script setup>
12import { onMounted } from 'vue'
13import { useFetch } from './composables/useFetch'
14
15const { data, error, loading, fetchData } = useFetch('/api/modules')
16
17onMounted(() => fetchData())
18</script>

Ten sam useFetch obsłuży listę modułów, pogodę na Marsie czy raport załogi - zmienia się tylko adres.

Dlaczego Composables?

Bez composablesZ composables
Kopiujesz ten sam kod timera do 5 komponentówJeden useTimer(), importowany wszędzie
Zmiana logiki = edycja 5 plikówZmiana w jednym pliku
Trudne testowanie (logika w komponentach)Łatwiejsze testowanie (logika w osobnych funkcjach)
Komponenty pełne powtarzającego się koduKomponenty skupione na wyświetlaniu

Composable korzystający tylko z ref i computed przetestujesz, wywołując go wprost w teście, a ten z hookami cyklu życia potrzebuje komponentu-gospodarza - wrócimy do tego w Laboratorium Naukowym.

Plan Budowy Stacji

W NOVA LAB aplikację z komponentów budujemy w stałej kolejności: planujemy strukturę komponentów, tworzymy komponenty bazowe, takie jak przycisk czy karta, wydzielamy logikę do composables, składamy komponenty w widoki, a na końcu dopinamy hooki cyklu życia i style. Ten porządek polecam Ci bo każdy krok opiera się na poprzednim. W edytorze poniżej komponent MarsStation korzysta z useMarsData i useTimer - otwórz ich pliki i odszukaj wzorce z tej lekcji.

Zapamiętaj: composable to wspólny system stacji - funkcja use..., która zwraca reaktywny stan i logikę, a każdy moduł podłącza się do niej niezależnie.

Kod do tej lekcji: App.vue
1<script setup>
2import { ref } from 'vue'
3import MarsStation from './MarsStation.vue'
4
5const stationName = ref('NOVA LAB Alpha')
6</script>
7
8<template>
9  <div class="composables-lab">
10    <div class="header">
11      <h1>Composables - NOVA LAB</h1>
12      <p class="subtitle">Wielokrotne uzycie logiki z composables</p>
13    </div>
14
15    <div class="info-panel">
16      <h3>Czym sa composables?</h3>
17      <p><strong>Composable</strong> to funkcja zaczynajaca sie od <strong>use</strong> (np. useMarsData)</p>
18      <p>Kapsulkuje reaktywny stan (ref) i logike, ktora mozna wspoldzielic miedzy komponentami</p>
19      <p>Struktura: export function useXxx() { const state = ref(...); return { state } }</p>
20    </div>
21
22    <MarsStation :station-name="stationName" />
23  </div>
24</template>
25
26<style scoped>
27.composables-lab {
28  background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
29  min-height: 100vh;
30  padding: 2rem;
31  color: #ffffff;
32  font-family: 'Courier New', monospace;
33}
34
35.header {
36  text-align: center;
37  margin-bottom: 2rem;
38  padding-bottom: 1rem;
39  border-bottom: 2px solid #00ff88;
40}
41
42.header h1 {
43  font-size: 2rem;
44  color: #00ff88;
45  text-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
46  margin: 0;
47}
48
49.subtitle {
50  color: #00b4d8;
51  font-size: 1.1rem;
52  margin-top: 0.5rem;
53}
54
55.info-panel {
56  background: rgba(0, 180, 216, 0.1);
57  border: 2px solid #00b4d8;
58  border-radius: 10px;
59  padding: 1.5rem;
60  margin-bottom: 2rem;
61}
62
63.info-panel h3 {
64  color: #00b4d8;
65  margin-top: 0;
66}
67
68.info-panel strong {
69  color: #00ff88;
70}
71</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. Co to jest composable w Vue 3?

  2. 2. Jak powinna nazywać się composable zgodnie z konwencją Vue?

To 2 z 8 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Utwórz composable useMarsWeather z reaktywnymi danymi i funkcją fetch.

  • Klikanie w kolejności

    Ułóż elementy struktury composable w kolejności:

  • Układanie w pionie

    Ułóż kroki użycia composable w komponencie:

  • Edytor kodu

    Importuj i użyj composable useMarsWeather w komponencie WeatherPanel.vue.

  • Układanie w pionie

    Ułóż etapy tworzenia i testowania composable od pliku do użycia:

  • Klikanie w kolejności

    Ułóż kroki od stworzenia do użycia composable:

  • Edytor kodu

    Stwórz MarsColony z zagnieżdżonymi komponentami HabitatModule, LabModule i StorageModule.

  • Układanie w pionie

    Ułóż proces od stworzenia do wyświetlenia komponentu:

  • Układanie w pionie

    Ułóż hooki w kolejności wywołania przy montowaniu komponentu:

  • Edytor kodu

    Stwórz zestaw komponentów: MarsButton i MarsCard z odpowiednimi stylami.

  • Klikanie w kolejności

    Ułóż składnię użycia v-bind() w sekcji style:

  • Układanie w pionie

    Ułóż kroki dodania lifecycle hooka do komponentu:

  • Układanie w poziomie

    Ułóż składnię użycia klasy CSS Module w template:

  • Edytor kodu

    Użyj composable useMarsData z onMounted do inicjalizacji danych w komponencie Dashboard.

  • Układanie w pionie

    Ułóż etapy budowania aplikacji z komponentów:

  • Układanie w poziomie

    Ułóż poprawny import ref i onMounted z Vue:

  • Klikanie w kolejności

    Ułóż composable z onMounted:

  • Układanie w pionie

    Ułóż metody stylowania od najszerszego do najwęższego zakresu:

  • Układanie w poziomie

    Ułóż poprawną składnię użycia onMounted:

Przydatne artykuły