Realizacja Aplikacji Internetowych
Rok akademicki: 2026/27
Instrukcja
laboratoryjna: Architektura Frameworka Vue.js (Vue 3.5+), Środowisko
Vite, Composition API, Pinia, Typowany Routing
(unplugin-vue-router), TanStack Query, Tailwind CSS i
Testowanie (Vitest & Playwright POM)
Prowadzący: Tomasz Goluch
Wersja: 1.0
1.
Wprowadzenie: Architektura MVVM, Progresywność i System Reaktywności
(Proxy)
Cel: Zrozumienie założeń architektonicznych
frameworka Vue.js, wzorca Model-View-ViewModel (MVVM), mechanizmu
Wirtualnego DOM (Virtual DOM) oraz ewolucji systemu reaktywności w Vue 3
opartego na obiektach ES6 Proxy.
Vue.js to progresywny framework
JavaScript/TypeScript służący do budowania nowoczesnych
interfejsów użytkownika oraz aplikacji jednostronicowych (SPA –
Single-Page Applications). Określenie
„progresywny” oznacza, że Vue zostało zaprojektowane tak, aby
można je było adaptować stopniowo – od lekkiej biblioteki dołączanej z
serwera CDN, wzbogacającej klasyczne widoki serwerowe HTML, aż po
pełnoskalowy framework korporacyjny wykorzystujący komponenty
jednoplikowe (.vue), kompilację w Vite,
silnie typowany routing, zarządzanie stanem (Pinia,
TanStack Query) oraz renderowanie po stronie serwera
(SSR – Nuxt).
1. Wzorzec architektoniczny MVVM (Model-View-ViewModel)
Architektura Vue.js jest bezpośrednio inspirowana wzorcem MVVM, który oddziela logikę warstwy prezentacji od stanu danych i logiki biznesowej:
- Model (Stan danych): Czyste obiekty
JavaScript/TypeScript reprezentujące dane dziedzinowe aplikacji
(zwracane przez opcję
data()lub zdefiniowane za pomocą funkcjiref()/reactive()). - View (Widok / DOM): Struktura dokumentu HTML
wyświetlana użytkownikowi w przeglądarce, wzbogacona o deklaratywne
szablony i dyrektywy Vue (
{{ }},v-bind,v-for,v-if). - ViewModel (Instancja Vue / System Reaktywności):
Warstwa pośrednicząca (instancja komponentu zwrócona przez metodę
.mount()), która automatycznie synchronizuje stan Modelu z Widokiem poprzez dwukierunkowe wiązanie danych (Data Binding) oraz nasłuchiwanie zdarzeń DOM (DOM Listeners).
┌───────────────────┐ ┌───────────────────────────┐ ┌───────────────────┐
│ WIDOK (View) │ │ WIDOK-MODEL (ViewModel) │ │ MODEL (State) │
│ │ DOM │ │ │ │
│ Elementy HTML │ ◄──────── │ Wiązania Danych (Proxy) │ ◄───────► │ Obiekty danych │
│ Szablony Vue │ │ │ │ JS / TypeScript │
│ (#app) │ ────────► │ Nasłuchiwanie Zdarzeń │ │ (ref, reactive, │
│ │ Zdarzenia│ (v-on / @click / v-model)│ │ data, Pinia) │
└───────────────────┘ └───────────────────────────┘ └───────────────────┘
2.
Jak działa reaktywność w Vue 3? (ES6 Proxy vs
Object.defineProperty)
Kluczową cechą Vue jest automatyczna reaktywność: gdy zmienisz wartość zmiennej w kodzie JavaScript, powiązany fragment drzewa DOM odświeża się samoczynnie.
🧠 Zrozumieć różnicę pokoleniową: Vue 2 vs Vue 3 (3.5+)
- W starszym Vue 2 reaktywność opierała się na metodzie
Object.defineProperty(), która rekurencyjnie nadpisywała gettery i settery każdej właściwości obiektu w momencie startu aplikacji. Miało to poważne ograniczenia: Vue 2 nie potrafiło wykryć dodania nowej właściwości do obiektu bez użyciaVue.set(), ani bezpośredniej modyfikacji tablicy po indeksie (tab[0] = 'nowy').- W Vue 3 system reaktywności został napisany od zera w oparciu o natywny mechanizm ES6
Proxy(dla obiektów) oraz lekkie gettery/settery właściwości.value(dla prymitywówref). ObiektProxytworzy niewidoczną „powłokę” wokół całego obiektu danych, przechwytując w locie wszystkie operacje odczytu (track– zapisanie, który komponent używa danej zmiennej) oraz zapisu, dodawania i usuwania właściwości (trigger– powiadomienie kompilatora szablonów o konieczności odświeżenia Wirtualnego DOM). W Vue 3.5+ system reaktywności został dodatkowo zoptymalizowany pod kątem pamięciowym (redukcja zużycia RAM o 56%) i szybkości śledzenia tablic.
2. Środowisko Uruchomieniowe, Narzędzia (NVM, Vite, IDE) i Konteneryzacja (Docker)
Cel: Poznanie sposobów instalacji Vue.js, konfiguracji środowiska Node.js z menedżerem NVM, zrozumienie architektury narzędzia budującego Vite oraz uruchamianie projektu w IDE i kontenerach Docker.
Laboratorium może być realizowane na maszynach fizycznych z wykorzystaniem preferowanego edytora lokalnego (Visual Studio Code, WebStorm, Visual Studio 2022, JetBrains Rider) lub w odizolowanym środowisku kontenerowym Docker / Docker Compose.
1. Sposób I: Tryb bez kroku budowania (Pobranie biblioteki z CDN)
W przypadku prostych stron lub nauki podstawowych dyrektyw nie trzeba
instalować żadnych narzędzi budujących – wystarczy dołączyć skrypt
pobierający globalną wersję Vue 3 bezpośrednio z serwera CDN (w miejsce
3 lub latest serwer podstawi najnowszą
stabilną wersję z gałęzi 3.x):
<!-- Wersja globalna z unpkg (zalecana do szybkiego prototypowania) -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- Alternatywne serwery CDN (jsDelivr oraz cdnjs) -->
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.5.13/vue.global.min.js"></script>
2. Sposób II: Profesjonalne środowisko SPA oparte na Node.js i Vite
Aby korzystać z pełnych możliwości ekosystemu Vue.js – takich jak
Komponenty Jednoplikowe (.vue),
TypeScript, nowoczesne wtyczki, Pinia
czy testy jednostkowe – potrzebujemy środowiska wykonawczego
Node.js oraz narzędzia budującego
Vite.
A. Zarządzanie wersjami Node.js za pomocą NVM (Node Version Manager)
Zamiast instalować pojedynczą, sztywną wersję Node.js w systemie, pracę warto rozpocząć od instalacji menedżera NVM. Pozwala on na błyskawiczne przełączanie się pomiędzy różnymi wersjami Node.js (np. stabilną wersją LTS a wersją najnowszą) bez konieczności reinstalacji środowiska.
- Instalator dla systemu Windows (nvm-windows): https://github.com/coreybutler/nvm-windows/releases/
Podstawowe komendy narzędzia nvm (wymagają uruchomienia
terminala z uprawnieniami administratora):
nvm install lts(lubnvm install latest) – pobranie i instalacja najnowszej wersji długoterminowego wsparcia (LTS) lub wersji bieżącej.nvm ls(lubnvm list) – wyświetlenie listy zainstalowanych wersji Node.js wraz ze wskazaniem wersji aktualnie aktywnej.nvm use <X.X.X>– przełączenie aktywnej wersji środowiska Node.js.
B.
Alternatywne menedżery pakietów (pnpm, yarn)
oraz npm-gui
Wraz z Node.js instalowany jest domyślny menedżer pakietów npm. Alternatywnie można zainstalować szybszy i oszczędzający miejsce na dysku menedżer pnpm (wykorzystujący globalny magazyn dowiązań twardych) lub Yarn:
npm install --global pnpm
# lub:
npm install --global yarn
Przydatnym narzędziem wizualnym jest graficzny interfejs do
zarządzania zależnościami z pliku package.json:
npm install -g npm-gui
npm-gui
C. Dlaczego Vite zastąpiło dawne Vue CLI (Webpack)?
Do inicjalizacji nowych projektów Vue 3 wykorzystujemy oficjalny
kreator create-vue oparty na narzędziu
Vite (zastępujący przestarzałe narzędzie
vue-cli oparte na Webpacku).
1. TRADYCYJNY BUNDLER (Webpack / Vue CLI):
[Wszystkie pliki .ts/.vue] ──► [Budowanie całej paczki (Bundle)] ──► [Serwer Dev (wolny start)]
2. NOWOCZESNY SERWER ESM (Vite):
[Natychmiastowy start serwera] ──► Przeglądarka żąda pliku HTTP GET ──► [Kompilacja 1 pliku w locie (ESM)]
- Natywne moduły ECMAScript (ESM): Vite nie pakuje
całej aplikacji przed uruchomieniem serwera deweloperskiego. Zamiast
tego wykorzystuje natywną obsługę
<script type="module">w przeglądarkach i kompiluje tylko te pliki, których przeglądarka aktualnie zażąda. - Błyskawiczny HMR (Hot Module Replacement):
Po edycji pliku
.vuew edytorze Vite podmienia w przeglądarce wyłącznie zmodyfikowany moduł w ciągu kilkunastu milisekund, zachowując aktualny stan uruchomionej aplikacji.
D.
Tworzenie i uruchamianie projektu za pomocą create-vue
Aby utworzyć nowy projekt z pełną konfiguracją (TypeScript, Vue Router, Pinia, Vitest, Playwright, ESLint oraz Prettier), wykonujemy w terminalu:
npm create vue@latest
Kreator przeprowadzi nas przez interaktywną konfigurację projektu:
Vue.js - The Progressive JavaScript Framework
✔ Project name: … rai-lab
✔ Add TypeScript? … Yes
✔ Add JSX Support? … No
✔ Add Vue Router for Single Page Application development? … Yes
✔ Add Pinia for state management? … Yes
✔ Add Vitest for Unit Testing? … Yes
✔ Add an End-to-End Testing Solution? › Playwright
✔ Add ESLint for code quality? › Yes
✔ Add Prettier for code formatting? … Yes
✔ Add Vue DevTools 7 extension for debugging? (experimental) … Yes
Instalacja zależności i uruchomienie serwera deweloperskiego:
cd rai-lab
npm install
npm run format
npm run dev
Po uruchomieniu w konsoli pojawią się adresy lokalne (domyślny port Vite to 5173):
- Aplikacja:
http://localhost:5173/ - Zintegrowane narzędzia Vue DevTools:
http://localhost:5173/__devtools__/(lub skrótAlt + Shift + Dbezpośrednio w oknie aplikacji). - Zatrzymanie serwera w terminalu:
q + Enter(lubCtrl + C).
3. Konfiguracja w środowiskach IDE (VS Code oraz Visual Studio 2022)
A. Visual Studio Code
- Otwarcie projektu z wiersza poleceń (będąc w katalogu projektu):
code .(lub w IDE skrótemCtrl+K, Ctrl+O). - Rekomendowane rozszerzenia: Vue (Official) (dawniej
Volar – zapewnia kolorowanie składni
.vueoraz sprawdzanie typów TypeScript) oraz Vite (VS Code for Vite ⚡ – automatycznie uruchamia serwer deweloperski i podgląd po otwarciu folderu).
B. Visual Studio 2022
(Plik projektu .esproj)
Aby otworzyć i uruchamiać projekt Vue w pełnym środowisku Visual Studio 2022 (np. w ramach wspólnej solucji z backendem ASP.NET Core Web API), wykorzystywane są dwa pliki konfiguracyjne:
- Plik projektu
rai-lab.esproj– określa wersję SDK JavaScript, komendę startową zpackage.json, framework testowy oraz katalog wyjściowy kompilacji produkcyjnej:<Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/1.0.1184077"> <PropertyGroup> <StartupCommand>npm run dev</StartupCommand> <JavaScriptTestRoot>src\</JavaScriptTestRoot> <JavaScriptTestFramework>Vitest</JavaScriptTestFramework> <ShouldRunBuildScript>false</ShouldRunBuildScript> <BuildOutputFolder>$(MSBuildProjectDirectory)\dist</BuildOutputFolder> </PropertyGroup> </Project> - Plik
.vscode/launch.json– wskazuje przeglądarkę docelową i port serwera Vite (http://localhost:5173).
4. Konteneryzacja aplikacji Vue w Docker i Docker Compose
Zgodnie z wymogami nowoczesnych wdrożeń, aplikację Vue możemy uruchomić w kontenerze Docker zarówno w trybie deweloperskim (z obsługą HMR), jak i w zoptymalizowanym obrazie produkcyjnym serwowanym przez serwer Nginx:
Plik Dockerfile (Wieloetapowy – Multi-stage
build):
# ETAP 1: Środowisko deweloperskie i budujące (Node.js LTS)
FROM node:22-alpine AS build-stage
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ETAP 2: Lekki serwer produkcyjny Nginx
FROM nginx:stable-alpine AS production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Plik docker-compose.yml (Uruchomienie serwera
deweloperskiego Vite w kontenerze):
services:
vue-dev:
image: node:22-alpine
working_dir: /app
volumes:
- .:/app
- /app/node_modules
ports:
- "5173:5173"
command: sh -c "npm install && npm run dev -- --host 0.0.0.0"
3. Tworzenie Aplikacji Vue.js, Instancja Główna i Wbudowane Dyrektywy
Cel: Utworzenie pierwszej reaktywnej aplikacji Vue 3, poznanie globalnego i lokalnego API instancji oraz opanowanie wbudowanych i własnych dyrektyw szablonów.
1. Pierwsza aplikacja Vue 3 (Standalone CDN)
Rozpoczniemy od utworzenia prostej aplikacji bez narzędzi budujących,
składającej się z pliku labVUE.html oraz skryptu
labVUE.js.
Plik labVUE.html:
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Laboratorium Vue.js 3</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app" class="demo">
<h2>{{ title }}</h2>
<input v-model="title" placeholder="Zmień tytuł aplikacji">
<p>{{ $filters.capitalize('To jest przykładowy napis') }}</p>
<p>{{ "Sufit z 7,45 to: " + Math.ceil(7.45) }}</p>
</div>
<script src="labVUE.js"></script>
</body>
</html>
Plik labVUE.js:
// Definicja obiektu głównego komponentu (Root Component)
const MyVueApp = {
// UWAGA: W Vue 3 nie używamy już opcji 'el: "#app"' znanej z Vue 2!
// Opcja data() w Vue 3 zawsze powinna być funkcją zwracającą obiekt stanu:
data() {
return {
title: 'Moja aplikacja Vue.js'
};
}
};
// 1. Tworzymy instancję aplikacji za pomocą globalnego API Vue.createApp()
const myApp = Vue.createApp(MyVueApp);
// 2. Rejestracja właściwości globalnych (np. zastępujących wycofane z Vue 2 filtry)
// MUSI nastąpić PRZED wywołaniem metody mount()!
myApp.config.globalProperties.$filters = {
capitalize(value) {
if (!value) return '';
return value.toString().toUpperCase();
}
};
// 3. Montujemy aplikację w elemencie DOM '#app'.
// Metoda mount() zwraca instancję głównego komponentu (ViewModel - Proxy), a nie samą aplikację!
const vm = myApp.mount('#app');
⚠️ Ważna różnica między
myApp(Instancją Aplikacji) avm(Instancją Komponentu / Proxy):
Większość metod konfiguracyjnych instancji aplikacji (takich jakmyApp.component(),myApp.directive(),myApp.use()) zwraca sam obiekt aplikacjimyApp, co pozwala na ich wygodne łańcuchowanie (Fluent API):
Vue.createApp(MyVueApp).use(router).mount('#app').
Wyjątkiem jest metoda.mount('#app'), która kończy konfigurację i zwraca reaktywną instancję głównego komponentu (vm– obiektProxy). Właściwości globalne (globalProperties) muszą zostać zdefiniowane przed wywołaniem.mount(), w przeciwnym razie otrzymamy ostrzeżenie:[Vue warn]: Property "$filters" was accessed during render but is not defined on instance.
2. Wbudowane dyrektywy Vue.js
Dyrektywy to specjalne atrybuty HTML rozpoczynające
się prefiksem v-, które nakładają
reaktywne zachowanie na renderowane elementy DOM. Dzięki nim nasz kod w
JS skupia się wyłącznie na logice stanu, a wszelkie operacje na drzewie
DOM wykonuje za nas Vue.
A. Dwukierunkowe
wiązanie formularzy: v-model
Dyrektywa v-model automatycznie synchronizuje wartość
kontrolki HTML (<input>, <select>,
<textarea>) ze zmienną w stanie komponentu. Wspiera
przydatne modyfikatory:
v-model.lazy="title"– aktualizuje stan dopiero po utracie fokusu (zdarzeniechangezamiastinput),v-model.number="age"– automatycznie rzutuje wpisany ciąg znaków na typ liczbowy (Number),v-model.trim="username"– automatycznie usuwa białe znaki z początku i końca wpisanego tekstu.
B.
Renderowanie list i znaczenie atrybutu :key:
v-for
Dyrektywa v-for pozwala iterować po tablicach oraz
właściwościach obiektów. Rozbudujmy metodę data() w pliku
labVUE.js:
data() {
return {
title: 'Moja aplikacja Vue.js',
tab: [
{ id: 1, name: 'kot', checked: true },
{ id: 2, name: 'pies', checked: false },
{ id: 3, name: 'mysz', checked: false }
]
};
}
Renderowanie listy z dostępem do elementu oraz jego bieżącego indeksu
(index):
<ul>
<!-- Zawsze podawaj unikalny atrybut :key podczas używania v-for! -->
<li v-for="(pet, index) in tab" :key="pet.id">
<label>
<input type="checkbox" v-model="pet.checked">
{{ index }}. {{ pet.name }}
</label>
</li>
</ul>
🧠 Dlaczego modyfikacja w konsoli powinna odbywać się przez
vm.tab.push(...)oraz dlaczego:keyjest obowiązkowe?
- Dostęp przez Proxy (
vm): Jeśli otworzysz konsolę przeglądarki (F12) i wpiszeszvm.tab.push({ id: 4, name: 'lis', checked: true }), nowy element natychmiast pojawi się na ekranie! Dzieje się tak, ponieważvmto reaktywneProxyutworzone przez Vue. Gdybyś próbował modyfikować surowy obiekt zewnętrzny zdefiniowany poza Vue, ominąłbyśProxyi widok nie zostałby powiadomiony o zmianie.- Rola atrybutu
:key: Algorytm Wirtualnego DOM w Vue porównuje stare i nowe drzewo węzłów. Unikalny identyfikator:key="pet.id"pozwala Vue precyzyjnie śledzić tożsamość każdego wiersza przy sortowaniu, filtrowaniu lub usuwaniu elementów, zamiast kosztownego przebudowywania całych elementów DOM i gubienia stanu zaznaczenia kontrolek.
C.
Jednokierunkowe wiązanie atrybutów, klas i stylów: v-bind
(skrót :)
Dyrektywa v-bind:<atrybut> (w zapisie skróconym po
prostu dwukropek :<atrybut>) wiąże
atrybut HTML z wyrażeniem JavaScript:
// W data():
path: 'https://vuejs.org/images/',
filename: 'logo.png'
<!-- Wersja pełna i zalecana wersja skrócona (:src) -->
<img v-bind:src="path + filename" height="42" alt="Logo Vue" />
<img :src="path + filename" height="42" alt="Logo Vue" />
Dla atrybutów :class oraz :style Vue
oferuje specjalną składnię obiektową oraz tablicową, pozwalającą
dynamicznie przełączać klasy CSS w zależności od warunków
logicznych:
<ul>
<!-- Składnia obiektowa: klasa 'checked_class' zostanie dodana, gdy pet.checked === true -->
<li v-for="pet in tab" :key="pet.id" :class="{ 'checked_class': pet.checked }">
{{ pet.name }}
</li>
</ul>
<ul>
<!-- Składnia tablicowa z operatorem trójargumentowym: przełączanie między dwiema klasami -->
<li v-for="pet in tab" :key="pet.id" :class="[pet.checked ? 'checked_class' : 'unchecked_class']">
<div class="checkbox">
<label>
<input type="checkbox" v-model="pet.checked"> {{ pet.name }}
</label>
</div>
</li>
</ul>
D.
Renderowanie warunkowe: v-if / v-else-if /
v-else vs v-show
Vue udostępnia dwa mechanizmy warunkowego wyświetlania elementów:
<div class="checkbox" v-for="pet in tab" :key="pet.id">
<label>
<input type="checkbox" v-model="pet.checked">
<!-- v-if oraz v-else muszą znajdować się bezpośrednio po sobie! -->
<span v-if="pet.checked">{{ pet.name }}</span>
<span v-else>removed</span>
</label>
<!-- Alternatywnie z użyciem v-show: -->
<span v-show="pet.checked">(Widoczny przez CSS)</span>
</div>
| Kryterium | Dyrektywa v-if
(v-else-if, v-else) |
Dyrektywa v-show |
|---|---|---|
| Mechanizm działania w DOM | Fizycznie tworzy lub usuwa element z drzewa DOM (wraz z wywołaniem haków cyklu życia komponentów potomnych). | Element jest zawsze renderowany w
DOM, a przełączana jest jedynie właściwość CSS
display: none;. |
Zachowanie przy starcie
(false) |
Leniwe (Lazy):
Jeśli na starcie warunek wynosi false, nic nie jest
renderowane. |
Element zostaje od razu zbudowany w DOM w stanie ukrytym. |
| Koszt wydajnościowy | Wyższy koszt przełączania (modyfikacja struktury DOM), niski koszt początkowy. | Bardzo niski koszt przełączania, wyższy koszt początkowego renderowania. |
| Kiedy stosować? | Gdy warunek zmienia się rzadko w trakcie działania aplikacji (np. uprawnienia administratora). | Gdy element jest bardzo często przełączany przez użytkownika (np. zakładki, rozwijane panele). |
⚠️ Ważna zasada w Vue 3 (
v-ifvsv-for):
Nigdy nie umieszczajv-iforazv-forna tym samym znaczniku HTML! W Vue 3 dyrektywav-ifma wyższy priorytet niżv-for, co oznacza, że warunekv-ifnie ma jeszcze dostępu do zmiennej iteracyjnej zv-for. Jeśli chcesz przefiltrować listę, użyj właściwości obliczonej (computed) lub opakuj listę w znacznik<template v-for="...">.
E. Obsługa zdarzeń DOM:
v-on (skrót @)
Dyrektywa v-on:<zdarzenie> (w zapisie skróconym
znak @<zdarzenie>) przypisuje
nasłuchiwacze zdarzeń. Metody definiujemy w sekcji methods
komponentu:
// W MyVueApp:
data() {
return {
counter: 0
};
},
methods: {
increment(event) {
// Słowo kluczowe 'this' w Options API automatycznie wskazuje na instancję komponentu
this.counter += 1;
}
}
<hr>
<!-- Wywołanie metody zdefiniowanej w sekcji methods -->
<button v-on:click="increment">Dodaj 1</button>
<!-- Zapis skrócony (@click) wraz z wyrażeniem inline -->
<button @click="counter++">(Wersja inline) Dodaj 1</button>
<!-- Użycie modyfikatora .prevent (odpowiednik event.preventDefault()) -->
<form @submit.prevent="increment">
<button type="submit">Zatwierdź bez przeładowania strony</button>
</form>
<p>Przycisk naciśnięto {{ counter }} razy.</p>
3.
Tworzenie niestandardowych dyrektyw
(Custom Directives)
Gdy potrzebujemy bezpośredniego, niskopoziomowego dostępu do elementu
DOM (np. automatyczne ustawienie fokusu, kolorowanie czy formatowanie),
rejestrujemy własną dyrektywę metodą myApp.directive():
// Przykład 1: Dyrektywa v-random-color nadająca losowy kolor tekstu po zamontowaniu
myApp.directive('random-color', {
mounted: (el) => {
const hex = Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');
el.style.color = `#${hex}`;
}
});
// Przykład 2: Dyrektywa v-ceil odbierająca przekazaną wartość przez parametr 'binding'
myApp.directive('ceil', {
mounted: (el, binding) => {
el.textContent = Math.ceil(binding.value);
},
updated: (el, binding) => {
el.textContent = Math.ceil(binding.value);
}
});
Użycie własnych dyrektyw w szablonie HTML:
<h2 v-random-color>{{ title }}</h2>
<p>Sufit z 7,45 to: <span v-ceil="7.45"></span></p>
Obiekt definicji dyrektywy w Vue 3 udostępnia uchwyty
(hooks) odpowiadające cyklowi życia komponentu:
created, beforeMount, mounted,
beforeUpdate, updated,
beforeUnmount oraz unmounted.
4.
Właściwości Obliczone (Computed Properties) i Obserwatory
(Watchers)
Cel: Opanowanie deklaratywnego tworzenia danych
pochodnych z wykorzystaniem buforowanych właściwości obliczonych
(computed) oraz wykonywania asynchronicznych efektów
ubocznych w odpowiedzi na zmianę stanu (watch).
1.
Właściwości Obliczone (computed) i mechanizm
Cachowania
Zbyt rozbudowane wyrażenia w szablonach HTML
({{ firstName + ' ' + lastName.toUpperCase() }})
zmniejszają czytelność kodu. Właściwości obliczone
(computed) pozwalają deklaratywnie definiować
wartości pochodne, które zależą od innych reaktywnych danych.
- Kluczowa zaleta – Buforowanie (Caching): W
przeciwieństwie do zwykłej metody wywoływanej przy każdym przerysowaniu
ekranu, właściwość
computedpamięta swój ostatni wynik i przelicza go ponownie tylko wtedy, gdy zmieniła się przynajmniej jedna z jej reaktywnych zależności (firstNamelublastName).
// Fragment JS (w obiekcie MyVueApp):
const MyVueApp = {
data() {
return {
firstName: 'Jan',
lastName: 'Kowalski'
};
},
computed: {
// Funkcja zostanie wywołana TYLKO wtedy, gdy zmieni się this.firstName lub this.lastName
fullName() {
console.log('Obliczanie fullName...');
return `${this.firstName} ${this.lastName}`;
}
}
};
<!-- Fragment HTML: używamy 'fullName' jak zwykłej właściwości (BEZ nawiasów '()'!) -->
<input v-model="firstName" placeholder="Imię">
<input v-model="lastName" placeholder="Nazwisko">
<p>Pełne imię i nazwisko: {{ fullName }}</p>
2.
Obserwatory (watch) – Reagowanie na zmiany i Efekty
Uboczne
Podczas gdy właściwości computed muszą być czystymi
funkcjami synchronicznymi zwracającymi wartość (nie wolno w nich
wykonywać zapytań HTTP ani modyfikować innych zmiennych!),
Obserwatory (watch) służą do wywoływania
tzw. efektów ubocznych (Side Effects) – np.
asynchronicznego odpytywania serwera API, zapisu do
localStorage lub uruchamiania timerów w odpowiedzi na
zmianę konkretnej właściwości.
// Fragment JS (w obiekcie MyVueApp):
const MyVueApp = {
data() {
return {
question: '',
answer: 'Pytania zwykle kończą się znakiem zapytania "?" ;-)'
};
},
methods: {
getAnswer() {
this.answer = 'Myślę...';
// Symulacja asynchronicznego zapytania do zewnętrznego API
setTimeout(() => {
this.answer = 'Odpowiedź brzmi: 42.';
}, 1500);
}
},
watch: {
// Nazwa metody obserwatora musi dokładnie odpowiadać nazwie obserwowanej właściwości!
// Vue przekazuje do niej nową (newQuestion) oraz poprzednią (oldQuestion) wartość:
question(newQuestion, oldQuestion) {
if (newQuestion.includes('?')) {
this.getAnswer();
}
}
}
};
<!-- Fragment HTML -->
<p>
Zadaj pytanie:
<input v-model="question" placeholder="Wpisz pytanie zakończone znakiem '?'" />
</p>
<p><strong>Odpowiedź:</strong> {{ answer }}</p>
5.
Architektura Komponentów, Komunikacja (Props /
Emits), Sloty i v-model
Cel: Podział interfejsu na reużywalne komponenty,
zrozumienie jednokierunkowego przepływu danych (Props Down, Events
Up), wstrzykiwanie treści przez <slot> oraz
implementacja własnych kontrolek obsługujących v-model.
Komponenty pozwalają podzielić złożony interfejs użytkownika na niezależne, hermetyczne i wielokrotnie używane klocki.
1.
Tworzenie komponentów, szablony <template> i izolacja
stanu data()
Podczas globalnej rejestracji komponentu metodą
app.component('nazwa-komponentu', { ... }) zaleca się
stosowanie notacji kebab-case (z myślnikiem), ponieważ
standardowy kod HTML w przeglądarce nie rozróżnia wielkości liter
(case-insensitive).
Zamiast pisać kod HTML wewnątrz łańcucha znaków w JavaScript, w
aplikacjach bez kroku kompilacji definiujemy szablon w znaczniku
<template id="...">:
<!-- Szablon komponentu zdefiniowany w pliku HTML -->
<template id="component_id">
<h2>{{ msg }} {{ msg2 }}</h2>
</template>
<div id="app">
<!-- Przekazanie właściwości z rodzica do dziecka za pomocą dyrektywy v-bind (:msg2) -->
<my-component :msg2="gMsg"></my-component>
</div>
const app = Vue.createApp({
data() {
return {
gMsg: 'global'
};
}
});
app.component('my-component', {
template: '#component_id',
// Jawna deklaracja parametrów wejściowych (Props) odbieranych od komponentu nadrzędnego:
props: ['msg2'],
// KRYTYCZNE: W komponencie właściwość 'data' MUSI być funkcją zwracającą nowy obiekt!
data() {
return {
msg: 'To jest'
};
}
});
app.mount('#app');
⚠️ Dlaczego
dataw komponencie musi być funkcją, a nie zwykłym obiektem?
Gdyby właściwośćdatabyła zwykłym obiektem (data: { count: 0 }), wszystkie instancje<my-component>umieszczone na stronie współdzieliłyby tę samą referencję w pamięci RAM – kliknięcie przycisku w jednym komponencie zmieniłoby stan we wszystkich pozostałych! Dzięki temu, żedata()jest funkcją fabrykującą, każde utworzenie nowej instancji komponentu wywołuje funkcję i otrzymuje własną, odizolowaną kopię obiektu stanu. Przekazanie płaskiego obiektu wywoła błąd:[Vue warn]: The data option must be a function. Plain object usage is no longer supported.
2. Komunikacja między komponentami: Props Down, Events Up
W Vue obowiązuje zasada jednokierunkowego przepływu danych (One-Way Data Flow):
- Rodzic \(\rightarrow\)
Potomek (
Props Down): Dane spływają w dół drzewa komponentów przez atrybutyprops. Komponent potomny nie może bezpośrednio mutować otrzymanego propsa (jest on dla niego tylko do odczytu!). - Potomek \(\rightarrow\)
Rodzic (
Events Up): Aby poinformować rodzica o zmianie danych, komponent potomny emituje zdarzenie za pomocą metodythis.$emit('nazwa-zdarzenia', ladunekDanych), a komponent nadrzędny nasłuchuje na nie za pomocą dyrektywy@nazwa-zdarzenia="metodaRodzica".
┌─────────────────────────────────────────────────────┐
│ Komponent Nadrzędny (multi-component) │
│ Stan: mMsg = "komponent" │
└──────────┬───────────────────────────────┬──────────┘
│ │
1. Props Down: │ :in_msg="mMsg" │ 3. Props Down:
Przekazuje │ │ :msg2="mMsg"
wartość ▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ inp-component │ │ my-component │
│ <input :value=...> │ │ <h2>To jest {{msg2}}│
└──────────┬──────────┘ └─────────────────────┘
│ ▲
2. Events Up: │ $emit('inpupdated', newVal) │
Emituje nową └───────────────────────────────┘
wartość do rodzica (@inpupdated="updatemsg")
Kompletny, działający przykład komunikacji między trzema komponentami:
Struktura szablonów w HTML:
<!-- 1. Szablon komponentu wyświetlającego napis -->
<template id="component_id">
<h2>{{ msg }} {{ msg2 }}</h2>
</template>
<!-- 2. Szablon komponentu pola tekstowego -->
<template id="inp_id">
<input :value="in_msg" @input="onInput" placeholder="Wpisz nową nazwę..." />
</template>
<!-- 3. Szablon komponentu nadrzędnego łączącego oba powyższe komponenty -->
<!-- UWAGA: Nasłuchiwanie @inpupdated umieszczamy bezpośrednio na znaczniku <inp-component>! -->
<template id="multi_comp_id">
<div>
<inp-component :in_msg="mMsg" @inpupdated="updatemsg"></inp-component>
<my-component :msg2="mMsg"></my-component>
</div>
</template>
<!-- Miejsce montowania głównej aplikacji -->
<div id="app">
<multi-component></multi-component>
</div>
Implementacja logiki w JavaScript:
const app = Vue.createApp({});
// 1. Komponent prezentacyjny
app.component('my-component', {
template: '#component_id',
props: ['msg2'],
data() {
return { msg: 'To jest' };
}
});
// 2. Komponent wejściowy (emituje zdarzenie 'inpupdated' przy każdym wciśnięciu klawisza)
app.component('inp-component', {
template: '#inp_id',
props: ['in_msg'],
emits: ['inpupdated'], // Jawna deklaracja emitowanych zdarzeń (dobra praktyka w Vue 3)
methods: {
onInput(event) {
this.$emit('inpupdated', event.target.value);
}
}
});
// 3. Komponent nadrzędny (zarządza wspólnym stanem mMsg i obsługuje zdarzenie updatemsg)
app.component('multi-component', {
template: '#multi_comp_id',
data() {
return {
mMsg: 'komponent'
};
},
methods: {
updatemsg(newValue) {
this.mMsg = newValue;
}
}
});
app.mount('#app');
💡 Zasięg zdarzeń komponentów (Brak bąbelkowania):
W przeciwieństwie do natywnych zdarzeń DOM, zdarzenia emitowane przez$emitnie bąbelkują (do not bubble) w górę drzewa. Można nasłuchiwać wyłącznie zdarzeń emitowanych przez bezpośredniego potomka. Do komunikacji między głęboko zagnieżdżonymi lub siostrzanymi komponentami stosujemy paręprovide/injectlub globalny magazyn stanu Pinia.
3. Dystrybucja
treści za pomocą Slotów (<slot>)
Sloty to wyznaczone miejsca w szablonie komponentu potomnego, w które rodzic może wstrzyknąć dowolny fragment kodu HTML lub inne komponenty (idealne do budowy uniwersalnych przycisków, kart, ramek i okien modalnych).
// Definicja komponentu ze slotem oraz zawartością domyślną (Fallback Content):
app.component('fancy-button', {
template: `
<button class="fancy-btn">
<slot>Domyślny tekst przycisku</slot>
</button>
`
});
<!-- Użycie w komponencie nadrzędnym: -->
<div id="app">
<fancy-button>Kliknij mnie!</fancy-button>
<fancy-button>
<span style="font-weight: bold;">✔ Zatwierdź formularz</span>
</fancy-button>
</div>
4. Komponenty
kompatybilne z dyrektywą v-model
W Vue 3 możemy sprawić, aby nasz własny komponent obsługiwał
dwukierunkową dyrektywę v-model identycznie jak natywny
<input>.
Pod maską zapis
<custom-input v-model="message"></custom-input>
jest lukrem składniowym (syntactic sugar), który kompilator Vue
rozwija do:
- Przekazania właściwości (
prop) o nazwiemodelValue, - Nasłuchiwania na zdarzenie o nazwie
update:modelValue.
app.component('custom-input', {
// 1. Akceptuj prop 'modelValue'
props: ['modelValue'],
// 2. Zadeklaruj emitowane zdarzenie 'update:modelValue'
emits: ['update:modelValue'],
// 3. W szablonie powiąż :value i wyemituj nową wartość przy zdarzeniu @input
template: `
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
placeholder="Własny komponent input"
/>
`
});
<div id="app">
<custom-input v-model="message"></custom-input>
<p>Wiadomość w rodzicu: {{ message }}</p>
</div>
6. Cykl Życia
Komponentu (Lifecycle Hooks)
Cel: Poznanie etapów istnienia komponentu od jego inicjalizacji, przez montowanie i aktualizację w DOM, aż po zniszczenie, oraz prawidłowe wykorzystanie haków cyklu życia.
Każda instancja komponentu Vue przechodzi przez uporządkowaną serię kroków inicjalizacyjnych: konfigurację reaktywności stanu, kompilację szablonu, zamontowanie w drzewie DOM, aktualizacje po zmianie danych oraz odmontowanie. Na każdym z tych etapów Vue wywołuje specjalne funkcje zwrotne – haki cyklu życia (Lifecycle Hooks).
[ Renderer napotyka komponent ]
│
▼
┌───► [ setup() (Composition API) ]
│ │
├───► [ beforeCreate ]
│ ▼
│ (Inicjalizacja Options API: data, methods, computed)
│ │
├───► [ created ] (Reaktywność gotowa, ale brak jeszcze DOM!)
│ ▼
│ [ Kompilacja szablonu (jeśli brak prekompilacji) ]
│ │
├───► [ beforeMount ]
│ ▼
│ (Utworzenie i wstawienie węzłów do prawdziwego DOM)
│ │
└───► [ mounted / onMounted ] ◄─── Tutaj pobieramy dane z API /
│ operujemy na elementach DOM!
▼
(( ZAMONTOWANY )) ◄──────────────────┐
│ │ │
│ │ (Zmiana danych) │
│ ▼ │
│ [ beforeUpdate / onBeforeUpdate ]
│ │ │
│ ▼ │
│ (Przerysowanie Virtual DOM) │
│ │ │
│ └─► [ updated / onUpdated ]
│
│ (Usunięcie komponentu z widoku)
▼
┌───► [ beforeUnmount / onBeforeUnmount ]
│ │ (Czyszczenie timerów i nasłuchiwaczy!)
│ ▼
└───► [ unmounted / onUnmounted ]
│
▼
(( ODMONTOWANY ))
Zestawienie haków w Options API oraz Composition API
| Options API | Composition API
(<script setup>) |
Moment wywołania i typowe zastosowanie |
|---|---|---|
beforeCreate |
(Zastąpione przez
setup) |
Wywoływany przed inicjalizacją
data i methods. |
created |
(Zastąpione przez
setup) |
Stan reaktywny jest gotowy, ale element
$el w DOM jeszcze nie istnieje. |
beforeMount |
onBeforeMount(() => ...) |
Tuż przed pierwszym wyrenderowaniem komponentu w drzewie DOM. |
mounted |
onMounted(() => ...) |
Najczęściej używany hak. Komponent jest już w DOM – idealne miejsce na pobranie danych z API, inicjalizację wykresów lub dostęp do referencji DOM. |
beforeUpdate |
onBeforeUpdate(() => ...) |
Po zmianie reaktywnych danych, ale tuż przed aktualizacją drzewa DOM. |
updated |
onUpdated(() => ...) |
Po zaktualizowaniu drzewa DOM na skutek zmiany danych (uwaga: nie modyfikuj tu stanu, aby nie wywołać nieskończonej pętli!). |
beforeUnmount |
onBeforeUnmount(() => ...) |
Tuż przed usunięciem komponentu – komponent jest jeszcze w pełni funkcjonalny. |
unmounted |
onUnmounted(() => ...) |
Po usunięciu komponentu z DOM.
Obowiązkowe miejsce na czyszczenie timerów
(clearInterval) oraz globalnych nasłuchiwaczy
(window.removeEventListener). |
⚠️ Pułapka funkcji strzałkowych w Options API:
Definiując haki cyklu życia, metody lub właściwościcomputedw Options API, nigdy nie używaj funkcji strzałkowych (np.mounted: () => { this.fetchData(); }). Funkcje strzałkowe nie posiadają własnego kontekstuthis– wiążą go leksykalnie z zakresem nadrzędnym (np.undefinedw trybie ścisłym ESM), co skutkuje błędem:
Uncaught TypeError: Cannot read properties of undefined (reading 'fetchData'). Zawsze używaj skróconej składni metod obiektu:mounted() { this.fetchData(); }.
7.
Komponenty Jednoplikowe (SFC .vue), Composition API
(<script setup>) i Composables
Cel: Opanowanie architektury Komponentów
Jednoplikowych (.vue), przejście z klasycznego Options API
do nowoczesnego Composition API (<script setup>),
poznanie makr defineModel i useTemplateRef
oraz wydzielanie reużywalnej logiki do funkcji Composables.
1. Komponenty Jednoplikowe (Single-File Components – SFC)
W profesjonalnych projektach opartych na Vite zamykamy strukturę HTML
(<template>), logikę TypeScript/JavaScript
(<script>) oraz arkusz stylów CSS
(<style>) powiązane z jednym komponentem w
pojedynczym pliku z rozszerzeniem
.vue.
- Izolacja stylów
(
<style scoped>): Domyślnie reguły CSS wpisane w bloku<style>obowiązują globalnie w całej aplikacji. Dodanie atrybutuscopedsprawia, że kompilator Vue automatycznie dodaje do elementów HTML tego komponentu unikalny atrybut (np.data-v-7ba5bd90) i zawęża selektory CSS (.fancy-btn[data-v-7ba5bd90]), gwarantując, że style nie „wyciekną” do innych komponentów.
2. Options API vs
Composition API (<script setup>)
Vue 3 wspiera dwa style pisania komponentów:
| Cecha | Options API (Klasyczny) | Composition API z
<script setup> (Nowoczesny standard) |
|---|---|---|
| Organizacja kodu | Podział według typów
opcji (data, computed,
methods, watch). |
Podział według cech funkcjonalnych (logicznych) – powiązane zmienne i funkcje leżą obok siebie. |
| Skalowalność w dużych plikach | Prowadzi do tzw. „eksplozji opcji” – kod jednej funkcji jest rozrzucony po całym pliku. | Wysoka czytelność i zwięzłość (brak słowa
this oraz zbędnego kodu boilerplate). |
| Współdzielenie logiki | Trudne – wymagało przestarzałych
mixins (kolizje nazw, niejasne źródło właściwości). |
Bardzo proste i bezpieczne dzięki funkcjom
Composables (useNazwa()). |
| Wsparcie dla TypeScript | Wymaga złożonego wnioskowania typów dla
kontekstu this. |
Natywne, pierwszorzędne wsparcie dla TypeScript (czyste zmienne i funkcje). |
Porównanie:
Komponent w Options API vs <script setup>
(Composition API)
Wariant A: Klasyczne Options API
(SinglePageComp.vue oraz App.vue)
<!-- App.vue (Options API) -->
<template>
<div id="app">
<single-page-comp></single-page-comp>
<other-name></other-name>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import SinglePageComp from './components/SinglePageComp.vue';
export default defineComponent({
name: 'App',
components: {
SinglePageComp,
'other-name': SinglePageComp
},
data() {
return {
msg: 'Witaj w aplikacji Vue.js'
};
}
});
</script>
Wariant B: Nowoczesne Composition API z
<script setup> (Zalecane) W bloku
<script setup lang="ts"> każdy zaimportowany
komponent oraz każda zadeklarowana zmienna lub funkcja najwyższego
poziomu są automatycznie dostępne w szablonie
<template> bez konieczności ich ręcznego
zwracania!
<!-- App.vue (Composition API <script setup>) -->
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import SinglePageComp from './components/SinglePageComp.vue';
// 1. Stan reaktywny (ref):
// W kodzie JS/TS odwołujemy się przez właściwość '.value', natomiast w <template> Vue automatycznie ją odpakowuje!
const firstName = ref('Jan');
const lastName = ref('Kowalski');
const question = ref('');
const answer = ref('Pytania zwykle kończą się znakiem "?" ;-)');
// 2. Właściwość obliczona (computed):
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
// 3. Obserwator (watch):
watch(question, (newQuestion) => {
if (newQuestion.includes('?')) {
answer.value = 'Myślę...';
setTimeout(() => {
answer.value = 'Odpowiedź brzmi: 42.';
}, 1500);
}
});
</script>
<template>
<SinglePageComp />
<input v-model="firstName" placeholder="Imię" />
<input v-model="lastName" placeholder="Nazwisko" />
<p>Pełne imię: {{ fullName }}</p>
<input v-model="question" placeholder="Zadaj pytanie z '?'" />
<p>{{ answer }}</p>
</template>
3.
Nowoczesna implementacja Slotów, Propsów i defineModel()
(Vue 3.4 / 3.5+)
W komponentach jednoplikowych z <script setup>
korzystamy z makr kompilatora (nie wymagają one importowania z
'vue'):
A. Komponent ze
slotem w SFC (FancyButton.vue):
<!-- src/components/FancyButton.vue -->
<template>
<button class="fancy-btn">
<slot></slot>
</button>
</template>
<style scoped>
.fancy-btn {
background: #42b983;
color: white;
padding: 10px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
}
</style>
B.
Komponent z v-model w SFC (CustomInput.vue):
Klasyczne defineProps vs Nowoczesne
defineModel()
Począwszy od Vue 3.4+, zamiast ręcznego deklarowania
defineProps(['modelValue']) oraz
defineEmits(['update:modelValue']), zaleca się użycie makra
defineModel(), które zwraca gotową
referencję ref zsynchronizowaną z rodzicem:
<!-- src/components/CustomInput.vue -->
<script setup lang="ts">
// NOWOCZESNY STANDARD (Vue 3.4+):
// Makro defineModel() automatycznie rejestruje prop 'modelValue' oraz zdarzenie 'update:modelValue'!
const model = defineModel<string>({ default: '' });
// NOWOŚĆ W VUE 3.5+: Reaktywna destrukturyzacja Propsów z wartościami domyślnymi:
const { label = 'Wprowadź tekst:' } = defineProps<{
label?: string;
}>();
</script>
<template>
<label>
{{ label }}
<!-- Możemy bezpośrednio użyć v-model="model" na wewnętrznym inpucie! -->
<input v-model="model" placeholder="Własny input" />
</label>
</template>
💡 Nowość w Vue 3.5+:
useTemplateRef()do obsługi referencji DOM
Gdy potrzebujemy bezpośredniego odwołania do elementu HTML w<script setup>(np.<input ref="myInput">), od wersji Vue 3.5 stosujemy dedykowaną funkcjęconst inputEl = useTemplateRef<HTMLInputElement>('myInput');, która jest znacznie czytelniejsza niż dawne tworzenie pustegoconst myInput = ref(null)o tej samej nazwie co atrybut w szablonie.
4.
Reużywalna logika stanowa: Funkcje Composables
(use*)
Jedną z największych zalet Composition API jest możliwość
wyizolowania powtarzalnej logiki stanowej (wraz z ref,
watch i hakami cyklu życia) do zewnętrznych funkcji zwanych
Composables. Zgodnie z konwencją ich nazwy zawsze
rozpoczynają się od prefiksu use (np.
useMousePosition, useFetch). Każde wywołanie
funkcji Composable w komponencie tworzy niezależny, odizolowany stan dla
tego komponentu.
Przykład
1: Śledzenie pozycji kursora myszy
(src/composables/useMousePosition.ts)
import { ref, onMounted, onUnmounted } from 'vue';
export function useMousePosition() {
const x = ref(0);
const y = ref(0);
function update(event: MouseEvent) {
x.value = event.pageX;
y.value = event.pageY;
}
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
return { x, y };
}
Użycie w dowolnym komponencie:
<script setup lang="ts">
import { useMousePosition } from '@/composables/useMousePosition';
const { x, y } = useMousePosition();
</script>
<template>
<p>Pozycja myszy to: {{ x }}, {{ y }}</p>
</template>
Przykład
2: Uniwersalne pobieranie danych asynchronicznych
(src/composables/useFetch.ts)
Funkcja useFetch zarządza trzema stanami zapytania HTTP
(data, error, loading) oraz
udostępnia metodę refetch. Wykorzystanie funkcji
pomocniczej toValue() (wprowadzonej w Vue 3.3+) pozwala
przekazać jako argument zarówno zwykły tekst URL, jak i reaktywny
ref lub funkcję getter, automatycznie ponawiając zapytanie
przy zmianie adresu URL:
import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue';
export function useFetch<T = unknown>(url: MaybeRefOrGetter<string>) {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const loading = ref<boolean>(false);
async function fetchData() {
loading.value = true;
error.value = null;
try {
const response = await fetch(toValue(url));
if (!response.ok) {
throw new Error(`Błąd HTTP: ${response.status}`);
}
data.value = (await response.json()) as T;
} catch (err) {
error.value = err instanceof Error ? err : new Error(String(err));
} finally {
loading.value = false;
}
}
// watchEffect automatycznie śledzi toValue(url) i wywołuje fetchData przy każdej jego zmianie
watchEffect(() => {
fetchData();
});
return {
data,
error,
loading,
refetch: fetchData
};
}
8.
Routing w Aplikacjach SPA (Vue Router 4), Typowany Routing
Plikowy (unplugin-vue-router) oraz
TanStack Query
Cel: Budowa nawigacji w aplikacjach SPA za pomocą
Vue Router 4, wdrożenie automatycznego, silnie typowanego
routingu opartego na strukturze plików
(unplugin-vue-router) oraz zarządzanie stanem serwera z
biblioteką @tanstack/vue-query (na wzór ekosystemu React +
TanStack).
1. Klasyczna konfiguracja
Vue Router 4
Vue Router 4 to oficjalny router dla Vue 3, pozwalający mapować ścieżki adresów URL przeglądarki na odpowiednie komponenty widoków bez przeładowywania całej strony.
- W wersji CDN dołączamy skrypt:
<script src="https://unpkg.com/vue-router@4"></script>. - W projekcie Vite zaznaczamy opcję
Add Vue Routerw kreatorze lub instalujemy pakiet:npm install vue-router@4.
Podstawowa konfiguracja routera
(src/router/index.ts):
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
import HomeView from '@/views/HomeView.vue';
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'home',
component: HomeView
},
{
// Leniwe ładowanie komponentu (Lazy Loading / Code Splitting) - plik JS zostanie
// pobrany dopiero wtedy, gdy użytkownik odwiedzi ścieżkę '/about'
path: '/about',
name: 'about',
component: () => import('@/views/AboutView.vue')
},
{
// Ścieżka z parametrem dynamicznym :id przekazywanym jako prop do komponentu
path: '/users/:id',
name: 'user-details',
component: () => import('@/views/UserDetailsView.vue'),
props: true
}
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
});
export default router;
Rejestracja w main.ts (app.use(router))
oraz użycie w głównym szablonie App.vue:
<template>
<header>
<nav>
<!-- RouterLink generuje znacznik <a> przechwytujący kliknięcie bez przeładowania strony -->
<RouterLink to="/">Strona główna</RouterLink> |
<RouterLink to="/about">O projekcie</RouterLink> |
<RouterLink :to="{ name: 'user-details', params: { id: 42 } }">Profil #42</RouterLink>
</nav>
</header>
<main>
<!-- RouterView to dynamiczny kontener, w którym renderowany jest komponent dopasowany do URL -->
<RouterView />
</main>
</template>
2.
Nowoczesny Routing Plikowy i Pełne Bezpieczeństwo Typów:
unplugin-vue-router
W klasycznym vue-router każdą nową podstronę musimy
ręcznie dopisywać do tablicy routes, a literówka w ścieżce
<RouterLink to="/uzytkownicy/15"> nie jest wykrywana
przez kompilator TypeScript.
Wtyczka unplugin-vue-router (tworzona
przez autora Vue Routera i Pinia – Eduardo San Martin Morote) wprowadza
do Vue architekturę znaną z TanStack Router (React)
oraz Nuxt:
- Automatyczne generowanie tras na podstawie plików
(File-Based Routing): Wystarczy utworzyć plik
.vuew katalogusrc/pages/, a trasa powstaje automatycznie! - 100% bezpieczeństwo typów TypeScript (Type-Safe
Routing): Wtyczka generuje w locie plik deklaracji
typed-router.d.ts, dzięki czemu edytor VS Code podpowiada poprawne ścieżki i parametry w<RouterLink>orazuseRoute(), a każdą literówkę podkreśla jako błąd kompilacji!
Konfiguracja
unplugin-vue-router:
npm install -D unplugin-vue-router
W pliku vite.config.ts umieszczamy wtyczkę
VueRouter() PRZED wtyczką
vue():
// vite.config.ts
import { defineConfig } from 'vite';
import VueRouter from 'unplugin-vue-router/vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
VueRouter({
routesFolder: 'src/pages',
dts: 'src/typed-router.d.ts' // Automatycznie generowany plik typów tras
}),
vue()
]
});
W pliku src/router/index.ts importujemy trasy z
wirtualnego modułu vue-router/auto-routes:
import { createRouter, createWebHistory } from 'vue-router';
import { routes } from 'vue-router/auto-routes';
export const router = createRouter({
history: createWebHistory(),
routes
});
Konwencja
nazewnictwa plików w katalogu src/pages/:
Ścieżka pliku w
src/pages/ |
Wygenerowany adres URL | Nazwa trasy w TypeScript |
|---|---|---|
src/pages/index.vue |
/ |
'/' |
src/pages/about.vue |
/about |
'/about' |
src/pages/users/index.vue |
/users |
'/users/' |
src/pages/users/[id].vue |
/users/:id (parametr
dynamiczny) |
'/users/[id]' |
src/pages/[...path].vue |
/:path(.*) (strona błędu 404
Catch-all) |
'/[...path]' |
Odczyt w pełni typowanego parametru w komponencie
src/pages/users/[id].vue:
<script setup lang="ts">
import { useRoute } from 'vue-router';
// Przekazujemy nazwę trasy - TypeScript automatycznie wie, że route.params posiada właściwość 'id'!
const route = useRoute('/users/[id]');
</script>
<template>
<h1>Szczegóły użytkownika o ID: {{ route.params.id }}</h1>
</template>
3.
Zarządzanie Stanem Serwera z TanStack Query
(@tanstack/vue-query)
W nowoczesnych aplikacjach SPA rozróżniamy dwa fundamentalnie odmienne rodzaje stanu:
- Stan Klienta (Client State – np. Pinia): Synchroniczny stan należący wyłącznie do przeglądarki (np. otwarty modal, wybrany motyw ciemny/jasny, zawartość lokalnego koszyka).
- Stan Serwera (Server State –
@tanstack/vue-query): Asynchroniczne dane przechowywane na zdalnym backendzie API, które mogą ulec zmianie bez naszej wiedzy, wymagają buforowania (Caching), deduplikacji identycznych zapytań, odświeżania w tle (Stale-While-Revalidate), paginacji oraz optymistycznych aktualizacji.
Połączenie unplugin-vue-router +
@tanstack/vue-query w Vue zapewnia dokładnie tę
samą potężną architekturę pracy z danymi, co połączenie TanStack
Router + TanStack Query w React.
Instalacja i
rejestracja TanStack Query:
npm install @tanstack/vue-query
// W pliku src/main.ts:
import { createApp } from 'vue';
import { VueQueryPlugin } from '@tanstack/vue-query';
import App from './App.vue';
const app = createApp(App);
app.use(VueQueryPlugin); // Rejestracja globalnego klienta QueryClient
app.mount('#app');
Pobieranie
danych (useQuery) i modyfikacja z automatycznym
odświeżeniem cache (useMutation):
<!-- src/pages/users/index.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query';
interface User {
id: number;
name: string;
email: string;
}
const queryClient = useQueryClient();
const newUserName = ref('');
// 1. POBIERANIE I CACHOWANIE DANYCH (useQuery):
// Zapytanie jest identyfikowane kluczem ['users']. Jeśli inny komponent poprosi o ['users']
// w ciągu 60 sekund (staleTime), TanStack Query zwróci dane z pamięci RAM bez uderzania do sieci!
const { isPending, isError, data: users, error } = useQuery<User[]>({
queryKey: ['users'],
queryFn: async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
if (!res.ok) throw new Error('Błąd pobierania listy użytkowników');
return res.json();
},
staleTime: 60 * 1000 // Dane są uznawane za "świeże" przez 60 sekund
});
// 2. MODYFIKACJA DANYCH NA SERWERZE (useMutation):
const createUserMutation = useMutation({
mutationFn: async (name: string) => {
const res = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
body: JSON.stringify({ name, email: `${name.toLowerCase()}@uczelnia.pl` }),
headers: { 'Content-Type': 'application/json' }
});
return res.json();
},
onSuccess: () => {
// Automatyczne unieważnienie klucza ['users'] wymusza pobranie świeżej listy w tle!
queryClient.invalidateQueries({ queryKey: ['users'] });
newUserName.value = '';
}
});
</script>
<template>
<div>
<h2>Lista Użytkowników (TanStack Query)</h2>
<form @submit.prevent="createUserMutation.mutate(newUserName)">
<input v-model="newUserName" placeholder="Imię nowego użytkownika" required />
<button type="submit" :disabled="createUserMutation.isPending.value">
{{ createUserMutation.isPending.value ? 'Zapisywanie...' : 'Dodaj użytkownika' }}
</button>
</form>
<p v-if="isPending">Ładowanie danych z serwera...</p>
<p v-else-if="isError">Wystąpił błąd: {{ error?.message }}</p>
<ul v-else>
<li v-for="user in users" :key="user.id">
<strong>{{ user.name }}</strong> ({{ user.email }})
</li>
</ul>
</div>
</template>
9. Zarządzanie Globalnym Stanem Aplikacji (Pinia) i System Wtyczek (Plugins)
Cel: Centralizacja stanu klienta zgodnie ze wzorcem Single Source of Truth przy użyciu biblioteki Pinia (w składni Option Stores oraz Setup Stores) oraz wykorzystanie zewnętrznych wtyczek Vue.
1. Magazyn stanu Pinia (Single Source of Truth – SSOT)
W dużych aplikacjach przekazywanie danych przez dziesiątki poziomów
props (Prop Drilling) i zdarzeń emits
staje się nieczytelne. Pinia (oficjalny następca
dawnego Vuex) realizuje wzorzec Single Source of Truth
(SSOT), udostępniając modularne, w pełni typowane magazyny
danych (Stores).
- Instalacja:
npm install pinia(oraz rejestracja wmain.ts:app.use(createPinia())).
Każdy magazyn Pinia składa się z trzech filarów:
- State (
stan): Odpowiednikdata/refw komponencie. Przechowuje aktualne dane. - Getters (
gettery): Odpowiednik właściwości obliczonych (computed). Buforowane wartości wyliczane na podstawie stanu. - Actions (
akcje): Odpowiednik metod (methods). Mogą być synchroniczne lub asynchroniczne i służą do modyfikacji stanu oraz wykonywania logiki biznesowej.
Wariant
A: Magazyn w składni Option Store (state,
getters, actions)
// src/stores/counterStore.ts
import { defineStore } from 'pinia';
interface UserInfo {
name: string;
age: number;
}
interface CounterState {
count: number;
userList: UserInfo[];
user: UserInfo | null;
}
export const useCounterStore = defineStore('counter', {
// 1. STATE: Funkcja zwracająca stan początkowy (bezpieczna dla SSR)
state: (): CounterState => ({
count: 0,
userList: [],
user: null
}),
// 2. GETTERS: Odpowiednik computed
getters: {
// Typ zwracany (number) jest wnioskowany automatycznie z parametru state:
doubleCount(state) {
return state.count * 2;
},
// Gdy w getterze używamy słowa 'this' (odwołując się do innego gettera),
// w TypeScript musimy jawnie zadeklarować typ zwracany (: number):
doublePlusOne(): number {
return this.doubleCount + 1;
}
},
// 3. ACTIONS: Odpowiednik methods (NIE używamy tu funkcji strzałkowych, bo potrzebujemy 'this'!)
actions: {
increment() {
this.count++;
},
randomizeCounter() {
this.count = Math.round(100 * Math.random());
}
}
});
Wariant B: Magazyn w nowoczesnej składni Setup Store (Composition API)
Pinia pozwala również definiować magazyny dokładnie tak samo jak
komponenty <script setup>, gdzie:
ref()stają się właściwościami state,computed()stają się getterami,function()stają się akcjami:
// src/stores/contactsStore.ts
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
export interface Contact {
id: number;
name: string;
phone: string;
}
export const useContactsStore = defineStore('contacts', () => {
const contactsMode = ref(true);
const contactsList = ref<Contact[]>([
{ id: 1, name: 'Jan Kowalski', phone: '+48 500 100 200' },
{ id: 2, name: 'Anna Nowak', phone: '+48 600 300 400' }
]);
const totalContacts = computed(() => contactsList.value.length);
function addContact(contact: Contact) {
contactsList.value.push(contact);
}
return { contactsMode, contactsList, totalContacts, addContact };
});
⚠️ Bezpieczna destrukturyzacja magazynu Pinia (
storeToRefs):
Zwykła destrukturyzacja obiektu magazynuconst { count, doubleCount } = useCounterStore();niszczy reaktywność zmiennych stanu i getterów! Aby wypakować właściwości stanu i gettery do osobnych zmiennych z zachowaniem reaktywności, zawsze używaj funkcji pomocniczejstoreToRefs():import { storeToRefs } from 'pinia'; const store = useCounterStore(); // Stan i gettery wypakowujemy przez storeToRefs(): const { count, doubleCount } = storeToRefs(store); // Akcje (metody) możemy wypakować bezpośrednio ze store'a: const { increment, randomizeCounter } = store;
2.
System Wtyczek Vue (Plugins) na przykładzie
vue-good-table-next
Wtyczki (Plugins) rozszerzają możliwości aplikacji na
poziomie globalnym lub komponentowym (dodając nowe komponenty, dyrektywy
lub funkcje globalne za pomocą app.use(NazwaWtyczki)).
Zainstalujmy wtyczkę udostępniającą zaawansowaną tabelę danych z obsługą sortowania, filtrowania i paginacji:
npm install vue-good-table-next
Użycie
tabeli w połączeniu z magazynem Pinia w
<script setup> (Zalecane):
<!-- src/components/ContactsTable.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import { VueGoodTable } from 'vue-good-table-next';
import 'vue-good-table-next/dist/vue-good-table-next.css';
import { useContactsStore } from '@/stores/contactsStore';
// 1. Pobieramy instancję magazynu Pinia
const contactsStore = useContactsStore();
const { contactsMode, contactsList } = storeToRefs(contactsStore);
// 2. Definiujemy kolumny tabeli
const columns = ref([
{ label: 'Imię i nazwisko', field: 'name' },
{ label: 'Telefon', field: 'phone' }
]);
</script>
<template>
<div v-if="contactsMode" class="content">
<h2>Książka kontaktowa ({{ contactsStore.totalContacts }}):</h2>
<VueGoodTable
:columns="columns"
:rows="contactsList"
:search-options="{ enabled: true }"
:pagination-options="{ enabled: true, perPage: 5 }"
/>
</div>
</template>
💡 Alternatywa: Integracja vue-good-table-next oraz Pinia w klasycznym Options API (mapState) (Kliknij, aby rozwinąć/zwinąć)
W starszych komponentach opartych na Options API do mapowania stanu z
magazynu Pinia na właściwości obliczone komponentu wykorzystujemy
funkcję pomocniczą mapState:
<script lang="ts">
import { defineComponent } from 'vue';
import { VueGoodTable } from 'vue-good-table-next';
import 'vue-good-table-next/dist/vue-good-table-next.css';
import { mapState } from 'pinia';
import { useContactsStore } from '@/stores/contactsStore';
export default defineComponent({
components: { VueGoodTable },
data() {
return {
columns: [
{ label: 'Imię i nazwisko', field: 'name' },
{ label: 'Telefon', field: 'phone' }
]
};
},
computed: {
...mapState(useContactsStore, ['contactsMode', 'contactsList'])
}
});
</script>
10. Nowoczesne Stylowanie Interfejsu z Tailwind CSS (v4 oraz v3)
Cel: Integracja frameworka CSS typu Utility-First (Tailwind CSS) z projektem Vue + Vite.
Tailwind CSS to najpopularniejszy framework CSS
działający w paradygmacie Utility-First. Zamiast
wymyślać dziesiątki nazw klas w osobnych plikach arkuszy stylów,
budujemy nowoczesny, responsywny interfejs bezpośrednio w szablonie
<template> za pomocą atomowych klas narzędziowych
(np.
flex items-center justify-between bg-emerald-600 text-white px-4 py-2 rounded-lg shadow-md hover:bg-emerald-700).
Podczas budowania projektu kompilator skanuje pliki .vue i
generuje wyłącznie te reguły CSS, których faktycznie użyliśmy w
kodzie.
1.
Konfiguracja najnowszej wersji Tailwind CSS v4 (z wtyczką
@tailwindcss/vite)
W wersji Tailwind CSS v4 konfiguracja została
radykalnie uproszczona – nie są już wymagane pliki
postcss.config.js ani tailwind.config.js:
- Instalacja pakietów:
npm install tailwindcss @tailwindcss/vite - Dodanie wtyczki w pliku
vite.config.ts:import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ plugins: [ vue(), tailwindcss() ] }); - Import w głównym pliku arkusza stylów
./src/assets/main.css(lub./src/index.css):@import "tailwindcss"; - Zaimportowanie pliku CSS w
./src/main.ts:import { createApp } from 'vue'; import App from './App.vue'; import './index.css'; createApp(App).mount('#app');
💡 Konfiguracja klasyczna (Tailwind CSS v3 z PostCSS – dla starszych projektów) (Kliknij, aby rozwinąć/zwinąć)
W projektach korzystających z linii Tailwind CSS v3 konfiguracja opiera się na procesorze PostCSS:
- Instalacja zależności:
npm install -D tailwindcss@^3 postcss autoprefixer npx tailwindcss init -p - Wskazanie ścieżek do szablonów w
tailwind.config.js:/** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}", ], theme: { extend: {} }, plugins: [], }; - Dodanie dyrektyw w pliku
./src/index.css:@tailwind base; @tailwind components; @tailwind utilities;
11. Diagnostyka i Debugowanie Aplikacji Vue.js
Cel: Opanowanie narzędzi inspekcji i usuwania błędów w projektach Vue.js – od zintegrowanego panelu Vue DevTools, przez konsolę przeglądarki, po zaawansowane konfiguracje debuggera w Visual Studio Code.
1. Zintegrowane narzędzia Vue DevTools w Vite
Projekty utworzone przez create-vue posiadają wtyczkę
vite-plugin-vue-devtools, która udostępnia zaawansowany
inspektor bezpośrednio w przeglądarce bez konieczności instalowania
zewnętrznych rozszerzeń z Chrome Web Store:
- Włączenie nakładki w oknie aplikacji: Naciśnij
skrót
Alt + Shift + Dpodczas pracy z serwerem deweloperskim (http://localhost:5173). - Otwarcie w osobnym oknie przeglądarki: Przejdź pod
adres
http://localhost:5173/__devtools__/. - Najważniejsze funkcje diagnostyczne:
- Drzewo komponentów (Components): Podgląd
hierarchii
<App>,<RouterView>oraz edycja na żywo wartościprops,datai zmiennychref/reactive. - Inspektor Pinia (Pinia): Podgląd wszystkich magazynów stanu, oś czasu wywoływanych akcji (Timeline) oraz tzw. Time-Travel Debugging (cofanie stanu aplikacji do wybranego momentu w przeszłości).
- Inspektor Routingu i graf zależności Vite (Routes &
Assets): Weryfikacja dopasowanych ścieżek, parametrów
$routeoraz rozmiaru modułów.
- Drzewo komponentów (Components): Podgląd
hierarchii
2. Debugowanie w konsoli przeglądarki i Haki Śledzenia Renderowania
W prostych aplikacjach korzystających z CDN możemy przypisać wynik
app.mount('#app') do globalnej zmiennej
window.vm = app.mount('#app') i badać stan bezpośrednio w
konsoli DevTools (F12).
W komponentach <script setup> (oraz Options API)
Vue 3 udostępnia ponadto dwa specjalne haki diagnostyczne cyklu
życia, które pozwalają precyzyjnie ustalić,
dlaczego dany komponent został ponownie
wyrenderowany:
<script setup lang="ts">
import { onRenderTracked, onRenderTriggered } from 'vue';
// Wywoływany, gdy Vue rejestruje zależność reaktywną podczas renderowania:
onRenderTracked((event) => {
console.log('Śledzona właściwość:', event.key, 'na obiekcie:', event.target);
});
// Wywoływany, gdy zmiana właściwości wymusza ponowne przerysowanie komponentu:
onRenderTriggered((event) => {
console.log('Przerysowanie wywołane przez zmianę:', event.key, 'z:', event.oldValue, 'na:', event.newValue);
// Możesz tu odkomentować instrukcję 'debugger;', aby zatrzymać wykonanie dokładnie w momencie mutacji!
// debugger;
});
</script>
3.
Konfiguracja Debuggera w Visual Studio Code (launch.json
oraz Auto Attach)
Visual Studio Code pozwala stawiać punkty przerwania
(Breakpoints – klawisz F9) bezpośrednio w plikach
.vue oraz .ts:
- Tryb Auto Attach: Z palety poleceń
(
Ctrl + Shift + P) wybierzDebug: Toggle Auto Attach(tryb Smart lub Always). Od tej chwili uruchomienie skryptu w terminalu VS Code (Ctrl + \`` $\rightarrow$npm run dev`) automatycznie podłączy debugger Node.js. - Przycisk Debug w
package.json: Po otwarciu plikupackage.jsonnad sekcją"scripts"pojawia się przycisk▷ Debug, pozwalający uruchomić wybrany skrypt w trybie śledzenia. - Konfiguracja złożona (
compounds) w pliku.vscode/launch.json(Zalecana):
Pozwala jednym naciśnięciem klawiszaF5automatycznie uruchomić w tle serwer deweloperski Vite (npm run dev) oraz otworzyć powiązaną sesję debugowania przeglądarki Chrome/Edge z obsługą map źródłowych (Source Maps):
{
"version": "0.2.0",
"configurations": [
{
"type": "msedge",
"request": "launch",
"name": "Przeglądarka (Edge :5173)",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/src"
},
{
"type": "node-terminal",
"request": "launch",
"name": "Serwer Vite (npm run dev)",
"command": "npm run dev",
"cwd": "${workspaceFolder}"
}
],
"compounds": [
{
"name": "Uruchom Full-Stack Debug (Vite + Edge)",
"configurations": ["Serwer Vite (npm run dev)", "Przeglądarka (Edge :5173)"],
"stopAll": true
}
]
}
12. Kompleksowa Strategia Testowania: Vitest, Vue Test Utils i Playwright (Page Object Model)
Cel: Opanowanie trójpoziomowej piramidy testowania
aplikacji Vue.js: testów jednostkowych (Vitest), testów
integracyjnych komponentów (@vue/test-utils) oraz testów
End-to-End w Playwright z zastosowaniem wzorca projektowego
Page Object Model (POM).
/\
/ \ 3. Testy E2E (Playwright + Page Object Model)
/────\ Prawdziwa przeglądarka, pełny przepływ użytkownika
/ \
/────────\ 2. Testy Komponentów (@vue/test-utils + Vitest)
/ \ Montowanie komponentów, Props, Emits, Sloty, DOM
/────────────\
/ \ 1. Testy Jednostkowe (Vitest)
/________________\ Czyste funkcje TS/JS, Composables, Magazyny Pinia
1.
Poziom I: Testy Jednostkowe (Vitest) i Testowanie
Composables
Vitest to ultraszybki framework testowy stworzony
przez zespół Vue/Vite. Korzysta z tej samej konfiguracji
vite.config.ts i udostępnia API w pełni kompatybilne z Jest
(describe, test, it,
expect, vi).
- Uruchomienie testów jednostkowych w trybie obserwowania zmian:
npm run test:unit.
A. Testowanie
czystej funkcji (sum.ts oraz
sum.spec.ts):
// src/utils/sum.ts
export function sum(a: number, b: number): number {
return a + b;
}
// src/utils/__tests__/sum.spec.ts
import { expect, test } from 'vitest';
import { sum } from '../sum';
test('dodaje 1 + 2 zwracając wynik 3', () => {
expect(sum(1, 2)).toBe(3);
});
B.
Testowanie niezależnej funkcji Composable
(useCounter.ts):
Jeśli funkcja Composable korzysta wyłącznie z reaktywności
(ref, computed, watch) i nie
wymaga haków cyklu życia DOM, możemy testować ją bezpośrednio jak zwykłą
funkcję:
// src/composables/useCounter.ts
import { ref } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const increment = () => count.value++;
return { count, increment };
}
// src/composables/__tests__/useCounter.spec.ts
import { expect, test } from 'vitest';
import { useCounter } from '../useCounter';
test('useCounter poprawnie inicjalizuje i zwiększa stan licznika', () => {
const { count, increment } = useCounter(10);
expect(count.value).toBe(10);
increment();
expect(count.value).toBe(11);
});
2. Poziom
II: Testy Komponentów (@vue/test-utils +
Vitest)
Testy komponentów sprawdzają, czy komponent poprawnie renderuje
szablon HTML na podstawie przekazanych props, reaguje na
kliknięcia użytkownika i emituje odpowiednie zdarzenia
emits. Testujemy zawsze publiczny kontrakt
komponentu, a nie jego wewnętrzne zmienne prywatne.
Biblioteka @vue/test-utils udostępnia
dwie metody montowania komponentu w wirtualnym środowisku DOM
(jsdom / happy-dom):
shallowMount(Komponent, opcje)(Montowanie płytkie): Automatycznie podmienia wszystkie zagnieżdżone komponenty potomne na puste zaślepki (stubs). Jest bardzo szybka i gwarantuje pełną izolację testowanego komponentu.mount(Komponent, opcje)(Montowanie pełne): Renderuje testowany komponent wraz z całym drzewem jego komponentów podrzędnych.
// src/components/__tests__/WeatherHeader.spec.ts
import { describe, it, expect } from 'vitest';
import { shallowMount } from '@vue/test-utils';
import WeatherHeader from '../WeatherHeader.vue';
describe('Komponent WeatherHeader.vue', () => {
it('renderuje przekazany tytuł w props oraz emituje zdarzenie odświeżenia po kliknięciu', async () => {
// 1. Arrange & Act: Płytkie zamontowanie komponentu z przekazanym propsem 'title'
const wrapper = shallowMount(WeatherHeader, {
props: {
title: 'Prognoza Pogody Vue Project'
}
});
// 2. Assert: Weryfikacja wygenerowanego tekstu w drzewie DOM
expect(wrapper.text()).toMatch('Prognoza Pogody Vue Project');
// 3. Symulacja kliknięcia przycisku odświeżania i weryfikacja wyemitowanego zdarzenia
const refreshBtn = wrapper.find('button.refresh-btn');
if (refreshBtn.exists()) {
await refreshBtn.trigger('click');
expect(wrapper.emitted()).toHaveProperty('refresh');
}
});
});
3.
Poziom III: Testy End-to-End (E2E) z użyciem Playwright i
wzorca Page Object Model (POM)
Testy End-to-End (E2E) weryfikują działanie kompletnego, uruchomionego systemu z perspektywy prawdziwego użytkownika. Nie stosuje się tu atrap komponentów – narzędzie testowe uruchamia prawdziwy silnik przeglądarki (Chromium, Firefox oraz WebKit/Safari), otwiera adres aplikacji i wykonuje rzeczywiste kliknięcia, wypełnianie formularzy oraz zapytania sieciowe.
W nowoczesnych projektach Vue standardem przemysłowym do testów E2E
jest Microsoft Playwright
(@playwright/test), oferujący:
- Mechanizm Auto-Waiting: Playwright
automatycznie czeka, aż element pojawi się w DOM, stanie się widoczny i
gotowy do kliknięcia (eliminuje potrzebę stosowania sztucznych opóźnień
sleep). - Odporne lokatory użytkownika (User-Facing
Locators): Zamiast kruchych selektorów CSS
(
.div > span:nth-child(2)), używamy lokatorów semantycznych:page.getByRole(),page.getByLabel(),page.getByPlaceholder(),page.getByTestId(). - Narzędzie Trace Viewer: Nagrywa pełny
zrzut DOM, konsoli i zapytań sieciowych dla każdego kroku testu
(
npx playwright show-trace).
Dlaczego stosujemy wzorzec Page Object Model (Model Obiektu Strony – POM)?
Gdy piszemy dziesiątki testów E2E i w każdym z nich bezpośrednio
powtarzamy te same selektory
(await page.getByPlaceholder('Dodaj zadanie...').fill('...')),
drobna zmiana tekstu w szablonie .vue powoduje awarię 50
plików testowych naraz!
Wzorzec Page Object Model (POM) rozwiązuje ten problem poprzez separację warstw:
- Klasa Page Object
(
e2e/pages/TodoPage.ts): Hermetyzuje wszystkie lokatory elementów danej podstrony oraz udostępnia metody wysokiego poziomu reprezentujące akcje użytkownika (np.addTodo('Kupić mleko'),toggleTodo(0)). Nie zawiera asercjiexpect! - Plik specyfikacji testu
(
e2e/todo.spec.ts): Korzysta z metod obiektu strony i skupia się wyłącznie na scenariuszu biznesowym oraz asercjach sprawdzających wynik (expect). Jeśli w przyszłości zmieni się struktura HTML formularza, aktualizujemy lokator tylko w jednym miejscu – w klasieTodoPage!
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
│ TEST SPECYFIKACJI (e2e/todo.spec.ts) │ │ PAGE OBJECT (e2e/pages/TodoPage.ts) │
│ │ │ │
│ Scenariusz biznesowy i asercje expect: │ │ Lokatory i operacje na stronie: │
│ 1. await todoPage.goto() │ ────► │ - readonly input: Locator │
│ 2. await todoPage.addTask('Raport') │ │ - readonly submitBtn: Locator │
│ 3. expect(todoPage.items).toHaveCount()│ │ + async addTask(title: string) │
└─────────────────────────────────────────┘ └────────────────────┬────────────────────┘
│ Steruje przez CDP
▼
┌─────────────────────────────────────────┐
│ Przeglądarka (Chromium/Firefox/WebKit) │
└─────────────────────────────────────────┘
Krok 1:
Implementacja klasy modelu strony
(e2e/pages/TodoPage.ts)
import { type Page, type Locator } from '@playwright/test';
export class TodoPage {
readonly page: Page;
readonly newTodoInput: Locator;
readonly addButton: Locator;
readonly todoItems: Locator;
readonly completedCounter: Locator;
constructor(page: Page) {
this.page = page;
// Definiujemy odporne lokatory semantyczne w jednym centralnym miejscu:
this.newTodoInput = page.getByPlaceholder('Co jest do zrobienia?');
this.addButton = page.getByRole('button', { name: 'Dodaj zadanie' });
this.todoItems = page.getByTestId('todo-item');
this.completedCounter = page.getByTestId('completed-count');
}
// Nawigacja do badanej podstrony aplikacji Vue
async goto() {
await this.page.goto('/todos');
}
// Akcja wysokiego poziomu: dodanie nowego zadania przez interfejs
async addTask(taskTitle: string) {
await this.newTodoInput.fill(taskTitle);
await this.addButton.click();
}
// Akcja wysokiego poziomu: oznaczenie zadania o podanej nazwie jako wykonane
async toggleTask(taskTitle: string) {
const item = this.todoItems.filter({ hasText: taskTitle });
await item.getByRole('checkbox').check();
}
}
Krok
2: Implementacja czytelnego testu E2E korzystającego z POM
(e2e/todo.spec.ts)
import { test, expect } from '@playwright/test';
import { TodoPage } from './pages/TodoPage';
test.describe('Moduł listy zadań (E2E z Page Object Model)', () => {
test('pozwala użytkownikowi dodać nowe zadania i oznaczyć je jako ukończone', async ({ page }) => {
// 1. Inicjalizacja obiektu Page Object Model
const todoPage = new TodoPage(page);
// 2. Otwarcie strony aplikacji
await todoPage.goto();
// 3. Wykonanie akcji użytkownika za pomocą czytelnych metod POM
await todoPage.addTask('Przygotować sprawozdanie z laboratorium Vue 3');
await todoPage.addTask('Skonfigurować testy Playwright z POM');
// 4. Asercje (Playwright automatycznie poczeka na odświeżenie DOM przez Vue!)
await expect(todoPage.todoItems).toHaveCount(2);
await expect(todoPage.todoItems.first()).toContainText('Przygotować sprawozdanie');
// 5. Oznaczenie pierwszego zadania jako ukończone i sprawdzenie licznika
await todoPage.toggleTask('Przygotować sprawozdanie z laboratorium Vue 3');
await expect(todoPage.completedCounter).toHaveText('Ukończono: 1 z 2');
});
});
Uruchamianie testów Playwright z wiersza poleceń:
# Jednorazowe pobranie silników przeglądarek (Chromium, Firefox, WebKit):
npx playwright install
# Uruchomienie wszystkich testów E2E w trybie bezokienkowym (Headless):
npm run test:e2e
# Uruchomienie interaktywnego interfejsu graficznego Playwright UI Mode (zalecane podczas pisania testów!):
npx playwright test --ui