Zajęcia 2#
Vue.js#
Vue.js to progresywny framework JavaScript do budowania interfejsów użytkownika. Jest lekki, intuicyjny i świetny jako pierwszy framework frontendowy.
Spis treści#
- Czym jest Vue.js?
- Instalacja i konfiguracja
- Struktura projektu
- Komponenty i Single File Components (SFC)
- Template syntax – szablony
- Reaktywność –
refireactive - Dyrektywy
- Obsługa zdarzeń
- Computed properties i watchers
- Cykl życia komponentu
- Props i komunikacja między komponentami
- Emit – komunikacja w górę
- Composables (logika wielokrotnego użytku)
- Vue Router – podstawy
- Pinia – zarządzanie stanem
- Podsumowanie i co dalej
1. Czym jest Vue.js?#
Vue.js (wersja 3) to framework oparty na komponentach, który:
- Deklaratywnie renderuje HTML na podstawie stanu aplikacji
- Używa Composition API (nowoczesne podejście) lub Options API
- Jest reaktywny – UI automatycznie aktualizuje się przy zmianie danych
- Łatwo integruje się z istniejącymi projektami
Vue vs React vs Angular#
| Cecha | Vue 3 | React | Angular |
|---|---|---|---|
| Krzywa uczenia | ✅ Łagodna | ⚠️ Średnia | ❌ Stroma |
| Rozmiar bundla | ✅ Mały | ✅ Mały | ❌ Duży |
| Two-way binding | ✅ Wbudowany | ⚠️ Ręczny | ✅ Wbudowany |
| Oficjalny router | ✅ Vue Router | ⚠️ 3rd party | ✅ Wbudowany |
2. Instalacja i konfiguracja#
Wymagania#
- Node.js >= 18
- npm, yarn lub pnpm (zalecany)
Tworzenie projektu przez Vite (zalecane)#
Kreator zapyta o opcje – dla początkujących wybierz: - ✅ Vue Router - ✅ Pinia - ❌ TypeScript (opcjonalnie, na start można pominąć)
Alternatywnie – CDN (do szybkiego testowania)#
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app">{{ message }}</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const message = ref('Cześć, Vue!')
return { message }
}
}).mount('#app')
</script>
3. Struktura projektu#
Po create vue dostaniesz:
moj-projekt/
├── public/ # Pliki statyczne
├── src/
│ ├── assets/ # Obrazki, fonty, style globalne
│ ├── components/ # Komponenty wielokrotnego użytku
│ ├── views/ # Strony (używane przez router)
│ ├── stores/ # Stan aplikacji (Pinia)
│ ├── router/
│ │ └── index.js # Definicja tras
│ ├── App.vue # Komponent główny
│ └── main.js # Punkt wejścia aplikacji
├── index.html
└── vite.config.js
4. Komponenty i Single File Components#
Każdy plik .vue to Self-Contained Component – zawiera logikę, szablon i style w jednym miejscu.
<!-- src/components/Powitanie.vue -->
<template>
<div class="powitanie">
<h1>Cześć, {{ imie }}!</h1>
<p>Licznik: {{ licznik }}</p>
<button @click="zwieksz">Kliknij mnie</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
// Props (dane z zewnątrz)
const props = defineProps({
imie: {
type: String,
default: 'Świecie'
}
})
// Stan lokalny
const licznik = ref(0)
function zwieksz() {
licznik.value++
}
</script>
<style scoped>
/* scoped = style działają tylko w tym komponencie */
.powitanie {
padding: 1rem;
border: 1px solid #ccc;
}
</style>
💡
<script setup>to skrócona składnia Composition API – zalecana w Vue 3.
5. Template Syntax – szablony#
Interpolacja tekstu#
Bindowanie atrybutów (v-bind lub :)#
<!-- Pełna składnia -->
<img v-bind:src="urlObrazka" />
<!-- Skrócona (zalecana) -->
<img :src="urlObrazka" :alt="opisObrazka" />
<button :disabled="czyNieaktywny">Wyślij</button>
Surowy HTML (v-html)#
6. Reaktywność – ref i reactive#
ref – dla wartości prostych (i nie tylko)#
import { ref } from 'vue'
const licznik = ref(0)
const imie = ref('Kuba')
const czyZalogowany = ref(false)
// W skrypcie odwołuj się przez .value
console.log(licznik.value) // 0
licznik.value++
console.log(licznik.value) // 1
// W szablonie .value jest zbędne – Vue robi to automatycznie
// {{ licznik }} → "1"
reactive – dla obiektów#
import { reactive } from 'vue'
const uzytkownik = reactive({
imie: 'Kuba',
wiek: 30,
adres: {
miasto: 'Warszawa'
}
})
// Modyfikacja – bez .value
uzytkownik.wiek = 31
uzytkownik.adres.miasto = 'Kraków'
Kiedy używać czego?#
ref |
reactive |
|---|---|
| Prymitywy (string, number...) | Złożone obiekty / tablice |
| Gdy potrzebujesz reassign | Gdy modyfikujesz właściwości |
| Domyślny wybór (bezpieczniejszy) | Wygodniejsza składnia dla obiektów |
💡 Dla uproszczenia – używaj
refdo wszystkiego, aż poczujesz potrzebęreactive.
7. Dyrektywy#
v-if, v-else-if, v-else – renderowanie warunkowe#
<div v-if="wynik > 90">Ocena: 5</div>
<div v-else-if="wynik > 75">Ocena: 4</div>
<div v-else-if="wynik > 60">Ocena: 3</div>
<div v-else>Ocena: 2</div>
v-show – ukrywanie przez CSS#
<!-- Element istnieje w DOM, ale display: none gdy false -->
<p v-show="czyWidoczny">Ten tekst można ukryć</p>
Użyj
v-showgdy element często zmienia widoczność,v-ifgdy warunek rzadko się zmienia.
v-for – pętle#
<ul>
<li v-for="produkt in produkty" :key="produkt.id">
{{ produkt.nazwa }} – {{ produkt.cena }} zł
</li>
</ul>
<!-- Z indeksem -->
<div v-for="(element, index) in lista" :key="index">
{{ index + 1 }}. {{ element }}
</div>
⚠️ Zawsze dodawaj
:key– Vue potrzebuje go do efektywnego re-renderowania.
v-model – two-way binding#
<input v-model="szukana" placeholder="Wpisz szukaną frazę..." />
<p>Szukasz: {{ szukana }}</p>
<!-- Checkbox -->
<input type="checkbox" v-model="zgodaRodo" />
<!-- Select -->
<select v-model="wybraneMiasto">
<option>Warszawa</option>
<option>Kraków</option>
<option>Gdańsk</option>
</select>
8. Obsługa zdarzeń#
v-on / @#
<!-- Pełna i skrócona składnia -->
<button v-on:click="kliknij">Kliknij</button>
<button @click="kliknij">Kliknij</button>
<!-- Inline -->
<button @click="licznik++">+1</button>
<!-- Z argumentem -->
<button @click="usun(produkt.id)">Usuń</button>
<!-- Z obiektem zdarzenia -->
<input @keyup.enter="zatwierdz" />
<form @submit.prevent="wyslij">...</form>
Modyfikatory zdarzeń#
@click.stop <!-- event.stopPropagation() -->
@click.prevent <!-- event.preventDefault() -->
@click.once <!-- wywołaj tylko raz -->
@keyup.enter <!-- tylko klawisz Enter -->
@keyup.esc <!-- tylko klawisz Escape -->
9. Computed Properties i Watchers#
computed – wartości pochodne (z cache'owaniem)#
import { ref, computed } from 'vue'
const produkty = ref([
{ nazwa: 'Kawa', cena: 15, kategoria: 'napoje' },
{ nazwa: 'Herbata', cena: 8, kategoria: 'napoje' },
{ nazwa: 'Ciastko', cena: 6, kategoria: 'przekąski' },
])
const filtr = ref('')
// Przelicza się tylko gdy produkty lub filtr się zmienią
const przefiltrowane = computed(() =>
produkty.value.filter(p =>
p.nazwa.toLowerCase().includes(filtr.value.toLowerCase())
)
)
const sumaCen = computed(() =>
produkty.value.reduce((sum, p) => sum + p.cena, 0)
)
watch – reagowanie na zmiany#
import { ref, watch, watchEffect } from 'vue'
const szukana = ref('')
// Reaguje na zmianę konkretnej wartości
watch(szukana, (nowaWartosc, staraWartosc) => {
console.log(`Zmieniono z "${staraWartosc}" na "${nowaWartosc}"`)
pobierzWyniki(nowaWartosc)
})
// watchEffect – uruchamia się od razu i śledzi zależności automatycznie
watchEffect(() => {
console.log(`Aktualna fraza: ${szukana.value}`)
})
10. Cykl życia komponentu#
import { onMounted, onUpdated, onUnmounted, onBeforeMount } from 'vue'
// Najczęściej używane:
onMounted(() => {
// Komponent jest w DOM – tutaj pobierasz dane z API
console.log('Komponent zamontowany')
pobierzDane()
})
onUpdated(() => {
// Wywołane po każdej aktualizacji DOM
})
onUnmounted(() => {
// Sprzątanie: zatrzymaj timery, odsubskrybuj eventy
clearInterval(timer)
})
Przykład – pobieranie danych#
import { ref, onMounted } from 'vue'
const posty = ref([])
const ladowanie = ref(true)
const blad = ref(null)
onMounted(async () => {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5')
posty.value = await res.json()
} catch (err) {
blad.value = 'Nie udało się pobrać danych'
} finally {
ladowanie.value = false
}
})
11. Props – komunikacja z rodzica do dziecka#
<!-- Rodzic: App.vue -->
<template>
<KartaUzytkownika
:imie="uzytkownik.imie"
:wiek="uzytkownik.wiek"
:aktywny="true"
/>
</template>
<!-- Dziecko: KartaUzytkownika.vue -->
<script setup>
const props = defineProps({
imie: {
type: String,
required: true
},
wiek: {
type: Number,
default: 0
},
aktywny: {
type: Boolean,
default: false
}
})
</script>
<template>
<div :class="{ aktywny: props.aktywny }">
<h2>{{ props.imie }}</h2>
<p>Wiek: {{ props.wiek }}</p>
</div>
</template>
⚠️ Props są readonly – dziecko nie powinno ich modyfikować!
12. Emit – komunikacja z dziecka do rodzica#
<!-- Dziecko: FormularzKomentarza.vue -->
<script setup>
import { ref } from 'vue'
const emit = defineEmits(['dodajKomentarz', 'anuluj'])
const tresc = ref('')
function wyslij() {
if (tresc.value.trim()) {
emit('dodajKomentarz', { tresc: tresc.value, data: new Date() })
tresc.value = ''
}
}
</script>
<template>
<div>
<textarea v-model="tresc" placeholder="Twój komentarz..."></textarea>
<button @click="wyslij">Wyślij</button>
<button @click="emit('anuluj')">Anuluj</button>
</div>
</template>
<!-- Rodzic -->
<FormularzKomentarza
@dodaj-komentarz="handleNowyKomentarz"
@anuluj="pokazFormularz = false"
/>
13. Composables – wielokrotna logika#
Composable to funkcja, która opakowuje logikę reaktywną do ponownego użycia.
// src/composables/useLicznik.js
import { ref, computed } from 'vue'
export function useLicznik(poczatkowa = 0) {
const wartosc = ref(poczatkowa)
const podwojena = computed(() => wartosc.value * 2)
function zwieksz(o = 1) { wartosc.value += o }
function zmniejsz(o = 1) { wartosc.value -= o }
function reset() { wartosc.value = poczatkowa }
return { wartosc, podwojena, zwieksz, zmniejsz, reset }
}
<!-- Użycie w komponencie -->
<script setup>
import { useLicznik } from '@/composables/useLicznik'
const { wartosc, podwojena, zwieksz, zmniejsz, reset } = useLicznik(10)
</script>
<template>
<p>Wartość: {{ wartosc }} (podwojona: {{ podwojena }})</p>
<button @click="zwieksz()">+1</button>
<button @click="zmniejsz()">-1</button>
<button @click="reset()">Reset</button>
</template>
14. Vue Router – podstawy#
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Strona from '@/views/Strona.vue'
import OFirmie from '@/views/OFirmie.vue'
import Produkt from '@/views/Produkt.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Strona },
{ path: '/o-firmie', component: OFirmie },
{ path: '/produkt/:id', component: Produkt }, // parametr dynamiczny
]
})
export default router
<!-- Nawigacja w szablonie -->
<nav>
<RouterLink to="/">Strona główna</RouterLink>
<RouterLink to="/o-firmie">O firmie</RouterLink>
<RouterLink :to="`/produkt/${produkt.id}`">{{ produkt.nazwa }}</RouterLink>
</nav>
<!-- Tutaj renderują się strony -->
<RouterView />
// Programowa nawigacja w skrypcie
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// Odczyt parametru URL: /produkt/42
console.log(route.params.id) // "42"
// Nawigacja
router.push('/o-firmie')
router.push({ path: `/produkt/${id}` })
router.back()
15. Pinia – zarządzanie stanem#
Pinia to oficjalny store Vue 3 – zastąpił Vuex.
// src/stores/koszyk.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useKoszykStore = defineStore('koszyk', () => {
// Stan
const produkty = ref([])
// Computed (jak getters)
const liczbaProduktow = computed(() => produkty.value.length)
const sumaKoszyka = computed(() =>
produkty.value.reduce((sum, p) => sum + p.cena * p.ilosc, 0)
)
// Akcje
function dodaj(produkt) {
const istniejacy = produkty.value.find(p => p.id === produkt.id)
if (istniejacy) {
istniejacy.ilosc++
} else {
produkty.value.push({ ...produkt, ilosc: 1 })
}
}
function usun(id) {
produkty.value = produkty.value.filter(p => p.id !== id)
}
function wyczysc() {
produkty.value = []
}
return { produkty, liczbaProduktow, sumaKoszyka, dodaj, usun, wyczysc }
})
<!-- Użycie w komponencie -->
<script setup>
import { useKoszykStore } from '@/stores/koszyk'
const koszyk = useKoszykStore()
</script>
<template>
<p>Produktów w koszyku: {{ koszyk.liczbaProduktow }}</p>
<p>Suma: {{ koszyk.sumaKoszyka }} zł</p>
<button @click="koszyk.dodaj(produkt)">Dodaj do koszyka</button>
</template>
16. Podsumowanie i co dalej#
Czego się nauczyłeś#
| Temat | Słowa kluczowe |
|---|---|
| Komponenty | .vue, <script setup>, SFC |
| Reaktywność | ref(), reactive() |
| Szablony | {{ }}, :bind, @event |
| Dyrektywy | v-if, v-for, v-model, v-show |
| Logika pochodna | computed(), watch() |
| Cykl życia | onMounted(), onUnmounted() |
| Komunikacja | props, emit |
| Wielokrotna logika | Composables |
| Routing | Vue Router, RouterLink, RouterView |
| Stan globalny | Pinia, defineStore |
Dalsze kroki#
- Oficjalna dokumentacja: vuejs.org/guide – najlepsza w branży
- TypeScript z Vue 3 – silne typowanie, bardzo polecane
- Nuxt.js – meta-framework Vue z SSR, file-based routing
- VueUse – biblioteka gotowych composables (useFetch, useStorage, useGeolocation...)
- Vitest + Vue Test Utils – testowanie komponentów
- Vite – dogłębne poznanie bundlera
Zadanie 2#
Vue.js: Aplikacje Reaktywne
Zasady ogólne#
- Projekt tworzysz przy użyciu Vite + Vue 3 (
npm create vue@latest) - Używasz wyłącznie Composition API ze składnią
<script setup> - Każdy komponent powinien znajdować się w osobnym pliku
.vue - Kod powinien być czytelny i opatrzony komentarzami tam, gdzie to konieczne
- Niedozwolone jest używanie gotowych bibliotek komponentów (Vuetify, PrimeVue itp.)
- Stylowanie: dowolne (czysty CSS, SCSS, Tailwind – do wyboru)
- Oddajesz repozytorium Git (GitHub / GitLab) lub spakowany folder projektu
Część 1 – Kalkulator BMI (10 pkt)#
Opis zadania#
Stwórz aplikację w Vue 3, która oblicza wskaźnik BMI (Body Mass Index) na podstawie danych wprowadzonych przez użytkownika.
Wzór:
Wymagania funkcjonalne#
Aplikacja musi:
- Posiadać dwa pola tekstowe (
<input>) do wprowadzenia: - masy ciała w kilogramach
- wzrostu w centymetrach
- Na bieżąco (bez przycisku) obliczać i wyświetlać wynik BMI z dokładnością do 2 miejsc po przecinku
- Wyświetlać kategorię wagową odpowiadającą wyniku:
| BMI | Kategoria |
|---|---|
| poniżej 18.5 | Niedowaga |
| 18.5 – 24.99 | Waga prawidłowa |
| 25.0 – 29.99 | Nadwaga |
| 30.0 i powyżej | Otyłość |
- Wyświetlać kategorię w innym kolorze dla każdego przedziału
- Gdy pola są puste lub dane są niepoprawne – nie wyświetlać wyniku (żadnych błędów
NaN)
Wymagania techniczne#
| Wymaganie | Punkty |
|---|---|
Poprawne użycie v-model na obu polach |
1 pkt |
Obliczenie BMI przez computed() |
1 pkt |
Warunkowe wyświetlanie kategorii (v-if) |
1 pkt |
| Kolorowanie kategorii + obsługa błędnych danych | 1 pkt |
| Razem (merytoryczne) | 4 pkt |
Pozostałe 6 punktów#
| Wymaganie | Punkty |
|---|---|
| Realizacja na zajęciach (dopuszczalny work in progress, byle nie blank page) | 2 pkt |
| Kod na GitHubie | 2 pkt |
| Działające demo online (dowolny serwer, np. GitHub Pages, Render, GCP/AWS/Azure free tier, MIKR.US od 35 zł/rok, lub prezentacja lokalna na zajęciach) | 2 pkt |
| Razem (organizacyjne) | 6 pkt |
Wskazówki#
- Wartości z
<input>są zawsze stringiem – użyjparseFloat()do konwersji - Sprawdź czy wzrost i waga są liczbami większymi od 0 przed obliczeniem
- Pamiętaj o konwersji centymetrów na metry przed podstawieniem do wzoru
- Kategorie najwygodniej zwrócić z osobnej funkcji pomocniczej lub drugiego
computed
Część 2 – Aplikacja Quizowa (14 pkt)#
Opis zadania#
Stwórz wielokomponentową aplikację quizową w Vue 3. Quiz powinien zawierać minimum 5 pytań jednokrotnego wyboru (4 odpowiedzi do każdego pytania). Pytania mogą dotyczyć dowolnego tematu.
Wymagania funkcjonalne#
Aplikacja musi składać się z trzech ekranów:
Ekran 1 – Start#
- Wyświetla tytuł quizu i ewentualny opis
- Zawiera przycisk „Rozpocznij quiz" uruchamiający rozgrywkę
Ekran 2 – Pytanie#
- Wyświetla numer pytania (np. „Pytanie 3 / 5") i pasek postępu
- Wyświetla treść aktualnego pytania
- Pokazuje 4 przyciski z odpowiedziami do wyboru
- Po wybraniu odpowiedzi:
- Podświetla wybraną odpowiedź na zielono (poprawna) lub czerwono (błędna)
- Podświetla poprawną odpowiedź jeśli wybrano błędną
- Blokuje możliwość zmiany odpowiedzi
- Aktywuje przycisk „Następne pytanie"
- Przejście do następnego pytania czyści zaznaczenie i pokazuje kolejne
Ekran 3 – Wynik#
- Wyświetla liczbę poprawnych odpowiedzi (np. „4 / 5")
- Wyświetla procentowy wynik i słowną ocenę:
| Wynik | Komunikat |
|---|---|
| 100% | Doskonale! Bezbłędny wynik! 🏆 |
| 80–99% | Bardzo dobrze! 🎉 |
| 60–79% | Nieźle, ale jest pole do poprawy |
| poniżej 60% | Warto powtórzyć materiał 📚 |
- Zawiera przycisk „Zagraj ponownie" resetujący quiz do stanu początkowego
Wymagania techniczne i architektura#
Aplikacja powinna być podzielona na co najmniej 3 komponenty:
App.vue
├── EkranStart.vue # Ekran powitalny
├── EkranPytanie.vue # Pojedyncze pytanie + odpowiedzi
└── EkranWynik.vue # Podsumowanie
Dane pytań definiujesz jako tablicę obiektów w App.vue lub osobnym pliku questions.js:
// Przykładowa struktura danych
const pytania = [
{
id: 1,
tresc: 'Jakie rozszerzenie mają pliki komponentów Vue?',
odpowiedzi: ['.jsx', '.vue', '.component', '.html'],
poprawna: 1 // indeks poprawnej odpowiedzi (0-based)
},
// ...
]
Kryteria oceniania#
| Wymaganie | Punkty |
|---|---|
| Trzy ekrany działają poprawnie, nawigacja między nimi | 2 pkt |
| Poprawna struktura komponentów, props i emit | 2 pkt |
| Podświetlanie odpowiedzi (zielony/czerwony) + blokada po wyborze | 2 pkt |
| Ekran wyników z oceną słowną + reset quizu | 1 pkt |
| Pasek postępu + licznik pytań | 1 pkt |
| Razem (merytoryczne) | 8 pkt |
Pozostałe 6 punktów#
| Wymaganie | Punkty |
|---|---|
| Realizacja na zajęciach (dopuszczalny work in progress, byle nie blank page) | 2 pkt |
| Kod na GitHubie | 2 pkt |
| Działające demo online (dowolny serwer, np. GitHub Pages, Render, GCP/AWS/Azure free tier, MIKR.US od 35 zł/rok, lub prezentacja lokalna na zajęciach) | 2 pkt |
| Razem (organizacyjne) | 6 pkt |
Wskazówki#
- Stan aktualnego ekranu trzymaj w
App.vuejakoref(np.'start' | 'quiz' | 'wynik') - Do przełączania ekranów użyj
v-if/v-else-ifna komponentach - Wynik (liczba poprawnych odpowiedzi) trzymaj w
App.vuei przekazuj doEkranWynikprzezprops - Komunikacja dziecko → rodzic (wybór odpowiedzi, przejście dalej) przez
emit - Pamiętaj o resetowaniu indeksu pytania i wyniku przy ponownym starcie
Punktacja końcowa#
| Część | Merytoryczne | Organizacyjne | Max. punktów |
|---|---|---|---|
| Część 1 – Kalkulator BMI | 4 pkt | 6 pkt | 10 pkt |
| Część 2 – Quiz | 8 pkt | 6 pkt | 14 pkt |
| Łącznie | 12 pkt | 12 pkt | 24 pkt |
⚠️ Projekty, które się nie uruchamiają lub są plagiatem, otrzymują 0 punktów.
Powodzenia! 🚀