Wcześniej pokazałem, jak rozpocząć pracę z Vuei obiecałem, że będzie druga część, i proszę bardzo!
Spoiler alert: będzie nawet trzecia seria, ale do rzeczy!
Skoro znamy już podstawy Vue, musimy teraz zagłębić się w platformę Vue, aby stworzyć niestandardowy komponent. Najpierw tworzymy komponent reprezentujący każdego gracza w naszej drużynie piłkarskiej. W trakcie całego procesu uczymy się kilku podstawowych pojęć, takich jak wywoływanie komponentów w innym komponencie, wysyłanie danych do komponentu za pośrednictwem rekwizytów lub stosowanie podejścia provide/inject.
Podstawy komponentów
Komponenty są instancjami Vue wielokrotnego użytku. W aplikacji, nad którą zacząłem pracować w ostatnim artykule, znajduje się główny komponent: App.vue.
Wszelkie inne komponenty akceptują te same właściwości, co komponent główny: dane, obliczone, metody, obserwacje itp.
Trzy części komponentu w Vue
Pojedynczy plik komponentu vue składa się z trzech części: Składnia HTML określająca widok wizualny dla komponentu. JavaScript zapewnia listę właściwości do tworzenia komponentu, przy użyciu standardowej składni eksportu modułów JavaScript w JS. Arkusze stylów są używane do definiowania najlepszych interfejsów użytkownika.
Należy utworzyć nowy plik (zazwyczaj w folderze komponentów). Nazwijmy ten plik: PlayerCard.vue.
Najpierw dodaj sekcję szablonu z elementami HTML:
<template>
<h1>This is a new component</h1>
</template>
Eksport komponentów Vue
Szczerze mówiąc, przez długi czas myślałem, że muszę dodać kolejną sekcję pod sekcją Szablony: i dodać obiekt eksportu, taki jak ten:
<script>
export default {}
</script>
Nie jest to jednak konieczne. Komponenty może mieć tylko w sekcję template. W każdym razie, zazwyczaj w aplikacjach znajdują się bardziej zaawansowane komponenty, więc dobrze jest dodać domyślny obiekt eksportu z właściwością name.
<template>
<h1>This is a new component</h1>
</template>
<script>
export default {
name: 'PlayerdCard'
}
</script>
Jak importować komponenty Vue
Ponieważ wyeksportowaliśmy komponenty, możemy użyć ich w innym miejscu. W tym przypadku zaimportujemy komponent PlayerdCard Vue do komponentu App. Aby to zrobić, zróbmy to:
Importowanie komponentów podrzędnych do komponentów nadrzędnych wewnątrz znaczników skryptu
<script>
import PlayerCard from './components/PlayerCard.vue';
export default {
name: 'App',
(...)
Rejestracja komponentu podrzędnego w komponencie nadrzędnym
Musimy poinformować komponent App, że może korzystać z komponentu PlayerdCard. Istnieje właściwość components , która jest odpowiedzialna za rejestrowanie komponentów:
<script>
import PlayerCard from './components/PlayerCard.vue';
export default {
name: 'App',
components: { PlayerCard },
(...)
Na koniec: Użyj komponentu podrzędnego do renderowania szablonu komponentu podrzędnego.
Aby wyrenderować komponent PlayerCard, należy umieścić niestandardowy element wewnątrz szablonu komponentu App. Zaimportowaliśmy komponent jako PlayerCard, więc jest to nazwa elementu niestandardowego:
<template>
<PlayerCard/>
</template>
<script>
import PlayerCard from './components/PlayerCard.vue';
export default {
name: 'App',
components: { PlayerCard },
// other stuff below
data() {
return {
players: [
]
}
},
computed: {
},
methods: {
watch: {
}
}
</script>
<style>
</style>
Komponenty globalne i lokalne
Zarejestrowaliśmy globalnie nasz pierwszy komponent w App.vue, komponent główny. Jest to typowe podejście, ale czasami określone komponenty są używane w mniejszej części aplikacji.
Vue pozwala na rejestrowanie komponentów w innych komponentach. Możesz więc zarejestrować komponent w instancji komponentu PlayerCard, a następnie użyć go w dowolnych jego elementach potomnych.
Oznacza to, że komponent jest zarejestrowany lokalnie i nie będzie dostępny w komponentach nadrzędnych – w naszym przypadku w obiekcie komponentu aplikacji.
Komunikacja między komponentami
Komponent PlayerdCard działa, ale nie jest zbyt pomocny. Dodajmy do niego trochę treści:
<template>
<div class="player">
<h1>{{ player.name }}</h1>
<img :src="player.image" :alt="player.name" />
<p><strong>{{ player.club }}, {{ player.country }}</strong></p>
<p>Position: {{ player.position }}</p>
<p>
Price: {{ player.price }}
<strong v-if="player.playerLabel">{{ player.playerLabel }}</strong>
</p>
<button type="button" @click="toggleForSale()">
<span v-if="player.forSale">Remove from transfer list</span>
<span v-if="!player.forSale">Add to transfer list</span>
</button>
</div>
</template>
Obiekt gracza jest używany w wielu miejscach, ale obiekt komponentu nie zawiera danych gracza. Jak przekazać dane gracza do komponentu?
Wprowadzenie rekwizytów
Aby przekazać dane do komponentów, można użyć props. Props to atrybuty HTML, które można, powiedzmy, przenieść do zakresu komponentu.
Po pierwsze, musimy zdefiniować, że komponent PlayerCard może odbierać właściwości gracza:
// PlayerCard.vue
export default {
name: 'PlayerdCard',
props: ['player']
}
Następnie możemy przekazać właściwość gracza do PlayerCard w App.vue
<template>
<PlayerCard :player="players[0]"/>
</template>
Player to tablica zdefiniowana w danych komponentu App:
export default {
name: 'App',
components: { PlayerCard },
data() {
return {
players: [
{
id: 1,
name: 'Mo Salah',
club: 'Liverpool FC',
country: 'Egypt',
position: 'Striker',
price: 2000,
image: './avatar.png',
forSale: false
},
{
id: 2,
name: 'Robert Lewandowski',
club: 'FC Bayern',
country: 'Poland',
position: 'Striker',
price: 3000,
image: './avatar.png',
forSale: true
}
]
}
}
}
</script>
Aby przykład był bardziej dokładny, wykonajmy iterację przez tablicę graczy za pomocą dyrektywy v for i wyświetlmy wszystkich graczy na ekranie:
<template>
<PlayerCard v-for="player in players" :key="player.id" :player="player"/>
</template>
Obsługiwane props
Powyżej przekazaliśmy obiekt jako właściwość, ale można również podać inne typy. Przyjrzyj się typom props obsługiwanym przez Vue:
- String
- Liczba
- Wartość logiczna
- Tablica
- Obiekt
- Data
- Funkcja
- Symbol
Sprawdzanie poprawności props
Kiedy zarejestrowałem właściwość w komponencie PlayerCard, po prostu wkleiłem nazwę do tablicy właściwości:
props: ['player']
ale istnieje opcja walidacji props. Zobacz:
props: {
player: Object
}
Teraz komponent oczekuje, że gracz będzie obiektem, więc każdy inny typ przekazany do komponentu spowoduje wyświetlenie ostrzeżenia w konsoli.
Możesz również określić, że rekwizyt jest wymagany:
props: {
player: Object,
required: true
}
Wartość domyślna:
props: {
player: Object,
default: {
name: 'Bot'
}
}
Poza tym jest więcej opcji. Postępuj zgodnie z dokumentacją Vue, aby poznać je wszystkie. Ostatnią rzeczą, którą chcę tutaj zrobić, jest powtarzanie dokumentów Vue.
Po tym kroku aplikacja renderuje dwóch graczy:
Wydarzenia niestandardowe: komunikacja dziecko-rodzic
Nasz pierwszy komponent Vue działa, ale jest kilka błędów. Kiedy klikam „dodaj do listy transferowej”, widzę błąd w konsoli:
Uncaught TypeError: _ctx.toggleForSale is not a function
Dzieje się tak, ponieważ w PlayerCard nie mamy funkcji toggleForSale. Funkcja ta znajduje się w zakresie komponentu App Vue…:
methods: {
toggleForSale() {
this.forSale = !this.forSale;
}
},
Tak czy inaczej, ta funkcja jest przestarzała. Zadziałało to w przypadku, gdy mieliśmy jednego gracza. Teraz mamy tablicę graczy, więc musimy zaktualizować funkcję toggleForSale, aby to umożliwić.
methods: {
toggleForSale(playerId) {
const player = this.players.find(player => player.id === playerId);
player.forSale = !player.forSale;
}
},
Teraz funkcja otrzymuje parametr playerId, aby zidentyfikować, który gracz powinien zostać zmodyfikowany, ale nadal – funkcja toggleForSale jest w zakresie komponentu App. Czy możliwe jest wywołanie tej metody z innych komponentów Vue? Zwłaszcza z komponentów dla dzieci?
Jest na to sposób: niestandardowe wydarzenia!
Definiowanie zdarzeń niestandardowych
Aby zdefiniować zdarzenie, należy dodać je do tablicy emits w następujący sposób:
export default {
name: 'PlayerdCard',
props: ['player'],
emits: ['toggle-for-sale']
}
Technicznie nie jest to obowiązkowe, ale jest to dobra praktyka, aby definiować zdarzenia, ponieważ wtedy łatwiej jest zrozumieć, jak działają komponenty.
Następnym krokiem jest wyemitowanie naszego niestandardowego zdarzenia, gdy użytkownik kliknie przycisk:
<button type="button" @click="$emit('toggle-for-sale', player.id)">
<span v-if="player.forSale">Remove from transfer list</span>
<span v-if="!player.forSale">Add to transfer list</span>
</button>
Na koniec musimy nasłuchiwać tego zdarzenia w komponencie App:
1// App.vue
<template>
<PlayerCard
v-for="player in players"
:key="player.id"
:player="player"
@toggle-for-sale="toggleForSale"
/>
</template>
Dla niestandardowego zdarzenia @toggle-for-sale powiązałem funkcję toggleForSale dostępną w App.js i teraz funkcjonalność działa zgodnie z oczekiwaniami.
Sprawdzanie poprawności zdarzeń niestandardowych
Tak samo jak props, zdarzenia niestandardowe mogą być walidowane. Jest to możliwe dzięki zdefiniowaniu emiterów nie jako tablic, ale jako obiektów. Każda właściwość tego obiektu to niestandardowa nazwa zdarzenia i jest to funkcja sprawdzania poprawności:
emits: {
'toggle-for-sale': (playerId) => {
if (!playerId) {
console.warn('Player ID is missing')
return false;
}
return true
}
}
Prop drilling
Jeśli trafiłeś tutaj z Reacta, prawdopodobnie znasz powszechny problem zwany: props drilling. Jeśli nie, pozwól mi szybko wyjaśnić na przykładzie.
Dodamy dwa komponenty do komponentu PlayerCard:
- PlayerData
- PlayerAttributes
Zanim zaczniemy wdrażać te komponenty, dodajemy kilka dodatkowych danych do naszych graczy:
players: [
{
id: 1,
name: 'Mo Salah',
club: 'Liverpool FC',
country: 'Egypt',
position: 'Striker',
price: 2000,
image: './avatar.png',
forSale: false,
birthday: '10/10/2000',
growth: '180cm',
betterLeg: 'right',
speed: 94,
shooting: 90,
passes: 89,
dribble: 99,
defense: 30,
physical: 85
},
{
id: 2,
name: 'Robert Lewandowski',
club: 'FC Bayern',
country: 'Poland',
position: 'Striker',
price: 3000,
image: './avatar.png',
forSale: false,
birthday: '20/05/2000',
growth: '190cm',
betterLeg: 'right',
speed: 90,
shooting: 99,
passes: 89,
dribble: 85,
defense: 78,
physical: 89
}
]
Składnik PlayerData
// PlayerCard/PlayerData.vue
<template>
<div class="player-data">
<ul>
<li>
<strong>Birthday: </strong>
<span>{{ player.birthday }}</span>
</li>
<li>
<strong>Growth: </strong>
<span>{{ player.growth }}</span>
</li>
<li>
<strong>Better leg: </strong>
<span>{{ player.betterLeg }}</span>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'PlayerData',
props: ['player'],
}
</script>
Ten komponent renderuje pewne informacje o zawodniku, takie jak data urodzenia, wzrost i lepsza noga.
Zaimportujmy go i zarejestrujmy w komponencie playerCard Vue:
<script>
import PlayerData from './PlayerCard/PlayerData.vue'
export default {
name: 'PlayerdCard',
props: ['player'],
emits: ['toggle-for-sale'],
components: {
PlayerData
}
}
</script>
Teraz możemy użyć go w sekcji szablonu:
// PlayerCard.vue
<template>
<div class="player">
(...)
<div class="additional-data">
<PlayerData :player="player"/>
</div>
</div>
</template>
Składnik PlayerAttributes
Ten komponent będzie prawie taki sam. Różnica polega na tym, że renderuje inne dane:
<template>
<div class="player-attributes">
<ul>
<li>
<strong>Speed: </strong>
<span>{{ player.speed }}</span>
</li>
<li>
<strong>Shooting: </strong>
<span>{{ player.shooting }}</span>
</li>
<li>
<strong>Passes: </strong>
<span>{{ player.passes }}</span>
</li>
<li>
<strong>Dribble: </strong>
<span>{{ player.passes }}</span>
</li>
<li>
<strong>Passes: </strong>
<span>{{ player.dribble }}</span>
</li>
<li>
<strong>Defense: </strong>
<span>{{ player.defense }}</span>
</li>
<li>
<strong>Physical: </strong>
<span>{{ player.physical }}</span>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'PlayerData',
props: ['player'],
}
</script>
Uwaga: na razie te komponenty wyglądają tak samo, ale w przyszłości zamierzam dodać tam więcej logiki, więc nie martw się, mam pomysł 😆
Ostatnią rzeczą w tym kroku jest zarejestrowanie, zaimportowanie i użycie nowego komponentu w PlayerCard.
Ostateczny kod komponentu PlayerCard:
<template>
<div class="player">
<h1>{{ player.name }}</h1>
<img :src="player.image" :alt="player.name" />
<p><strong>{{ player.club }}, {{ player.country }}</strong></p>
<p>Position: {{ player.position }}</p>
<p>
Price: {{ player.price }}
<strong v-if="player.playerLabel">{{ player.playerLabel }}</strong>
</p>
<button type="button"
@click="$emit('toggle-for-sale', player.id)">
<span v-if="player.forSale">Remove from transfer list</span>
<span v-if="!player.forSale">Add to transfer list</span>
</button>
<div class="additional-data">
<PlayerData :player="player"/>
<PlayerAttributes :player="player"/>
</div>
</div>
</template>
<script>
import PlayerData from './PlayerCard/PlayerData.vue';
import PlayerAttributes from './PlayerCard/PlayerAttributes.vue';
export default {
name: 'PlayerdCard',
props: ['player'],
emits: ['toggle-for-sale'],
components: {
PlayerData,
PlayerAttributes
}
}
</script>
Jest to renderowany playerCard w przeglądarce:
Chciałem ci pokazać problem z wierceniem rekwizytów, jeśli zapomniałeś, i proszę bardzo. Mamy komponent PlayerdCard, który odbiera gracza jako właściwość, a ten komponent ma dzieci: PlayerData i PlayerAttributes, które również otrzymują gracza jako rekwizyt.
Co więcej, komponenty podrzędne PlayerCard również mogą mieć dzieci, które potrzebują gracza. Problem prop drilling polega na przekazywaniu props od rodzica do dziecka. Może to być problematyczne i frustrujące, jeśli masz duże drzewo komponentów.
W aplikacji React istnieje sposób, aby poradzić sobie z tym w inny sposób – używając Context. W vue jest coś znajomego.
provide/inject jako rozwiązanie
Zamiast przekazywać props do każdego komponentu podrzędnego, można podać dane w komponencie nadrzędnym i wstrzyknąć je do komponentów podrzędnych. Zobacz:
export default {
name: 'App',
components: { PlayerCard },
data() {
return {
players: [
{
id: 1,
name: 'Mo Salah',
club: 'Liverpool FC',
country: 'Egypt',
position: 'Striker',
price: 2000,
image: './avatar.png',
forSale: false,
birthday: '10/10/2000',
growth: '180',
betterLeg: 'right',
speed: 94,
shooting: 90,
passes: 89,
dribble: 99,
defense: 30,
physical: 85
},
{
id: 2,
name: 'Robert Lewandowski',
club: 'FC Bayern',
country: 'Poland',
position: 'Striker',
price: 3000,
image: './avatar.png',
forSale: false,
birthday: '20/05/2000',
growth: '190',
betterLeg: 'right',
speed: 90,
shooting: 99,
passes: 89,
dribble: 85,
defense: 78,
physical: 89
}
]
}
},
provide: {
players: this.players
},
}
Następnie w każde dziecko można wstrzyknąć graczy:
export default {
name: 'PlayerData',
inject: ['player'],
}
Po wstrzyknięciu gracza do komponentów PlayerData i PlayerAttributes, mogę usunąć z nich atrybut gracza w komponencie PlayerCard:
<div class="additional-data">
<PlayerData />
<PlayerAttributes/>
</div>
Aplikacja nadal działa, ale dane są przekazywane w inny sposób.
Komponenty dynamiczne
Ostatnią rzeczą, którą chcę pokazać, są komponenty dynamiczne. Teraz mamy dwa komponenty w komponencie PlayerCard: PlayerData i PlayerAttributes, i są one renderowane na ekranie. Chciałbym mieć coś takiego jak zakładki i możliwość zmiany aktywnej zakładki. Wówczas widoczny będzie tylko jeden komponent odpowiadający wybranej zakładce.
Najpierw utwórz nawigację dla zakładek:
// PlayerCard.js
<nav>
<button type="button" @click="selectTab('PlayerData')">Player Data</button>
<button type="button" @click="selectTab('PlayerAttributes')">Player Attributes</button>
</nav>
Powiązałem metodę selectTab ze zdarzeniem kliknięcia, więc stwórzmy tę metodę:
methods: {
selectTab(tab) {
this.selectedTab = tab;
}
},
Ponadto dodaj nową właściwość danych z polem selectedTab:
data() {
return {
selectedTab: 'PlayerData'
}
},
Na koniec wyrenderujmy nasze komponenty dynamicznie, dodając konkretny komponent
<div class="additional-data">
<component :is="selectedTab"/>
</div>
Dzięki temu możemy renderować dynamiczne komponenty na podstawie właściwości danych, tak jak w tym przykładzie. Gdy właściwość zostanie zmieniona, Vue dynamicznie przełącza komponent i renderuje go.
Kompletny kod zawierający dynamiczne komponenty:
<template>
<div class="player">
<h1>{{ player.name }}</h1>
<img :src="player.image" :alt="player.name" />
<p><strong>{{ player.club }}, {{ player.country }}</strong></p>
<p>Position: {{ player.position }}</p>
<p>
Price: {{ player.price }}
<strong v-if="player.playerLabel">{{ player.playerLabel }}</strong>
</p>
<button type="button"
@click="$emit('toggle-for-sale', player.id)">
<span v-if="player.forSale">Remove from transfer list</span>
<span v-if="!player.forSale">Add to transfer list</span>
</button>
<nav>
<button type="button" @click="selectTab('PlayerData')">Player Data</button>
<button type="button" @click="selectTab('PlayerAttributes')">Player Attributes</button>
</nav>
<div class="additional-data">
<component :is="selectedTab"/>
</div>
</div>
</template>
<script>
import PlayerData from './PlayerCard/PlayerData.vue';
import PlayerAttributes from './PlayerCard/PlayerAttributes.vue';
export default {
name: 'PlayerdCard',
props: ['player'],
emits: ['toggle-for-sale'],
provide() {
return {
player: this.player
}
},
components: {
PlayerData,
PlayerAttributes
},
data() {
return {
selectedTab: 'PlayerData'
}
},
methods: {
selectTab(tab) {
this.selectedTab = tab;
}
}
}
</script>
Na razie aplikacja wygląda następująco:
Nie jest to piękne:😎 ale nie martw się – następnym razem pokażę Ci jak stylizować aplikacje Vue.
Podsumowanie
Vue to framework Javascript do tworzenia aplikacji internetowych. Podobnie jak w przypadku innych frameworków, umożliwia korzystanie z komponentów wielokrotnego użytku. Dzięki temu tworzenie front-endu i budowanie jednostronicowych aplikacji jest łatwe i wydajne.
Dzisiaj pokazałem podstawy dotyczące komponentów:
- Jak utworzyć i zarejestrować komponent vue
- jak komponenty komunikują się z innymi komponentami
- jak używać props
- Co to jest prop drilling
- jak korzystać z mechanizmu dostarczania/wstrzykiwania
- Jak korzystać z komponentów dynamicznych
- i tak dalej