Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Struktura aplikacji misji

W poprzedniej lekcji zbudowałeś swój pierwszy działający panel: kontenery

ref()
, wąsy w szablonie, przycisk pod
@click
i klasę zależną od stanu misji. Panel ożył - ale ożył trochę magicznie. Wpisałeś kod, a obraz po prostu pojawił się na ekranie. Nie wiesz jeszcze, kto go tam wstawił ani w którym momencie.

Na stacji badawczej takie "po prostu" nie przechodzi. Inżynier dyżurny musi wiedzieć, który przełącznik odpala system, w jakiej kolejności budzą się moduły i gdzie szukać usterki, gdy ekran panelu pozostaje czarny. Dziś, @name, rozbierzemy sekwencję zapłonu aplikacji Vue na pojedyncze kroki - i po tej lekcji żaden z nich nie będzie już magią.

Otwórz projekt, który wygenerował kreator z lekcji o Vite. Są w nim trzy pliki, których jeszcze ani razu nie tknąłeś:

index.html
w katalogu głównym,
src/main.js
oraz
src/App.vue
. Żaden z nich sam z siebie nie wyświetla niczego. Dopiero we trójkę tworzą łańcuch, który zamienia Twój kod w obraz na monitorze centrum kontroli.

Jedna uwaga porządkowa, zanim ruszymy. Jeśli w kreatorze zgodziłeś się na TypeScript, plik startowy nazywa się

main.ts
, a bez TypeScriptu -
main.js
. To ten sam plik, ta sama rola i ta sama zawartość, różni się wyłącznie rozszerzenie. W tej lekcji piszę o
main.js
, bo tak nazywa się w ćwiczeniach, które dostaniesz zaraz po niej.

index.html - platforma lądowania

Zacznijmy od początku łańcucha, czyli od tego, co jako pierwsze dostaje przeglądarka. Przeglądarka nie potrafi otworzyć pliku

.vue
- to nie jest format, który zna. Umie jedno: wczytać stronę HTML. Dlatego każdy projekt Vue ma dokładnie jeden plik HTML i to on jest pierwszą rzeczą, jaka trafia na ekran.

W środku warto zobaczyć cztery rzeczy. Znacznik

<meta charset="UTF-8">
ustawia kodowanie znaków, żeby polskie ogonki nie zamieniły się w krzaczki. Znacznik
<meta name="viewport">
mówi urządzeniom mobilnym, żeby nie udawały szerokiego ekranu - bez tej linii panel na tablecie w module mieszkalnym wyświetli się jak pomniejszona wersja strony biurkowej. Znacznik
<title>
to napis na karcie przeglądarki. Najważniejsze są jednak dwie ostatnie linie: pusty
<div>
z atrybutem
id
o wartości
app
oraz znacznik
<script>
z atrybutem
type="module"
.

Ten pusty

<div>
to platforma lądowania. Vue nie doklei interfejsu gdziekolwiek - potrzebuje jednego, wyraźnie wskazanego elementu, w którym ma prawo rządzić. Atrybut
type="module"
oznacza z kolei, że wskazany plik jest modułem ES, czyli że wolno w nim używać instrukcji
import
. Bez tego atrybutu przeglądarka zatrzymałaby się na pierwszej linii
main.js
i zgłosiła błąd składni.

1<!DOCTYPE html>
2<html lang="pl">
3  <head>
4    <meta charset="UTF-8" />
5    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6    <title>NOVA LAB - Mission Control</title>
7  </head>
8  <body>
9    <div id="app"></div>
10    <script type="module" src="/src/main.js"></script>
11  </body>
12</html>

Przeczytaj ten plik jeszcze raz i zauważ, czego w nim nie ma. Nie ma ani jednego znacznika Vue, ani jednych wąsów, ani nazwy komponentu. To zwykły HTML, dokładnie taki, jaki pisałeś w pierwszym świecie - Vue niczego tu nie zmienia. Kluczowy

<div>
jest pusty i pusty pozostanie na dysku; treść pojawi się w nim dopiero w przeglądarce, w chwili uruchomienia aplikacji.

Wartość

app
w atrybucie
id
nie jest magiczna. To zwykła nazwa, którą sam wybierasz, i mogłaby brzmieć
control-room
albo
nova
. Musi się natomiast zgadzać z tym, co za moment podasz w
main.js
- i to jedno z najczęstszych źródeł czarnego ekranu, do którego wrócimy pod koniec tej lekcji.

main.js - sekwencja zapłonu

Przeglądarka doszła do znacznika

<script>
i poprosiła o plik
/src/main.js
. W trybie deweloperskim tę prośbę obsługuje Vite: znajduje plik, w razie potrzeby przepuszcza go przez kompilator i odsyła gotowy moduł. To pierwszy fragment Twojego własnego kodu, jaki w ogóle się wykona - wszystko, co widzisz później na ekranie, zaczyna się w tych kilku linijkach.

Cały plik startowy to zwykle cztery, pięć linii. Prześledzimy je po kolei, bo każda robi dokładnie jedną rzecz.

Anatomia linii import

Pierwsze dwie linie sprowadzają narzędzia. Instrukcja

import
ma zawsze ten sam kształt i składa się z czterech części ustawionych w stałej kolejności: słowo
import
, lista nazw do sprowadzenia, słowo
from
, a na końcu źródło zapisane w cudzysłowie. Zapamiętaj tę czwórkę, bo napiszesz ją w każdym pliku Vue, jaki stworzysz.

Różnica między dwiema liniami tkwi w klamrach. Funkcja

createApp
jest jednym z wielu narzędzi udostępnianych przez pakiet
vue
, więc jej nazwę bierzemy w klamry - to tak zwany eksport nazwany, a klamry mówią "wybieram z tej paczki dokładnie to". Plik
.vue
jest inny: udostępnia na zewnątrz jedną jedyną rzecz, czyli swój komponent, i robi to jako eksport domyślny. Eksport domyślny sprowadzasz bez klamer, a nazwę nadajesz mu sam -
App
to tylko przyjęta konwencja, nic tej nazwy nie wymusza.

Zwróć jeszcze uwagę na samo źródło. Zapis

'vue'
bez ukośnika to nazwa pakietu, którego Vite poszuka w katalogu
node_modules
zainstalowanym przez
npm install
. Zapis
'./App.vue'
zaczyna się od kropki z ukośnikiem, więc jest ścieżką względną: szukaj obok mnie, w tym samym katalogu. Rozszerzenie
.vue
piszemy tu jawnie, inaczej narzędzie nie zgadnie, o który plik chodzi.

1import { createApp } from 'vue'
2import App from './App.vue'

Po tych dwóch liniach na ekranie nie zmieniło się absolutnie nic i nie mogło się zmienić. Import to wyłącznie wniesienie narzędzi na pokład: masz teraz pod ręką funkcję

createApp
oraz komponent
App
, ale żadnego z nich jeszcze nie użyłeś. Sam plik
App.vue
również pozostał nietknięty - nie zmieniasz go przez to, że go sprowadzasz.

createApp() - powołanie instancji

Mając narzędzia, możesz powołać do życia aplikację. Robi to funkcja

createApp
, a jej jedynym zadaniem jest zwrócenie instancji aplikacji Vue - jednego obiektu, który jest centrum dowodzenia całego systemu. To on wie, jaki komponent stoi na szczycie, to on przyjmie dodatkowe moduły i to jego poprosisz o uruchomienie.

W nawiasie podajesz komponent główny, czyli ten jeden, od którego zaczyna się cała hierarchia. U nas to

App
. Wszystkie pozostałe komponenty stacji będą później wisiały pod nim, ale
createApp
interesuje wyłącznie ten pierwszy.

Zanim zobaczysz kod, uporządkujmy, czego

createApp
nie robi - bo ta funkcja bywa mylona z trzema zupełnie innymi. Nie tworzy bazy danych: wspólną pamięcią misji zajmuje się Pinia i jej własna funkcja
createPinia
, którą zobaczysz jeszcze w tej lekcji. Nie kompiluje CSS: style opisujesz w sekcji
<style>
komponentu, a na gotowy arkusz zamienia je Vite podczas budowania projektu. Nie tworzy też routingu: nawigacją między ekranami stacji zarządza Vue Router z funkcją
createRouter
, do której wrócimy w Centrum Nawigacji. Wszystkie trzy są osobnymi narzędziami i żadne z nich nie ukrywa się w
createApp
.

1const app = createApp(App)

Kształt tej linii jest identyczny jak przy

ref()
z poprzedniej lekcji: słowo
const
, nazwa, znak równości, wywołanie funkcji z argumentem. Zmienna
app
trzyma teraz instancję aplikacji.

I tu najważniejsza obserwacja całej lekcji: na ekranie nadal nie ma niczego. Instancja istnieje w pamięci przeglądarki, zna swój komponent główny, ale nie ma najmniejszego pojęcia, w którym miejscu strony ma się pokazać. Plik

index.html
się nie zmienił, jego
<div>
jest wciąż pusty. Aplikacja jest zbudowana, lecz jeszcze nie wystartowała.

mount() - podłączenie do platformy

Brakuje ostatniego ruchu: wskazania platformy. Robi to metoda

mount
wywołana na instancji aplikacji. Podajesz jej selektor CSS - ten sam rodzaj zapisu, którego używałeś w arkuszach stylów. Znak
#
oznacza "element o tym identyfikatorze", więc
'#app'
czyta się jako "element o
id
równym
app
", czyli dokładnie ten pusty
<div>
z pliku HTML.

Od tego wywołania Vue przejmuje wskazany element: wstawia do niego wynik szablonu komponentu głównego i od tej pory sam pilnuje jego zawartości. To jest moment startu systemu.

Zapamiętaj też, czego

mount
nie robi, bo pomyłki idą tu zawsze w tę samą stronę. Nie tworzy żadnego pliku HTML - plik
index.html
musi istnieć wcześniej i to Ty go piszesz. Nie instaluje pakietów, bo od tego jest
npm install
z lekcji o Vite. Nie uruchamia też serwera deweloperskiego: serwer startuje poleceniem
npm run dev
i działa na długo przed tym, zanim Twój kod w ogóle się wykona.

1import { createApp } from 'vue'
2import App from './App.vue'
3
4const app = createApp(App)
5
6app.mount('#app')

To kompletny plik startowy najprostszej aplikacji Vue - cztery instrukcje i ani jednej więcej. Uruchom projekt, a w przeglądarce zobaczysz to, co masz w szablonie

App.vue
. Zwróć uwagę, że nie zmieniło się nic poza tym jednym plikiem:
index.html
wygląda tak samo,
App.vue
tak samo, a mimo to system ruszył.

Instancję można też zamontować bez zapisywania jej do zmiennej, łańcuchem

createApp(App).mount('#app')
. Działa identycznie i w przykładach z dokumentacji zobaczysz taki zapis dość często, ale w NOVA LAB polecam Ci wersję dwuetapową ze zmienną
app
. Powód jest praktyczny: za chwilę będziesz do tej instancji dokładał moduły stacji, a do tego potrzebujesz uchwytu, który łańcuch po prostu wyrzuca.

Gdy selektor nie pasuje do platformy

Skoro selektor jest zwykłym tekstem, to nikt nie broni Ci wpisać w nim czegokolwiek - i tu bierze się klasyczna awaria pierwszego dnia. Aplikacja się kompiluje, konsola nie krzyczy o składni, a strona jest pusta.

1// index.html contains <div id="app"></div>
2// but the selector below points to something else
3app.mount('#control-room')

Vue szuka elementu

#control-room
, nie znajduje go i nie ma gdzie postawić interfejsu, więc w konsoli przeglądarki pojawia się ostrzeżenie o braku elementu docelowego. Nie zmieniło się przy tym nic w Twoim komponencie -
App.vue
jest zupełnie poprawny, a mimo to nie zobaczysz z niego ani jednego znaku. Gdy więc ekran panelu pozostaje czarny, @name, zacznij od porównania dwóch miejsc: wartości
id
w
index.html
i tekstu w wywołaniu
mount
.

app.use() - dokładanie modułów stacji

Jeden panel to jeszcze nie stacja. Prędzej czy później system misji potrzebuje wspólnej pamięci, z której korzystają wszystkie moduły naraz, oraz nawigacji, która przełącza ekrany centrum kontroli. Takich rzeczy nie da się sensownie doczepić do jednego komponentu - one muszą obowiązywać w całej aplikacji.

Vue nazywa je pluginami, a w naszej terminologii są to moduły stacji dokładane do instancji przed startem. Podłącza się je metodą

use
wywołaną na instancji aplikacji: piszesz nazwę zmiennej, kropkę,
use
, a w nawiasie podajesz gotowy moduł. Poznaj od razu dwa, które kreator z lekcji o Vite wpisał Ci do projektu.

Pierwszym jest Pinia, czyli centralna baza danych misji. Funkcja

createPinia
tworzy jej świeżą instancję - tak jak
createApp
tworzy instancję aplikacji - a wynik tego wywołania wędruje prosto do
use
. Drugim jest router, sprowadzany z katalogu
./router
, który wygenerował kreator. To już gotowy obiekt, więc podajesz go bez żadnego wywołania.

1import { createApp } from 'vue'
2import { createPinia } from 'pinia'
3import App from './App.vue'
4import router from './router'
5
6const app = createApp(App)
7
8app.use(createPinia())
9app.use(router)
10
11app.mount('#app')

Popatrz, co się tu nie zmieniło: linia z

createApp
i linia z
mount
są dokładnie takie same jak w wersji minimalnej, a
App.vue
nadal o niczym nie wie. Doszły wyłącznie dwie linie w środku - i to jest cała idea pluginów. Rozbudowujesz system, nie przebudowując go.

Kolejność ma tu znaczenie i jest tylko jedna słuszna: wszystkie wywołania

use
muszą stać przed
mount
. Montowanie to start, a modułów nie dokłada się do rakiety po odpaleniu silników. Gdybyś przestawił te linie, pierwsze renderowanie odbyłoby się bez pluginów - komponenty nie znalazłyby ani wspólnej pamięci, ani elementów nawigacji, a w konsoli wylądowałyby błędy o nierozpoznanych nazwach. Metoda
use
zwraca zresztą tę samą instancję, więc technicznie dałoby się łączyć wywołania w łańcuch, ale polecam pisać je jedno pod drugim: w sytuacji awaryjnej od razu widać listę podłączonych modułów.

Jeśli w Twoim projekcie nie ma katalogu

router
albo nie zainstalowałeś Pinii, po prostu usuń odpowiedni import razem z jego linią
use
- aplikacja zadziała bez nich, tak jak w wersji minimalnej. Obydwa moduły dostaną zresztą własne lokacje: Centrum Nawigacji zajmuje się routerem, a Baza Danych Misji - Pinią.

App.vue - rama stacji

Został ostatni z trzech plików.

App.vue
to komponent główny, czyli jedyny, który montujesz bezpośrednio; wszystkie pozostałe będą później wisiały wewnątrz niego. Z tej roli wynika jego zawartość: trzymamy tu wyłącznie to, co jest wspólne dla całej stacji i nie znika przy przełączaniu ekranów. W praktyce jest to belka górna z nazwą i nawigacją, obszar główny na treść oraz stopka.

Do zbudowania takiej ramy wystarczą znaczniki, które znasz z pierwszego świata:

<header>
,
<nav>
,
<main>
i
<footer>
. Nowa jest tylko jedna rzecz - sposób podpięcia linków. W poprzedniej lekcji zapamiętałeś, że pod
@click
podaje się nazwę funkcji bez nawiasów. To prawda dopóty, dopóki funkcja nie potrzebuje argumentu. Gdy chcesz jej coś przekazać, piszesz normalne wywołanie z nawiasami i wartością w środku, na przykład
@click="changeSection('Systems')"
. Vue potraktuje to jako wyrażenie i samo opakuje je w funkcję wywoływaną przy kliknięciu - dlatego nie uruchamia się ono od razu przy wyświetleniu strony.

1<template>
2  <div class="nova-lab">
3    <header>
4      <h1>NOVA LAB</h1>
5      <nav>
6        <a href="#" @click="changeSection('Dashboard')">Dashboard</a>
7        <a href="#" @click="changeSection('Systems')">Systems</a>
8        <a href="#" @click="changeSection('Crew')">Crew</a>
9      </nav>
10    </header>
11
12    <main class="content">
13      <h2>{{ currentSection }}</h2>
14      <p>Section content will be loaded here.</p>
15    </main>
16
17    <footer>
18      <p>NOVA LAB Mission Control 2087</p>
19    </footer>
20  </div>
21</template>

Cała rama to zwykły, semantyczny HTML - nie zmieniło się w nim nic poza trzema małpami i jedną parą wąsów. Nagłówek, nawigacja, obszar główny i stopka wyglądałyby identycznie na stronie bez żadnego frameworka. Vue dołożyło tu tylko dwie rzeczy: reakcję na kliknięcie oraz jedno miejsce, w którym podstawia bieżącą wartość.

Teraz druga sekcja pliku. Potrzebujemy jednego kontenera z nazwą aktualnej sekcji oraz funkcji, która tę nazwę podmienia. Funkcja przyjmuje argument - tę samą wartość, którą przekazałeś w szablonie - i zapisuje ją do kontenera przez

.value
, zgodnie z regułą z poprzedniej lekcji: w skrypcie zawsze
.value
, w szablonie nigdy.

1<script setup>
2import { ref } from 'vue'
3
4const currentSection = ref('Dashboard')
5
6function changeSection(name) {
7  currentSection.value = name
8}
9</script>

Kliknij dowolny link, a nagłówek w obszarze głównym zmieni się na nazwę wybranej sekcji. Zauważ, co przy tym zostaje na miejscu: belka górna, nawigacja i stopka nie drgną nawet na moment, bo Vue odświeża wyłącznie ten fragment, który korzysta ze zmienionego kontenera. Dokładnie po to komponent główny jest ramą - rzeczy wspólne mają się nie przeładowywać.

Sekcję

<style scoped>
pominąłem tu celowo, bo o stylach mówiliśmy w lekcji o modułach
.vue
i nic nowego by nie wniosła. W ćwiczeniu, które zaraz dostaniesz, style są już przygotowane.

Na razie przełączamy tylko napis, bo to wszystko, czego potrzeba, żeby zrozumieć rolę komponentu głównego. W Centrum Nawigacji ta sama rama zmieni się w prawdziwą nawigację: znaczniki

<a>
zastąpi komponent
RouterLink
, a w miejsce nagłówka w
<main>
wskoczy
RouterView
, który podmienia całe ekrany stacji zamiast jednej linijki tekstu. Kontener
currentSection
przestanie być wtedy potrzebny.

Sekwencja startowa od początku do końca

Masz już wszystkie elementy, więc złóżmy je w jedną listę kontrolną. Tak wygląda start systemu NOVA LAB, krok po kroku, za każdym razem w tej samej kolejności:

  1. Przeglądarka ładuje
    index.html
    - jedyny plik HTML w projekcie.
  2. Napotyka znacznik
    <script type="module">
    i prosi o
    main.js
    , a Vite podaje jej ten plik.
  3. Instrukcje
    import
    sprowadzają
    createApp
    z pakietu
    vue
    oraz komponent
    App
    .
  4. Wywołanie
    createApp(App)
    tworzy instancję aplikacji - na ekranie wciąż nic nie widać.
  5. Kolejne wywołania
    app.use()
    dokładają moduły, jeśli projekt z nich korzysta.
  6. Wywołanie
    app.mount('#app')
    podłącza aplikację do pustego
    <div>
    w
    index.html
    .
  7. Vue renderuje szablon komponentu
    App
    wewnątrz tego elementu i system jest gotowy do pracy.

Ta kolejność nie jest umowna - wynika z zależności. Nie zamontujesz aplikacji, której jeszcze nie powołałeś, i nie powołasz jej, zanim sprowadzisz

createApp
. Jedyny krok, który w prostym projekcie może zniknąć, to punkt piąty - odpada, gdy nie podłączasz żadnych modułów. Cała reszta zawsze wygląda tak samo, w każdym projekcie Vue, jaki otworzysz.

Zapamiętaj, @name:

index.html
to platforma lądowania,
main.js
to sekwencja zapłonu,
App.vue
to rama stacji - a
app.mount('#app')
to jedno wywołanie, w którym system naprawdę rusza.

Przejdź do CodeWorlds