Bevezetés a Vue.js keretrendszerbe
Szinte már megkerülhetetlen a web alapú fejlesztéseknél a JavaScript használata. Az ügyfelek elvárják az interaktivitást, ami komplex kliens oldali logikával jár. Ha meg szeretnénk felelni az elvárásoknak, azzal szembesülünk, hogy jóval komplexebb kódot kell írnunk a problémák megoldásához, mint korábban. Mivel a JavaScript "beépített" funkcióinak használata kevésbé hatékony, ezért előbb-utóbb valamilyen library-hez vagy keretrendszerhez nyúlunk. A jQuery az egyik legszélesebb körben használt ilyen eszköz, bizonyos fokú komplexitást követően a kódunkat érdemes vele pluginokba szervezni, azonban legtöbbször még ez sem ad megfelelő támogatást a programunk szervezésében. Hogy tovább egyszerűsítsük az életünket, a keretrendszerek irányába érdemes tekintenünk. A legelterjedtebb keretrendszerek az Angular, a React, és amivel meg fogunk ismerkedni: a Vue.js. A cikk megértéséhez szükséges, hogy legyen némi gyakorlatunk a JavaScript, HTML és CSS hármasban. Elsősorban frontend és fullstack fejlesztőknek, vagy ilyen pályára készülőknek érdemes megismerkedniük a Vue.js-szel.
A cikkben a teljesség igénye nélkül bemutatok, és felépítek egy interaktív galériát. Lássunk hozzá!
Feladat specifikáció
A főoldalon a galéria címe lesz felül, alatta középen a fotók, majd legalul a lapozó fog elhelyezkedni. Az aktív oldal száma eltérő stílusú lesz a további oldaltól.
Amennyiben ki szeretnénk egy fotót nagyítani, ráklikkelünk a képre. Ekkor a képernyő elsötétül (az ’overlay
’ CSS
osztályú réteg segítségével), majd megjelenik a nagyobb méretű fotó. A kinagyított fotón is tudunk lapozni a navigációs nyilakkal (előre és vissza). A fotó alatt helyezkedhet el az aktuális fotó indexe, és az összes megtekinthető fotó száma. Amennyiben vissza szeretnénk térni a főoldalra, két lehetőségünk is lesz: vagy a fotó mellé a háttérre kattintunk, vagy az escape billentyűt leütjük.
Vue.js gyorstalpaló
Mint korábban említettem, a Vue.js egy JavaScript keretrendszer, ami MVVM mintát követ, azaz lényegében szétválasztja az üzleti logikát és a megjelenítést.
Érdemes nézegetnünk az angol nyelvű felhasználói kézikönyvet a vuejs.org oldalon, vagy a a magyar nyelvű leírásokat a vuejs.hu oldalon.
Első feladatunk a Vue egyed /vue instance/ létrehozása.
<!-- Vue.js betöltése CDNJS segítségével-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
// html kódunk
<div id="app">
<h1>{{ title }}</h1>
</div>
<script>
// vue egyed létrehozása
vue = new Vue({
el: '#app', // ehhez fér a Vue egyed
data: {
title: 'A galéria címe' // dinamikus változó
}
});
</script>
A galéria címét dinamikusan adtam meg. A Vue egyedünk képes sablonokat használni, de bármihez a DOM
-ban (HTML
dokumentum) nem fér hozzá. A hozzáférhetőséget az {el: '#app'}
paraméter határozza meg, ami egyébként a <div id="app">
elemre mutat. Tehát a sablonunk az app
azonosítóval meghatározott div
elem. Ha a sablonba szeretnénk beszúrni egy dinamikus értéket, akkor a dupla kacsacsőrös zárójel közé kell tennünk a változónk nevét így: {{ title }}
, illetve létre is kell hoznunk a property-t a Vue egyedünkben a data objektumon belül, így: data: { title: 'A galéria címe'}
. Miután a Vue értelmezi a sablont, majd lefordul a kód, a DOM
-ban ezt fogjuk látni:
...
<div id="app">
<h1>A galéria címe</h1>
</div>
...
Készítsük el a a fotókat
A pexel.com közösségi fotószolgáltatását fogom használni, ami a célnak tökéletesen megfelel, mert a fotók szabadon felhasználhatók, sőt elég, ha a képekhez tartozó azonosítót definiáljuk. Az url-ek programlogikával legenerálhatóak, az alábbi séma szerint: [https://images.pexels.com/photos/](https://images.pexels.com/photos/)<ID>/pexels-photo/<ID>.jpeg?w=<SZÉLESSÉG>&h=<MAGASSÁG>&auto=compress&cs=tinysrgb
. Szükség lesz bélyegképre (urlThumbnail
változó) és teljes képernyős méretre (urlPhoto
változó), azaz két felbontás fog rendelkezésre állni. Az alábbi JavaScript kódrészlet, a getPhotos()
metódus fogja elkészíteni a fotókat tartalmazó tömböt:
function getPhotos() {
var pexelIds = [
289225, 866019, 255349, 864939, 864990, 160998, 206529, 206388, 413998, 164262, 293029, 826349,
247199, 255332, 32267, 18809, 32267, 906052, 317157, 863977, 163360, 573299, 415829, 247322,
207936, 394545, 594365, 157967, 157967, 90754, 720598, 774367, 269746, 234059, 235516, 428543
];
var photos = [];
pexelIds.forEach(function(index, id) {
photos.push({
urlPhoto: 'https://images.pexels.com/photos/'+id+'/pexels-photo-'+id+'.jpeg?w=300&h=200&auto=compress&cs=tinysrgb',
urlThumbnail: 'https://images.pexels.com/photos/'+id+'/pexels-photo-'+id+'.jpeg?w=1260&h=750&auto=compress&cs=tinysrgb',
id: index
});
});
return photos;
};
A tömb szerkezete, azaz a getPhotos()
metódus által visszaadott érték ilyesmi lesz:
[
{
urlPhoto: "https://images.pexels.com/photos/289225/pexels-pho…289225.jpeg?w=300&h=200&auto=compress&cs=tinysrgb",
urlThumbnail: "https://images.pexels.com/photos/289225/pexels-pho…89225.jpeg?w=1260&h=750&auto=compress&cs=tinysrgb"
},
...
]
Lássuk a fotókat
A Vue képes a HTML
sablonban programszerkezeteket is futtatni. A v-for
HTML
attribútum a Vue sablon ciklusának felel meg (foreach
jellegű, tömbbejáró szerkezetről van szó). A kódunk így néz ki:
<div id="app">
<h1>{{ title }}</h1>
<div v-for="photo in photos">
<img :src="photo.urlThumbnail" alt="">
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
title: 'Collection of Awesome Girls',
photos: getPhotos()
}
});
</script>
Most képesek vagyunk az összes fotót megjeleníteni.
A v-for="photo in photos"
ciklus a photos
tömbön végiglépkedve az egyes fotókhoz tartozó értékeket bemásolja a photo
változóba. A v-for
direktívához tartozó HTML
elem belsejében tudunk hivatkozni erre a változóra: <img v-bind:src="photo.urlThumbnail" alt="">
. Megjegyezném, hogy a v-bind:
direktíva rövidítése a :
, a felső kódban nem is írtam ki a teljes alakját. Ezt azért kell használnunk, mert a HTML
attribútumokon belül nem használhatjuk a {{ }}
szintaxisú string interpolációt. Tehát ez a kód: <img src="{{ photo.urlThumbnail }}" alt="">
nem működne.
Formázzuk meg a galéria főoldalát
A html
kód ez lesz:
<div id="app" class="container">
<div class="gallery">
<h1>{{ title }} <small>Vue.js</small></h1>
<div class="row">
<div v-for="photo in photos" class="photo-frame">
<div class="photo" :style="{ backgroundImage: 'url(' + photo.urlThumbnail + ')' }"></div>
</div>
</div>
<div class="row">
<div class="pager-frame">
<ul class="pager-frame">
<li class="page active"><a href="#">1</a></li>
<li class="page"><a href="#">2</a></li>
<li class="page"><a href="#">3</a></li>
</ul>
</div>
</div>
</div>
</div>
A stíluslap (sass/scss) pedig ilyen:
@import url('https://fonts.googleapis.com/css?family=PT+Sans+Narrow');
body { font-family: 'PT Sans Narrow', sans-serif; }
.container {
max-width: 1500px;
margin: 10px auto;
clear: both;
text-align: center;
}
.gallery {
h1 { font-size: 3em; color: #226;
small { font-size: 1em; color: #888; }
}
.photo-frame {
width: 310px;
height: 210px;
padding: 5px;
border: 1px solid #eee;
background-color: #fafafa;
margin: 5px;
float: left;
cursor: pointer;
.photo {
width: 300px;
height: 200px;
border: 1px solid #aaa;
padding-left: 5px;
padding-top: 5px;
overflow: hidden;
background-position: top center;
background-size: cover;
}
}
}
div.row { clear: both; }
ul.pager-frame {
display: inline-block;
clear: both;
text-align: center;
list-style-type: none;
li.page {
width: 40px;
border-radius: 50px 50px;
border: 1px solid #888;
float: left;
margin-left: 20px;
font-size: 2em;
&:hover { background-color: #ccc; cursor: pointer; }
a { text-decoration: none; color: #222; }
&.active { background-color: #ddd; }
}
}
Most jöhet a lapozás
A lapozót eddig csak statikusan készítettem el, ezért annak logikájával még tartozom. Egész biztos, hogy ott is szükségünk lesz v-for
direktívára, azonban ki kell számítanunk az oldalak számát, meg kell határoznunk, hogy egy oldalon hány fotó helyezkedhet el, illetve azt is tudnunk kell, hogy hányadik oldalon járunk éppen. A fotókat tartalmazó tömböt kettéválasztom, lesz egy allPhotos
és egy photos
tömbünk. Az első a galéria összes fotóját, míg a második csak az aktuális oldal fotóit fogja tartalmazni. Amikor betöltődik az oldal és létrejön a Vue egyed, akkor kell az említett értékeket és az első oldal megjelenítését is megoldani. A kód így alakul:
<div id="app" class="container">
<div class="gallery">
<h1>{{ title }} <small>Vue.js</small></h1>
<div class="row">
<div v-for="photo in photos" class="photo-frame">
<div class="photo" :style="{ backgroundImage: 'url(' + photo.urlThumbnail + ')' }"></div>
</div>
</div>
<div class="row">
<div class="pager-frame">
<ul class="pager-frame">
<li v-for="page in idxLastPage" class="page" :class="{ active: (idxCurrentPage+1) == page }" @click="paginate(page-1)">
<a href="#">{{ page }}</a>
</li>
</ul>
</div>
</div>
</div>
</div>
Annyi volt a változás, hogy dinamikus értéket rendeltem a style attribútumhoz, így: :stlye="..."
, és egy osztályhoz így: :class="..."
. Felvettem egy eseményt (@click
)aminek a feladata, hogy elkapja a kattintás eseményt, ha a lapozó gombra klikkelne a felhasználó.
Csak megjegyezném, hogy jQuery
-ben ez így nézne ki: $('.page').click(function({...}))
, míg Vue.js-ben csupán ennyi: @click="..."
. Az eseményhez egy új metódust rendeltem: paginate(page-1)
. Ez a metódus a lapozásért fog felelni. Most nézzük meg a JavaScript kódot:
var allPhotos = getPhotos();
new Vue({
el: '#app',
data: {
title: 'Collection of Awesome Girls',
allPhotos: allPhotos, // a galéria összes fotója
photos: [], // az aktuális oldal fotói
photosPerPage: 12, // ennyi fotó lehet egy oldalon
idxLastPhoto: 0, // utolsó fotó indexe
idxCurrentPage: 0, // aktuális oldal idexe
idxLastPage: 0, // utolsó oldal indexe
},
mounted: function() {
// indexek inicializálása
this.idxLastPhoto = this.allPhotos.length;
this.idxLastPage = Math.floor(this.allPhotos.length / this.photosPerPage);
// első oldal mutatása
this.paginate(0);
},
methods: {
// Lapozáskor frissíti a tömböket
paginate: function(idxPage) {
this.idxCurrentPage = idxPage;
// periódus meghatározása
var idxStart = Math.floor(idxPage*this.photosPerPage);
var idxEnd = idxStart+this.photosPerPage>this.idxLastPhoto?this.idxLastPhoto:idxStart+this.photosPerPage;
// oldalon lévő fotók törlése
this.photos.splice(0, this.photos.length)
// látható fotók újraépítése
for(var idx = idxStart; idx < idxEnd; idx++) {
this.photos.push(this.allPhotos[idx]);
}
}
}
});
Metódusokat a Vue egyed methods
objektumán belül vehetünk fel. Amit le szeretnénk futtatni az app betöltésekor, azt a mounted
property-ben határozhatjuk meg. A Vue egyedünk paraméterei közül már ezeket megismertük:
new Vue({
el: '#app', // hatókör
data: {}, // adatok
mounted: function(){}, // betöltéskor futtatandó
methods: {}, // metódusok
});
A paginate
metódusban a felhasználó által kiválasztott oldalt fogjuk betölteni. Első lépésben a Vue egyed aktuális oldalához rendeljük hozzá a kiválasztott oldal számát, így: this.idxCurrentPage = idxPage
. A metóduson belül a Vue egyed változóit a this
kulcsszóval tudjuk elérni. Következő lépés, hogy meghatározzuk a megjelenítendő periódust, azaz mely fotók lesznek a képernyőn. Viszonylag egyszerű a dolgom, mert a tömb értékeit a Vue szinkronba hozza a DOM-al, csak arra kell figyelnem, hogy a photos
tömbben megfelelő adatok legyenek. Először kitörlöm a látható fotókat a this.photos.splice(0, this.photos.length)
paranccsal. Sajnos a jóval egyszerűbb this.photos = []
parancs itt nem működik, mert a Vue csak bizonyos parancsokat tud kezelni, amiről bővebb infót itt találtok. Utolsó lépésként az allPhotos
meghatározott fotóit áttöltjük a photos
változóba.
Mivel vannak olyan értékek, amiket a Vue egyed létrehozásakor is meg kell határoznunk, még egy kis feladatunk van. Létrehozzuk a mounted
tulajdonságot és meghatározzuk az utolsó fotó és oldal indexét idxLastPhoto
, idxLastPage
néven, majd meghívjuk a lapozó metódust az első oldalra: this.paginate(0);
. Íme az eredmény:
Azért tudtam a tömböt és a DOM-ot összerendelni, mert a háttérben a Vue.js készít egy köztes virtuális DOM-ot. Egyik oldalon figyeli a változókat, ami a DOM-ban fel van használva. Ha a változók értéke megváltozik, a DOM-ot automatikusan frissíti. Angolul ezt two side binding-nek hívják, magyarul talán kétoldali kötésnek fordíthatnám. Megemlíteném, hogy ugyanezt a technikát alkalmazza az Angular is, viszont a React már kicsit eltér ettől a módszertől, a JQuery pedig egyszerűen a valós DOM-ban dolgozik.
Mivel a JQuery mindig a DOM-ban olvas, ami lassú, a Vue.js pedig a saját virtuális DOM-jában, így a Vue.js sokkal gyorsabb a JQuery-nél.
Jelenítsük meg a fotót nagyban
Cél, hogy ha egy tetszőleges fotóra a felhasználó ráklikkel, akkor a főoldal sötétüljön el, a kiválasztott fotó pedig középre helyezve jelenjen meg. A fotó bezárása pedig a képernyő bármely részére történő kattintással, vagy az escape billentyű megnyomásával megtörténjen. A CSS kódom az alábbi résszel egészíttettem ki:
// elsötétedő háttér
#overlay {
position: fixed;
z-index: 10;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
text-align: center;
background: rgba(0, 0, 0, 0.7);
text-align: center;
}
// a fotó ezen a rétegen van
#layout {
z-index: 11;
position: absolute;
top: 0;
left: 0;
width: 95%;
margin-top: 50px;
text-align: center;
img {
margin: 40px auto 0;
max-width: 1260px;
width: 100%;
border: 1px solid #aaa;
box-shadow: 0 0 10px black;
}
}
A HTML kódba csak egy újabb eseményt kell felvenni, amikor a fotóra ráklikkelnek:
<div v-for="photo in photos" class="photo-frame" @click="showPhoto(photo.id)">
<div class="photo" :style="{ backgroundImage: 'url(' + photo.urlThumbnail + ')' }"></div>
</div>
A showPhoto()
metódus feladata lesz a kép megjelenítés nagyban. De még létre kell hoznom a megfelelő DIV-et is, ahol a fotó megjelenik:
<div id='app'>
...
<div id="overlay" v-show="isShowPhoto" @click="isShowPhoto=false"></div>
<div id="layout" v-show="isShowPhoto" @click="isShowPhoto=false">
<img :src="showPhotoUrl" alt="">
</div>
</div>
A v-show
direktíva csak akkor jeleníti meg a DIV
-et, ha az isShowPhoto
változó értéke true
, egyébként nem. Ha ráklikkelünk a fotóra, be kell záródnia, amit egyetlen értékadással is meg tudtam oldani, ezért nem kell külön függvényt létrehoznom: @click="isShowPhoto=false"
A nagy kép forrását a showPhotoUrl
változó határozza majd meg, ami a showPhoto
metódusban fog értéket kapni.
Csak akkor kell függvényt létrehoznunk, ha egy parancsnál többet szeretnénk a direktíván belül meghatározni. Tehát ez a kód hibás lenne: @click="a = 3; b = 2"
.
A fotót megjelenítő JavaScript kódrészlet:
showPhoto: function(id) {
var that = this;
// id alapján a fotó objektumának megkeresése
this.photos.forEach(function(photo) {
if (photo.id === id) {
that.showPhotoUrl = photo.urlPhoto; // a nagykép url-jének meghatározása
that.isShowPhoto = true; // a nagykép megjelenítése
return;
}
});
}
Itt csak annyi érdekeség van, hogy a forEach
-en belül a this
értéke nem egyezik meg a vue egyed this
értékével, ezért az egyik közkedvelt sémával a that
változó bevezetésével oldottam fel a problémát. A kinagyított kép így fog kinézni:
Azért, hogy az escape billentyű lenyomásával is bezárhassuk a galériát, a mounted
metódust kell kiegészítenünk, és a document
HTML objektum billentyűleütés eseményét kell figyelnünk:
mounted: function() {
...
document.addEventListener("keydown", e => {
if (this.isShowPhoto && e.keyCode == 27) {
this.isShowPhoto = false;
}
});
},
...
A nagy fotókat is lapozzuk
Következő feladatom, hogy a kinagyított fotó is lapozható legyen, azaz az előző és a következő fotót is meg tudjuk jeleníteni a nélkül, hogy a jelenlegi kinagyított fotót bezárnánk. Most szükség lesz egy filter (szűrő) bevezetésére is.
<div id="app">
...
<div id="layout" v-show="isShowPhoto" @click="isShowPhoto=false">
<img :src="showPhotoUrl" alt="">
<div class="prev" @click.stop="goPrevPhoto()"><</div>
<div class="next" @click.stop="goNextPhoto()">></div>
</div>
</div>
A szűrőket a direktíva után, ponttal adjuk meg, tehát a .stop
szűrőt így: @click.stop="..."
. A metódus az általunk megadott feladatokon kívül lefuttat egy event.stopPropagation() parancsot, aminek a célja, hogy az esemény ne fusson fel az eseménybuborékon. A stop szűrő nélkül az ablak akkor is bezáródna, ha a következő/előző nyomógombokra klikkelnénk, hiszen továbbterjedne az alatta lévő rétegekre, ahol megtörténne az "isShowPhoto=false"
értékadás. A goPrevPhoto()
metódus az előző, míg a goNextPhoto
metódus a következő fotóra lapoz.
A .stop filter jQuery megfelelője:
$('#element').click(function(e) {
e.stopPropagation();
});
A stíluslapot a következő kóddal bővítettem:
#layout {
...
.next, .prev {
position: absolute;
top: 70px;
width: 40px;
height: 30px;
border: 2px solid #ffffff88;
border-radius: 50px 50px;
box-shadow: 2px 2px 10px black;
padding-top: 10px;
color: white;
text-shadow: 2px 2px 10px black;
cursor: pointer;
&:hover {background-color: #ffffff22; border-color: #ffffff; }
}
.prev { left:20px; }
.next { right:20px; }
}
Most pedig nézzük meg a két új metódushoz tartozó kódrészletet:
methods: {
...
goPrevPhoto: function() {
var current = this.idxCurrentPhoto;
this.showPhoto(this.allPhotos[current === 0 ? this.idxLastPhoto : current-1].id);
},
goNextPhoto: function() {
var current = this.idxCurrentPhoto;
this.showPhoto(this.allPhotos[current === this.idxLastPhoto ? 0 : current+1].id);
}
}
Az utolsó fotó után az első fog következni, az első előtt pedig az utolsó. Vegyük észre, hogy a fotók lapozása független az aktuális oldalon megjelenített fotóktól, hiszen az allPhotos
tömböt használtam.
A böngészőben ilyesmit látunk:
Az app működés közben:
See the Pen Vue.js gallery by Rajcsányi Zoltán (@rajcsanyiz) on CodePen.
Zárszó
A technika, amivel felépítettem a galériát, alig karcolta a Vue.js képességeit, melyek közül kiemelendő az újrafelhasználható komponensek építése és azok kommunikációja.
A cikkben lehetnek hibák, pontatlanságok, ezért az építő jellegű kritikát örömmel fogadom és javítom az elírásokat.
Amennyiben tetszett a téma, és a további példakódjaimat is szeretnéd megnézni, várlak a Vue.js codepen gyűjteményemben: https://codepen.io/collection/DwJBEZ/
Rajcsányi Zoltán
https://rajcsanyizoltan.hu