Vissza az előzőleg látogatott oldalra (nem elérhető funkció)Vissza a tananyag kezdőlapjára (P)Ugrás a tananyag előző oldalára (E)Ugrás a tananyag következő oldalára (V)Fogalom megjelenítés (nem elérhető funkció)Fogalmak listája (nem elérhető funkció)Oldal nyomtatása (nem elérhető funkció)Oldaltérkép megtekintése (D)Keresés az oldalon (nem elérhető funkció)Súgó megtekintése (S)

Modern programozási minták a kliens és szerveroldali webprogramozásban / Magas szintű programszervezés és alkalmazásarchitektúrák

Tanulási útmutató

Összefoglalás

Kompex kliensoldali alkalmazások írásához elengedhetetlen a JavaScript kód megfelelő magas szintű szervezése. Ez a fejezet ennek alapjait mutatja be, majd rátér a korszerű MN* architektúrák rövid ismertetésébe.

Magas szintű programszervezés és alkalmazásarchitektúrák

Kliensoldali webes alkalmazások fejlődése

A 2000-es évek közepe óta egyre nagyobb teret kapnak a kliensoldali megoldással dolgozó oldalak. A 2000-es évek elején a fejlesztések elsősorban a szerveroldali technológiákra koncentráltak, többek között a kliensoldali platform egyenetlensége és megbízhatatlansága miatt. Ekkoriban szinte minden a szerveroldal hatáskörébe tartozott: HTTP input kezelése, HTML oldal generálása, üzleti logika futtatása, adatok perzisztálása, felhasználók nyilvántartása, más szolgáltatásokkal történő kapcsolattartás, stb.

A 2000-es évek közepétől azonban az AJAX technológia elterjedésével lehetővé vált olyan webes alkalmazások készítése, amely kényelmes, élményszerű használatot biztosított. Egymást erősítő folyamatokként felfedezték a JavaScript nyelv lehetőségeit, kialakították a fontosabb programozási mintákat, egyre nagyobb kód került a kliensoldalra, a böngészők ezeket pedig egyre optimálisabban, egyre gyorsabban futtatták. A felhasználói élmény és a web univerzalitása, mindenhonnan és bármikor való elérhetősége egyre több és egyre komplexebb webes alkalmazás megjelenését eredményezte. A korábban a szerveroldal hatáskörébe tartozó feladatok áttevődtek a kliensoldalra, és manapság sok alkalmazásban a böngészőben valósul meg a HTML elemek előállítása, az adatok tárolása és feldolgozása.

Ezek az előrelépések a webes alkalmazások új kategóriáját hozták létre. Ezekben az alkalmazásokban az oldal egyszer töltődik le a szerverről, lekérve a működéséhez szükséges összes JavaScript kódot és egyéb erőforrást, majd az alkalmazás egészének működése ugyanazon oldalon belül valósul meg, anélkül, hogy a teljes oldal újratöltődne. A háttérben természetesen AJAX vagy egyéb módon történik aszinkron kommunikáció a szerverrel, főként adatot cserél a kliens és a szerver egymással. Az alkalmazások ezen fajtáját egyoldalas alkalmazásoknak nevezzük (angolul Single Page Application, rövidítve SPA).

Az egyoldalas alkalmazások viszonylag sok funkciót valósítanak meg, a sok funkció pedig sok, nagyon sok JavaScript kód írását követeli meg, ehhez pedig elengedhetetlen a programkód megfelelő szervezése.

Vissza a tartalomjegyzékhez

Kódszervezési problémák

Az eddigi fejezetekben láthattuk, hogy alacsonyabb szinten milyen lehetőségünk van az alkalmazás kódjának szervezésére. Ennek alap építőelemei a függvények voltak, az általuk kezelt adatokkal együtt ezeket objektumokba foglaltuk, majd egy adott funkcionalitáshoz tartozó kódrészletet jól elkülönítve a többitől modulokba szerveztük. Mindegyik modul definiálja a publikus interfészét, a megvalósítás részleteit azonban elrejti az őt használó másik modultól.

Egy alkalmazás tehát nagy valószínűséggel modulokból áll. Az egyszerűbb alkalmazásokat néhány modul használatával már működtetni lehet. Bonyolultabb alkalmazásoknál azonban a modulok száma igen nagy lehet, és jól átgondolt stratégia nélkül könnyen modulok egymással keresztbe kasul szőtt hálózatával találhatjuk szembe magunkat, ami megint csökkenti alkalmazásunk áttekinthetőségét és karbantarthatóságát. Hogyan szervezzük ezeket a modulokat, hogyan tartsák a kapcsolatot, hogy elkerüljük az összevisszaságot?

A modulok kezelése mellett azonban más problémákba is ütközhetünk. Az eddigi fejezetekben ugyanis nem foglalkoztunk azzal sem, hogy hogyan érdemes a felületkezeléssel kapcsolatos logikát kapcsolatba hozni az adatok tárolását és feldolgozását végző kódrészlettel. Eddig csupán azzal foglalkoztunk, hogy a HTML dokumentum manipulálásával kapcsolatban milyen fogalmakkal érdemes tisztában lennünk, milyen programozási minták kapcsolódnak a böngésző és a dokumentum kezeléséhez, és ehhez milyen segédeszközöket érdemes használni.

Láthattuk, hogy egy megfelelő keretrendszer, pl. a jQuery, nagy mértékben megkönnyíti a DOM-mal történő munkánkat. Ugyanakkor ezek semmilyen iránymutatást nem adnak arra vonatkozóan, hogy hogyan tároljuk adatainkat, hogyan kezeljük a felületi eseményeket, hogyan jelenítsük meg a felületi elemeket. Egy jQuery alkalmazás megfelelő kódszervezés nélkül könnyen válhat szelektorok és eseménykezelő függvények egymásba ágyazott kusza szövevényévé, amely már kis alkalmazásoknál áttekinthetetlen, nyomkövetésre és tesztelésre alkalmatlan kódot eredményez. De a jQuery nem is alkalmazásszervezésre való eszköz, így nem is várhatjuk el tőle ezt a feladatot.

A fenti kérdések és nehézségek mögött megbúvó probléma alapvetően az, hogy az alkalmazás bizonyos szintjein különböző feladatokat ellátó kódrészletek keverednek egymással. Egészen alacsony szinten probléma származhat abból, ha egy függvény egyszerre több dologért felel, vagy mellékhatással rendelkezik. Ezért – ahogy azt korábban már írtuk – a függvényekre bontásnak is megvannak a maga szabályai. Eggyel magasabb szinten ugyanebbe a problémába ütközhetünk a függvények és adatok objektumokba vagy modulokba szervezésénél. Ezeket is úgy szükséges kialakítani, hogy egy dologért legyenek felelősek. A HTML dokumentum kezelésekor felmerülő problémák jórészt abból fakadnak, hogy keverednek a felületet író műveletek (DOM generálás) az azt olvasó műveletekkel (eseménykezelés), a megjelenítés (HTML, CSS) az adatokkal és a tárolás módjával. Itt is a felelősségi körök megfelelő elválasztására van szükség, és ezek megfelelő összekapcsolására, hogy alkalmazásunk nagyobb méretek öltve is áttekinthető és könnyen bővíthető legyen.

A kódszervezés fentiekben megfogalmazott kérdéseinek megválaszolásához először lépjünk eggyel hátrébb, és vizsgáljuk meg, hogy egy kliensoldali webes alkalmazásnak milyen feladatokkal kell megbirkóznia. Mindenekelőtt kezelnie kell a felhasználói felületet, reagálnia kell tudni a böngésző címsorában bekövetkező változásokra, használnia kell a különböző böngészőszolgáltatásokat, gondoskodnia kell az adatok megfelelő tárolásáról.

Egy alkalmazás feladatai nagy vonalakbanEgy alkalmazás feladatai nagy vonalakban

A fenti feladatok elvégzéséhez a böngésző megfelelő API-kat bocsát rendelkezésre: a DOM-ot a HTML dokumentum kezelésére, a HTML5-ös API-kat a böngészőszolgáltatásokhoz, az AJAX-ot vagy egyéb technológiákat a perzisztálás megvalósítására. Ezeknek az API-knak egy része különböző eseményeken keresztül kommunikál az alkalmazással. Maga az alkalmazás pedig több kisebb-nagyobb modulból áll össze.

Egy alkalmazás interfészei és felosztása nagy vonalakbanEgy alkalmazás interfészei és felosztása nagy vonalakban

Egy nagyobb alkalmazás feladatait tehát a következőkben lehet összefoglalni:

Vissza a tartalomjegyzékhez

Modulok kezelése

A modulok kezelése részben arról szól, amit a modulokról szóló fejezetben már láttunk: hogyan definiálunk egy modult, hogyan adjuk meg függőségeiket, hogyan töltjük be őket. Ebben segítségünkre vannak az egyes modulkönyvtárak, amiket korábban áttekintettünk. Az alkalmazásszinten történő modulkezelés azonban ennél több: egy olyan alapvető alkalmazásarchitektúra definiálását jelenti, amely elősegíti a modulok együttműködését és kommunikációját anélkül, hogy közben szorosan kapcsolódjanak egymáshoz.

A modulkezelés tipikusan a következő funkciókból áll össze:

A fenti fogalmakat magában foglaló architektúrakoncepciót Nicholas C. Zakas és Addy Osmani dolgozott ki, amikor nagy méretű, skálázható alkalmazások tervezésével foglalkoztak. Az architektúra sematikus vázát a következő ábra mutatja:

Nagy méretű, skálázható alkalmazások architektúravázaNagy méretű, skálázható alkalmazások architektúraváza

Mindkét koncepció hasonló elvre épül, amelynek középpontjában az alkalmazás lényegi részeit megvalósító modulok állnak. Ezekben a koncepciókban a modulok általában a felhasználói felület egyes részeit reprezentálják, így a logikát megvalósító JavaScript kód mellett adatok és felületi elemek is tartoznak hozzájuk. A modulkezelő általában előír egy szabványos modulinterfészt, amelyet a moduloknak implementálniuk kell. Ez a legegyszerűbb esetekben a modul elindítását és leállítását végző műveletek.

Forráskód
var moduleInterface = {
    start: function () ,
    stop: function () 
};

A szoros kapcsolódást elkerülendő az egyes modulok nem tudnak a többiről. Az alkalmazás többi részével való kommunikációra a homokozó objektum szolgál. Ennek egy-egy példányát minden modul megkapja. A homokozó objektum tartalmazza az összes olyan szolgáltatást, amelyet egy modul az alkalmazásból használhat. Ez általában egy homlokzat mintát valósít meg, absztrakt interfész mögé rejtve a konkrét megvalósításokat. Ezen keresztül lehet eseményeket küldeni vagy feliratkozni rájuk, a DOM-mal kapcsolatos műveleteket elérni, stb. A homokozó tulajdonképpen egy minden modul számára közös API-t jelent, így ennek megtervezése különösen figyelmet igényel, mert később nehezen változtatható. Mivel minden modul ezen keresztül éri el az alkalmazás szolgálatait, így joggal mondhatjuk, hogy az architektúra egyik leglényegesebb eleméről van szó.

Forráskód
var sandboxProto = extendDeep({
    //...
}, observable);

Egy modul definíciója így nézhet ki:

Forráskód
var module1 = function (sandbox) {
    //A modul implementációja
    //sandbox használata
    //Publikus API meghatározása
    return extendDeep(Object.create(moduleInterface), {
        //...
    });
};

Korábbi példánkat véve:

Forráskód
var weatherModule = function (sandbox) {
    var state = 'sunny',
        changeState = function (newState, hours) {
            state = newState;
            sandbox.notify(newState, hours);
        };
    return extendDeep(Object.create(moduleInterface), {
        changeState: changeState
    });
};
var smartphoneModule = function (sandbox) {
    var warnForRain = function (hours) {
            console.log('Rain is coming in ', hours, 'hours!');
        },
        prepareForSun = function (hours) {
            console.log('The sun will shine in ', hours, 'hours!');
        },
        init = function () {
            sandbox.addObserver('rainy', warnForRain);
            sandbox.addObserver('sunny', prepareForSun);
        };
    return extendDeep(Object.create(moduleInterface), {
        init: init
    });
};

A modulok kezelését egy alkalmazásobjektum végzi. Feladata a modulok regisztrálása, indítása és leállítása, a modulok működésében felmerülő hibák kezelése, a modulok közötti kommunikáció biztosítása.

Forráskód
var application = (function () {
    var modules = ;
    return {
        register: function (moduleId, module) {
            var moduleInstance = module(Object.create(sandboxProto));
            modules[moduleId] = moduleInstance;
            return moduleInstance;
        },
        start: function (moduleId) {
            modules[moduleId].start();
        },
        stop: function (moduleId) {
            modules[moduleId].stop();
        },
        startAll: function () {
            for (moduleId in modules) {
                if (modules.hasOwnProperty(moduleId)) {
                    this.start(moduleId);
                }
            }
        }
        //...
    };
})();

Használata:

Forráskód
application.register('weather', weatherModule);
application.register('smartphone', smartphoneModule);
application.startAll();

Az alkalmazás funkcionalitását további kiegészítőkön keresztül lehet gazdagítani. Ezek különböző szintekre épülnek be, általában a homokozó objektum tudását bővítik, a moduloknak szélesebb lehetőséget biztosítva.

Az elképzelést számos függvénykönyvtár implementálta. Közülük néhányat a lenti felsorolás tartalmaz:

Vissza a tartalomjegyzékhez

Események kezelése

Eseményekről már több helyen szóltunk ebben a tananyagban. Láthattuk, hogy az események igazán hasznosak akkor, ha lazán kapcsolt komponensek aszinkron kommunikációját szeretnénk megvalósítani. Láttuk azt is, hogy a böngésző és a HTML dokumentum is alapvetően esemény-vezérelt. A programozási mintáknál láthattuk, hogy a böngésző által biztosított eseményeken kívül saját eseményeket is létrehozhatunk. Ebben a fejezetben azt tekintjük át, hogy milyen lehetőségeink vannak a saját események kezelésére.

Eseményt kiváltó objektumok

Az egyik lehetőségünk, ha egyes objektumok eseményeket váltanak ki állapotuk bizonyos megváltozásakor. Ekkor a kérdéses objektum állapotváltozásában érdekelt objektumok megfigyelhetik őt, és az esemény bekövetkeztére reagálhatnak. Ez tulajdonképpen nem más, mint a megfigyelő minta, ahol az eseményt kiváltó objektum a megfigyelt, az érdekeltek pedig a megfigyelők. Ezt a mintát használják a böngésző és a HTML dokumentum elemei, amikor eseményeket definiálnak, és ezekre eseménykezelőkkel feliratkozhatunk.

E megoldás hátránya, hogy mindegyik megfigyelőnek referenciát kell tartalmaznia a megfigyelt objektumra. Ez nagyon sok objektum esetén megnöveli az objektum karbantartására fordított kód mennyiségét, ráadásul a kétféle objektum ezáltal szoros kapcsolatba kerül egymással, amit éppen elkerülni igyekszünk.

Eseménygyűjtők

Az eseménygyűjtő egy olyan központi objektum, ami különböző források eseményeit gyűjti össze, és hívja meg azokat a függvényeket, amelyek feliratkoztak ezekre az eseményekre. Ez a központi eseményvezérlő minta, ami nem más, mint a megfigyelő minta alkalmazása egy közvetítő objektumon.

Az eseménygyűjtők nagyon hasznosak, mert sem a megfigyelőnek, sem a megfigyeltnek nem kell tudnia a másikról, és így egészen lazán kapcsolt rendszerek alakíthatók ki. A fenti architektúrakoncepciók is elsősorban ezt a megoldást használják a homokozó objektumon keresztül a modulok közötti üzenetküldésre.

Eseménybuszok

Az eseménygyűjtők egyik nagy hátránya, hogy semmi nem biztosítja, hogy az esemény feldolgozásra is kerül az üzenet kézbesítésekor. Az eseménybuszok egy jól kontrollált és adminisztrált környezetet hoznak létre az üzenetek kezelésére. A beérkező eseményeket feljegyzik, a megfelelő címzetthez továbbítják, és nyomon követik állapotukat, így azok mindig ellenőrizhetők. Az üzenetek általában valamilyen jól meghatározott formában utaznak.

Eseménysorok

Egy speciális eseménybusz, amely elsősorban akkor hasznos, ha az eseményt kiváltó és az eseményt feldolgozó objektum eltérő sebességgel dolgozik, vagy szükséges az eredeti sorrend megtartása.

A megfelelő eseménykezelési stratégia kiválasztása

Mindegyik fenti eseménykezelési megoldásnak megvan a maga helye, egyik sem ad teljes megoldást minden problémára.

Vissza a tartalomjegyzékhez

MN* architektúrák

A modulok kezelése önmagában nem ad megoldást a modulon belüli adatok és a hozzá tartozó felhasználói felület kezelésével kapcsolatos problémákra. Mivel ez két külön feladatkör, ezért érdemes az ezekkel foglalkozó programlogikát is kettéválasztani.

A feladat elvégzéséhez tartozó adatokat és a hozzájuk kapcsolódó feldolgozó függvényeket modellnek szokták nevezni. Más szóval a feladat állapotterét és annak módosításához szükséges metódusokat tartalmazza. Ide tartozik minden, ami független a felhasználói felület bármilyen aspektusától: ebben nem jelenhetnek HTML elemek, és nem dolgozhat fel felületről jövő eseményeket sem. A felületfüggetlen adatábrázolás és feldolgozó logika tartozik ide.

A felhasználói felület kezelésével kapcsolatos logikát a nézet tartalmazza. Ez felel a megfelelő HTML elemek kirajzolásáért és módosításáért, és ebben történik az események feldolgozása is.

A maradék logika – sok egyéb mellett – felel a modell és a nézet összekapcsolásáért. Ez kezeli a nézetből érkező kéréseket, és ezeknek megfelelően módosítja a modellt.

Az adatnak és a megjelenítésnek ilyen jellegű kettéválasztása nem új keletű koncepció, sőt, egy viszonylag régi architekturális mintában, a Modell-Nézet-Vezérlő, vagy röviden MNV mintában már megjelent (angolul Model-View-Controller, röviden MVC). Ebben a maradék logikai részre úgy tekintenek, mint a folyamatok irányítójára, ezért ezt a részt vezérlőnek nevezik.

A JavaScript keretrendszerek azonban nem ragaszkodnak hűen ehhez a felosztáshoz. A modell és a nézet feladatköreit illetően egységesek a nézőpontok, azonban attól függően, hogy hogyan tekintenek a fennmaradó logikára, és milyen további felosztást végeznek benne, más és más nevet adtak ennek az architekturális mintának. Ezért nagyon gyakran a kód ilyen jellegű felosztását MN* architektúráknak nevezzük (angolul MV*)

A következőkben az egyes architektúrák főbb jellegzetességeit tekintjük át.

Az MNV minta

Az MNV mintát eredetileg a Smalltalk-80 tervezésekor dolgozták 1979-ben. Egy olyan architekturális minta, ami az üzleti adatok (modell) és a felhasználói felületek (nézet) szétválasztását követeli meg egy harmadik szereplő (vezérlő) közreműködésével, mely a felhasználói inputot, a modell és nézet kapcsolatát koordinálja. Jellegzetességei a következők voltak:

Az MNV minta sematikus ábrázolásaAz MNV minta sematikus ábrázolása

Az MNV minta JavaScript implementációi az eredeti minta két fő aspektusát tartják meg:

Ezeken túl JavaScriptben nagyon különbözően jelenik meg az a minta. Az MVC minta egyik klasszikusnak mondott képviselője, a Backbone keretrendszer, például nem definiál külön vezérlőt, annak a logikáját a nézet logikájába olvasztja bele. Ugyanakkor külön szereplőként jelenik meg a Router, amely az URL kezelését végzi, és a változásakor az útvonal alapján választja ki a futtatandó kódot.

Az MNV minta megvalósulása a Backbone keretrendszerbenAz MNV minta megvalósulása a Backbone keretrendszerben

Az MNP minta

A MNV minta koncepciójának egyik továbbfejlesztéseként jelent meg a 1990-es évek elején a Modell-Nézet-Prezenter minta (angolul Model-View-Presenter, MVP). Ebben a nézetet igyekeznek mentesíteni mindenféle logikától, így ebben a mintában gyakran hivatkoznak rá passzív nézetként. Egyetlen feladata, hogy a felületről érkező eseményeket továbbítsa a prezenternek. Ez utóbbi veszi át a megjelenítési és a bemenet feldolgozását végző logikát. A felületi események alapján módosítja a modellt, akiben bekövetkező változásokra ugyancsak ő figyel. Az architektúra leegyszerűsödik, a prezenter központi irányítóként ékelődik a másik két komponens közé. A nézettel egy interfészen keresztül kommunikál, aminek nagy előnye, hogy a két komponens fejlesztése elválhat egymástól, és tesztelhetőségük is javul. JavaScriptes világban viszonylag kevés keretrendszer épít erre a mintára.

A Modell-Nézet-Prezenter minta sematikus ábrájaA Modell-Nézet-Prezenter minta sematikus ábrája

Az MNNM minta

A Modell-Nézet-Nézetmodell minta még jobban próbálja elválasztani az adatokat és a hozzá tartozó üzleti logikát a nézettől és a megjelenítéshez használt logikát. A modell továbbra is a feladat elvégzéséhez szükséges adatokat tartalmazza, de egyáltalán nem hordoz viselkedésbeli információkat, vagy olyanokat, amelyek a megjelenítéshez kellenek (mint pl. egy dátum formátuma). Ehhez definiálja a Nézetmodellt, ami a modell bizonyos tulajdonságait teszi elérhetővé a nézet számára, tartalmazza az ehhez kapcsolódó logikát, és dolgozza fel a felületről érkező inputot. Egyfajta vezérlőként viselkedik, ami továbbítja a modelladatokat a nézetnek, és a nézetbeli változásokat a modellnek. Nagyon sokban inkább a modellre hasonlít, hiszen adatokkal dolgozik, de a megjelenítésért is felel. A fő különbség ebben a mintában a nézet és a nézetmodell viszonyában van. A nézetmodell ugyanis semmit nem tud a megjelenítésre szolgáló HTML struktúráról. A nézet ebben az esetben passzív olyan szempontból, hogy benne semmilyen logika nincsen. A nézetben deklaratív módon kell jeleznünk, hogy a felületi elemek tulajdonságai a nézetmodell melyik attribútumához kapcsolódnak. Az MNNM minta jellegzetessége ez a deklaratív kétirányú kötés. A nézetmodellben bekövetkező változások automatikusan a felületi elemeken is látszódnak, a felületi elemek tulajdonságaiban bekövetkező változások a nézetmodellben is érvényesülnek. A nézetnek nincsen állapota, a nézetmodellnek van.

Az MNNM minta sematikus ábrájaAz MNNM minta sematikus ábrája

Az MN* architektúrák létjogosultsága

Az MN* architektúrák magas szintű struktúrát adnak a kódnak. Kijelölik az adatok és a megjelenítéshez szükséges logikák helyét, ezáltal lazább kapcsolatokat és nagyobb rugalmasságot adva a kódnak. Minden olyan oldalon érdemes használni, amelyben a felületi elemeket kliensoldalon hozzuk létre, vagy intenzíven változtatjuk a felhasználói felületet. Az MN* keretrendszerek ráadásul számos olyan dologra is bevált megoldást adnak, amivel fejlesztés közben találkoznánk: az URL-ben bekövetkező változások kezelése, a modell szinkronizálása a szerverrel, adatok validálása, stb. Az előre elkészített és megbízható megoldásoknak köszönhetően rengeteg időt spórolhatunk meg fejlesztéskor. Természetesen egy keretrendszer megismerése is időt vesz igénybe, de az egyszeri befektetés mindenképpen megtérül a későbbi fejlesztések során.

Vissza a tartalomjegyzékhez

MN* architektúrák tipikus eszközei

Megfigyelhető modellek

A fent említett MN* architektúrák egyik közös jellemzője, hogy a modell (vagy nézetmodell) változás esetén eseményt bocsát ki, amin keresztül értesíti az őt figyelőket. Tipikusan a nézet iratkozik fel a modell változásaira, és automatikusan frissíti az oldal megjelenését a változásoknak megfelelően. Az egyes architektúrák által igényelt adatkötés is erre a mechanizmusra épít.

A modellek ilyen viselkedése alapvetően két komponensből áll:

Az első elvárást könnyű teljesíteni (ld. a megfigyelő minta). A második részre azonban nincsen általános megoldás. Maga az EcmaScript szabvány már kísérletezik megfigyelhető objektumok definiálásával az Object.observe() metóduson keresztül, de ezek az írás pillanatában erősen a tervezési fázisban vannak és limitált a támogatottságuk. Amíg nyelvi szinten nem lesz támogatás erre, addig a kívánt funkciót többféleképpen elérhetjük. Mindegyik megoldás hátterében az áll, hogy az egyes adattagokat valamilyen módon metódusokká kell alakítani, amelyen belül aztán a változtatás mellett a megfelelő esemény kibocsátása is elintézhető.

get() és set() metódusok

Az egyik megközelítés szerint a modellnek csak egy részét képezik a reprezentatív adatok, így ezek a modell egy speciális adattagjaként jelennek meg (pl. attributes), és ezeket explicit get() és set() függvényekkel lehet lekérdezni vagy beállítani. (Ilyen megoldással él pl. a Backbone.js keretrendszer.)

Forráskód
var book = {
    author: 'Stoyan Stefanov',
    title: 'JavaScript Patterns'
};
var observableBook = makeObservable(book);
observableBook.addObserver('change', function (book) {
    console.log('Book changed', book);
});
observableBook.addObserver('change:title', function (title) {
    console.log('Title changed', title);
});
observableBook.set('title', 'JavaScript Patterns rev.3.');
var title = observableBook.get('title');
//Megvalósítás
var makeObservable = function(o) {
    return extendDeep(, observable, {
        get: function (attr) {
            return this.attributes[attr];
        },
        set: function (attr, val) {
            this.attributes[attr] = val;
            this.notify('change:' + attr, val);
            this.notify('change', this.attributes);
        },
        attributes: extendDeep(, o)
    })
};

Natív getterek és setterek

Az előző megoldásban a modellparaméterek az attributes objektumban kaptak helyet. EcmaScript 5 azonban már nyelvi szinten támogatja a gettereket és settereket, így a megfigyelhető objektumot nem szükséges modellbe csomagolni. Ekkor is szükség van azonban egy segédfüggvényre, amely az objektum adattagjait getterekké és setterekké alakítja.

Forráskód
var book = {
    author: 'Stoyan Stefanov',
    title: 'JavaScript Patterns'
};
var observableBook = makeObservable(book);
observableBook.addObserver('change', function (book) {
    console.log('Book changed', book);
});
observableBook.addObserver('change:title', function (title) {
    console.log('Title changed', title);
});
observableBook.title = 'JavaScript Patterns rev.3.';
var title = observableBook.title;
//Megvalósítás
var makeObservable = function(o) {
    var newO = extendDeep(, observable);
    var attributes = ;
    for (var i in o) {
        if (o.hasOwnProperty(i)) {
            attributes[i] = o[i];
            Object.defineProperty(newO, i, {
                enumerable: true,
                get: function() {
                    return attributes[i];
                },
                set: function(val) {
                    attributes[i] = val;
                    this.notify('change:' + i, val);
                    this.notify('change', attributes);
                }
            });
        }
    }
    return newO;
};

Adattagok metódusokká alakítása

Egy harmadik megoldás azzal él, hogy az adattagokat egyszerűen függvénnyé alakítja. Ez akkor lehet hasznos, ha olyan böngésző támogatása is cél, amely nem implementálta még a natív getter és setter funkcionalitást. (Ezt a megközelítést használja a Knockout.js keretrendszer.)

Forráskód
var book = {
    author: 'Stoyan Stefanov',
    title: 'JavaScript Patterns'
};
var observableBook = makeObservable(book);
observableBook.addObserver('change', function (book) {
    console.log('Book changed', book);
});
observableBook.addObserver('change:title', function (title) {
    console.log('Title changed', title);
});
observableBook.title('JavaScript Patterns rev.3.');
var title = observableBook.title();
//Megvalósítás
var makeObservable = function(o) {
    var newO = extendDeep(, observable);
    var attributes = ;
    for (var i in o) {
        if (o.hasOwnProperty(i)) {
            attributes[i] = o[i];
            newO[i] = function(val) {
                if (arguments.length > 0) {
                    attributes[i] = arguments[0];
                    this.notify('change:' + i, val);
                    this.notify('change', attributes);
                } else {
                    return attributes[i];
                }
            }
        }
    }
    return newO;
};

Sablon-kezelés (template)

A nézet feladata a felhasználói felülettel kapcsolatos dolgok kezelése. Ebbe beletartozik az onnan érkező események feldolgozása vagy továbbítása, valamint a magának a felületnek a kialakítása, módosítása. Ez utóbbi gyakorlatilag a DOM megfelelő programozásából áll. Felhasználói felület generálásának egyik gyakran használt lehetősége, hogy szövegként előállítjuk a kívánt HTML-t, majd azt egy szülőelem belsejébe rakjuk. Valahogy így:

Forráskód
function createListItem(book) {
    return '<li><a href="show/' + book.id + '">' + 
        book.title + '</a></li>';
}

Ez a megoldást azonban már régóta rossz gyakorlatnak tartják. Egyrészt leírása körülményes, az eredmény átláthatatlan, könnyű benne hibát ejteni, másrészt az összeállítás számítási igénye is nagy. Végül ellentmond annak az elvnek, hogy a megfelelő feladatköröket elválasszuk: ebben összemosódik a logika (ahogy összerakjuk) a leíró nyelv építőelemeivel (HTML elemekkel).

E megközelítés helyett érdemes a végső struktúrát elkülöníteni a feldolgozás módjától, oly módon, hogy egy külön sablonban definiáljuk az elvárt HTML szerkezetet, benne jelölve azokat a helyeket, ahova dinamikus tartalom megjelenítésére van szükség, és egy sablonfeldolgozóra bízzuk azt a logikát, mely egy konkrét adatot a sablon segítségével megjelenít.

Dinamikus tartalom a legegyszerűbb és legtöbb esetben nem más, mint egy változó értékének megjelenítése, de gyakran további elemekkel is találkozhatunk, úgymint elágazásokkal vagy ciklusokkal sablonnyelvben. Ez a három utasítás általában elég a legtöbb felhasználói felület generálásához. Találkozhatunk olyan véleményekkel is, melyek az elágazásokat és ciklusokat sem szívesen látják a sablonokban, mondván, hogy azok már logikát képviselnek, és inkább a JavaScript kódban van a helyük. E szűkebb koncepció alapján a sablonokban csupán a megjelenítendő változók helyét jelölhetjük.

A sablonok tehát elválasztják megjelenítendő HTML struktúrát a feldolgozási logikától. A sablon csak azt jelzi, hogy hova kell adatot beszúrni, így maga a sablon különböző adattartalmakkal újra és újra felhasználható. A sablonfeldolgozó lesz az, amely a sablont a konkrét adattal kombinálja és állítja elő azt a DOM kimenetet, amelyet a képernyőn is megjelenítünk.

Manapság már nagyon sokféle sablonleírással találkozhatunk: Jade, Mustache, Handlebars, Haml, EJS, Plates, Pure, illetve az Underscore függvénykönyvtárnak is van egy sablonkezelő segédfüggvénye. Nézzük meg az előző példát néhány sablonnyelvben.

Jade

li a(href='show/'+id)= title

Handlebars.js

<li> <a href="show/{{id}}">{{title}}</a> </li>

Underscore.js

<li> <a href="show/<%= id %>"><%= title %></a> </li>

Az így megadott sablonokat szövegként is meg lehet adni, de jelezvén, hogy ez már nem a nézet logikai részéhez tartozik, vagy külön fájlba helyezhetjük, vagy egyszerűen a HTML dokumentumba ágyazzuk egy megfelelő típusú script elembe:

Forráskód
<script id="listItem" type="text/template">
    <li>
        <a href="show/<%= id %>"><%= title %></a>
    </li>
</script>

A speciális type attribútum miatt a böngésző nem értelmezi a script elem tartalmát, viszont azt az id-n keresztül az innerHTML tulajdonsággal el lehet érni, és átadni a sablonfeldolgozónak, amely értelmezi azt (függvényt állítván elő belőle), majd konkrét adatot kapva a kész HTML szöveget is előállítja. Az underscore.js-nél maradva ez a következőképpen néz ki:

Forráskód
//Sablonszöveg kiolvasása
var templateString = $('#listItem').html();
//Sablon fordítása
var compiledTemplate = _.template(templateString);
//Adat megadása
var book = {
    id: 12,
    title: 'JavaScript Patterns'
};
//HTML-szöveg előállítása
var htmlString = compiledTemplate(book);
//A HTML-szöveg beillesztése a dokumentumba
$('#viewWrapper').html(htmlString);

Vissza a tartalomjegyzékhez

Példa MNV keretrendszer megvalósítása

Az alábbiakban az MNV architekturális mintának egy minimális implementációját mutatjuk be. Természetesen ez csupán a szemléltetés célját szolgálja, hogy érzékeltesse, hogyan épülhet fel egy ilyen keretrendszer. (Az alábbi keretrendszer ihletője Addy Osmani Cranium keretrendszere volt.)

Keretrendszerünk, amelyet – mérete és a nyújtott szolgáltatásai alapján – minimvc-nek kereszteltünk, a korábbi fejezetekből megismert programozási mintákra épít. Az egyedi eseményeket a megfigyelő mintán keresztül biztosítjuk (observable), a megfigyelhető objektumoknál pedig az explicit get() és set() metódusokkal operáló változatra esett választásunk. Objektumok bővítését a korábban tárgyalt extendDeep() és extendShallow() metódusok biztosítják. A sablonkezeléshez és néhány további függvényhez az Underscore függvénykönyvtárat, a DOM manipuláláshoz pedig a jQuery könyvtárat használjuk. A keretrendszerhez tartozó objektumokat a minimvc névtér alá szervezzük.

Forráskód
var minimvc = ;

Modell

Egy MNV-s alkalmazásban a feladathoz tartozó adatok és az őket feldolgozó függvények együttese a modell. A legegyszerűbb esetben a modell nem más, mint különböző típusú adatok együttese. Általában az egy funkcióhoz tartozó adatokat és metódusokat egy objektumliterál fog össze, és szűkebb értelemben ezeket hívjuk modellnek. Mivel azonban az MNV mintában a modell eseményeket bocsát ki és megfigyelhető, ezért az objektumliterált megfigyelhetővé kell alakítani. A modellt további segédmetódusokkal láthatjuk el. A minden modellre nézve közös funkcionalitást (observable, get(), set(), stb) a konkrét modell prototípus-objektumában érdemes tárolni.

Forráskód
minimvc.model = (function() {
    var proto = extendDeep({
        get: function(attr) {
            return this.attributes[attr];
        },
        set: function(attr, val) {
            this.attributes[attr] = val;
            this.notify('change:' + attr, val);
            this.notify('change', this.attributes);
        },
        toJSON: function() {
            return _.clone(this.attributes);
        }
    }, observable);
    return {
        create: function(attrs) {
            return extendShallow(Object.create(proto), {
                id: _.uniqueId('model'),
                attributes: attrs || 
            });
        }
    };
})();

Ha például könyvek adatait szeretnénk nyilvántartani, akkor korábban egy könyv adatait objektumliterálként adhattuk meg:

Forráskód
var book = {
    author: 'Stoyan Stefanov',
    title: 'JavaScript Patterns'
};

A modell használatával ez a következőképpen történik:

Forráskód
var book = minimvc.model.create({
    author: 'Stoyan Stefanov',
    title: 'JavaScript Patterns'
});

A modell többletszolgáltatásaként a book objektum minden változását esemény formájában közli az őt megfigyelőkkel:

Forráskód
book.addObserver('change:title', function (val) {
    console.log('A könyv címe megváltozott: ', val);
});
book.set('title', 'Pro JavaScript Patterns');

A nézet

A nézet a modellben tárolt adatok megjelenítésére szolgál. Ennek érdekében a nézet megfigyeli a modellt, és változás esetén a megfelelő változásokat eszközöli a felületen. A nézetek nagyon gyakran sablonfeldolgozókkal dolgoznak együtt a megjelenítendő felület deklaratív leírására. A nézet azonban nem egyezik meg ezekkel a sablonokkal. A nézet az MNV-mintában objektum, és tartalmazza azokat a metódusokat, amelyek a felület kezeléséhez tartoznak. Implementációnkban nem tételezünk semmit sem a nézet leendő tulajdonságairól és metódusairól, így ez nem más, mint egy objektumgenerálóba csomagolt üres objektumliterál, amelyet a konkrét létrehozáskor töltünk fel.

Forráskód
minimvc.view = (function() {
    var proto = ;
    return {
        create: function(attrs) {
            return extendShallow(Object.create(proto), {
                id: _.uniqueId('view')
            }, attrs || );
        }
    };
})();

Vezérlő

A vezérlő feladata a nézetből érkező felhasználói események feldolgozása, és ennek megfelelő utasítások kiadása a modell és a nézet számára. A vezérlő tehát valahol a másik két komponens között helyezkedik el. A nézetből érkező események tulajdonképpen nem mások, mint a megfelelő DOM elemek által kiváltott események. A vezérlőben ezekre lehet feliratkozni és megfelelő metódusokat hozzájuk rendelni. Implementációnkban is ezt a funkcionalitást céloztuk meg, méghozzá úgy, hogy egy vezérlőnek meg lehet adni egy events nevű egymáshoz rendelést az események és a metódusok között. Az egyszerűség kedvéért minden eseményt dokumentumszintről delegálunk vissza a kezelés helyére. Ezen kívül a vezérlőt semmilyen egyéb funkcionalitással nem láttuk el, azt majd létrehozáskor kapja meg (attrs).

Forráskód
minimvc.controller = (function() {
    var proto = ; 
    return {
        create: function(attrs) {
            var obj = extendShallow(Object.create(proto), {
                id: _.uniqueId('controller')
            }, attrs || );
            if (attrs.events) {
                _.each(attrs.events, function(methodName, domEventName) {
                    var parts = domEventName.split('.');
                    var selector = parts[0];
                    var eventType = parts[1];
                    $(document).on(eventType, selector, function(e) {
                        obj[methodName](e);
                    });
                });
            }
            return obj;
        }
    };
})();

A példa keretrendszer használata

Nézzük meg, hogyan használható keretrendszerünk egy egyszerű kis példa esetében. A feladat az, hogy egy könyv adatait jelenítsük meg űrlapelemekben és statikus szövegként is az oldalon. Ha a könyv adataiban bármilyen változás áll be (pl. frissül a háttérben), akkor az adatait a felületen azonnal korrigálni kell. Ha viszont az űrlapmezőben írjuk át, akkor a modellben is módosuljon az adat.

A HTML oldal

A feladathoz tartozó HTML oldal a következőképpen néz ki:

Forráskód
<!doctype html>
<html>
<head>
    <title>Book</title>
</head>
<body>
    <div id="bookForm"></div>
    <div id="bookPanel"></div>
    <script type="text/template" id="bookForm-template">
        Title: <input type="text" id="txtTitle" value="<%= title %>"> <br>
        Author: <input type="text" id="txtAuthor" value="<%= author %>">
    </script>
    <script type="text/template" id="bookPanel-template">
        Title: <%= title %> <br>
        Author: <%= author %>
    </script>
    <script type="text/javascript" src="jquery.js"></script>
    <script type="text/javascript" src="underscore.js"></script>
    <script type="text/javascript" src="extendDeep.js"></script>
    <script type="text/javascript" src="extendShallow.js"></script>
    <script type="text/javascript" src="observable.js"></script>
    <script type="text/javascript" src="minimvc.js"></script>
    <script type="text/javascript" src="book.js"></script>
</body>
</html>

Az űrlap és a statikus megjelenítés sablonját a megfelelő típusó script elemek tartalmazzák. Ezeknek a helyét a bookForm és a bookPanel azonosítójú div-ek jelzik.

A modell definiálása

Egy könyvről egyelőre a szerzőjét és a címét tároljuk. Létrehozását fent már bemutattuk:

Forráskód
var book = minimvc.model.create({
    author: 'Stoyan Stefanov',
    title: 'JavaScript Patterns'
});

Az űrlapnézet

A felületet két nézetre bontjuk: az egyik az űrlapért, a másik a statikus szövegként történő megjelenítésért felel. Az űrlap nézetobjektumát a következőképpen hozzuk létre:

Forráskód
var formView = minimvc.view.create({
    el: '#bookForm',
    model: book,
    template: _.template($('#bookForm-template').html()),
    initialize: function() {
        this.model.addObserver('change', this.updateView.bind(this));
        this.render(this.model.toJSON());
    },
    render: function(data) {
        $(this.el).html(this.template(data));
    },
    updateView: function(data) {
        $(this.el).find('#txtTitle').val(data.title);
        $(this.el).find('#txtAuthor').val(data.author);
    }
});

Először is egy el nevű tulajdonságban tároljuk a nézet tartalmazó elemének szelektorát. Emellett megadjuk, hogy a nézet melyik modellhez tartozik, illetve előkészítjük a sablonfeldolgozót is (template). Az initialize() függvény szolgál a nézet alapállapotba hozására. Ezt majd később expliciten meg kell hívni. Ebben a függvényben definiáljuk, hogy a modell bármilyen változására a nézet updateView() metódusa fusson le. Ebben a modell megfelelő adatát a felületi szöveges beviteli mező értékéül adjuk. Végül a render() metódussal megjelenítjük az űrlapelemeket.

A panelnézet

A statikus kiírásért felelős panelnézet hasonlóképpen néz ki. Figyeli a modell változásait, és változás esetén teljesen újrarajzolja magát. Őt is az initialize() függvénnyel lehet útjára indítani.

Forráskód
var panelView = minimvc.view.create({
    el: '#bookPanel',
    model: book,
    template: _.template($('#bookPanel-template').html()),
    initialize: function() {
        this.model.addObserver('change', this.render.bind(this));
        this.render(this.model.toJSON());
    },
    render: function(data) {
        $(this.el).html(this.template(data));
    }
});

A vezérlő

A vezérlő mindhárom másik komponensre tartalmaz egy hivatkozást. Az initialize() függvényben elindítja a nézeteket. Ugyanakkor jelezzük azt is, hogy a két űrlapmezőből érkező keyup eseményeknél a writing() metódus meghívása történjen. Ebben frissítjük a modell megfelelő elemeit a set() metóduson keresztül.

Forráskód
var bookController = minimvc.controller.create({
    model: book,
    formView: formView,
    panelView: panelView,
    events: {
        '#txtTitle.keyup': 'writing',
        '#txtAuthor.keyup': 'writing'
    },
    initialize: function() {
        this.formView.initialize();
        this.panelView.initialize();
    },
    writing: function(e) {
        this.model.set('title', $('#txtTitle').val());
        this.model.set('author', $('#txtAuthor').val());
    }
});

Végül az egész alkalmazást elindítjuk:

Forráskód
bookController.initialize();

A végeredmény az alábbi ábrán látszik:

A minimvc keretrendszerrel megoldott példaalkalmazás működés közbenA minimvc keretrendszerrel megoldott példaalkalmazás működés közben

Vissza a tartalomjegyzékhez

Egy professzionális MN* keretrendszer bemutatása – Backbone.js

Manapság már számos keretrendszer létezik, amelyek egyoldalas alkalmazások írását segíti elő, és a fenti aspektusok valamelyikével vagy mindegyikével foglalkozik. A keretrendszerek – durván leegyszerűsítve – két kategóriába esnek: minimalisták vagy monolitikusak. A minimalista keretrendszerek (ilyen például a Backbone, Knockout) hasznos objektumok és függvények tárházát biztosítják, de semmilyen megkötést nem írnak elő arra vonatkozóan, hogy hogyan kell az egész alkalmazást felépíteni. A monolitikus keretrendszerek (mint például Angular vagy Ember) az alapvető koncepciók összeépítésére, az egész alkalmazás architektúrájára vonatkozóan is határozott megkötéseket tartalmaznak.

Az egyes keretrendszereket jól mutatja be Addy Osmani TodoMVC projektje, amelyben ugyanazt az alkalmazást különböző keretrendszerekben oldották meg. Érdemes itt körülnézni, hogy mennyiféle keretrendszer létezik (nem teljes a lista), gyors betekintés nyerhető egy-egy keretrendszer működésébe a forrást böngészve, valamint az egyes projektek össze is hasonlíthatóak egymással, így ki-ki kedvére kiválaszthatja a számára szimpatikus megoldásokat tartalmazó keretrendszert.

Backbone.js

A Backbone.js az MNV-mintát implementáló kisméretű függvénykönyvtár, amely keretet ad a kliensoldali alkalmazásunk kódjának. Használatával a feladatkörök elválaszthatóvá válnak, és így alkalmazásunk karbantarthatósága hosszú távon is fenntarthatóvá válik. Kis- és nagyméretű programok írására is alkalmas, és semmilyen megkötést nem ad az alkalmazás egészének felépítésére. Felhasználható csupán egy része is (pl. csak a modell), de sokkal jellemzőbb, hogy segítségével nagy méretű egyoldalas alkalmazásokat írnak. Igen elterjedt, rengeteg Backbone-ban írt alkalmazás bizonyítja koncepciójának sikerességét. Sokan használják, és viszonylag nagy a közösség körülötte, így a viszonylag jól használható dokumentáció mellett különböző közösségi portálokról is sok információ megtudható. Forráskódja érthető és megjegyzésekkel teli. Alapkönyvtár lévén maguk a keretrendszer fejlesztői is arra buzdítják a fejlesztőket, hogy bátran bővítsék az alapkönyvtár funkcionalitását.

Ebben a tananyagban nem az a célunk, hogy mélyében megértsünk egy professzionális könyvtárat. Ezt az olvasóra bízzuk. Ebben a fejezetben csupán arra szeretnénk ráirányítani a figyelmet, hogy a professzionális könyvtárak is alapvetően azokra a programozási és tervezési mintákra épülnek, amelyeket fent is láttunk. Természetesen az alapkoncepciókat további funkcionalitással bővítik, hogy kielégítsék a modern alkalmazásfejlesztés igényeit.

Bár a Backbone-t legtöbben MNV-mintát megvalósító keretrendszernek tartják, a keretrendszer több helyen eltér ettől, igazodva a kliensoldali környezet sajátosságaihoz. Ahogy minden egyéb MN*-os könyvtárban, itt is megtalálható a modell és a nézet komponens, elválasztván az adatokat a megjelenéstől. Modelleket a Backbone.Modell konstruktorfüggvény vagy annak leszármazottaival lehet létrehozni. A Backbone emellett bevezeti a gyűjtemények fogalmát is a Backbone.Collection konstruktorfüggvényen keresztül, amely nem más, mint a modellek tömbjének megfelelő koncepció.

A Backbone keretrendszer komponenseinek kapcsolataA Backbone keretrendszer komponenseinek kapcsolata

Nézetet létrehozni a Backbone.View függvény segítségével lehet. Itt is lehet és érdemes is származtatni az alaposztályból. Vezérlővel azonban nem találkozunk a Backbone könyvtárban, ennek funkcionalitása – a felületről érkező események metódusokhoz való hozzárendelése – beolvadt a nézet feladatkörébe. Itt tehát a nézetek azok, amik az alkalmazás központi szerepét töltik be.

Ezen kívül megjelenik még egy komponens, a Backbone.Router, amely az alkalmazás URL-jét kezeli, és azokat az alkalmazás megfelelő részéhez kapcsolja.

Az alábbiakban áttekintjük ezeknek a komponenseknek a Backbone-specifikus voltát, kiemeljük néhány fontosabb tulajdonságukat.

A modell

A modellnek Backbone-ban is az a feladata, mint általában az MN*-mintában: tárolni az adatokat és a hozzájuk tartozó logikát. Új modellkonstruktort a Backbone.Model kiterjesztésével hozhatunk létre (ha semmilyen plusz logikára nincsen szükség, akkor egyszerűen a Backbone.Model is használható modell létrehozására).

Forráskód
var Book = Backbone.Model.extend();
var book1 = new Book({
    title: 'JavaScript Patterns',
    author: 'Stoyan Stefanov'
});

Megfigyelhető objektumok

A Backbone modellek is megfigyelhető objektumok és változásaikat megfelelő eseményeken keresztül közlik. Ezt a keretrendszer úgy éri el, hogy a megfigyelő minta implementációját, a Backbone.Events objektummal bővíti a Backbone.Model prototípusát. A megfigyelésre a jQueryből ismert függvényhármas szolgál:

get() és set() metódusok

A megfigyelhető objektumok miatt itt is speciális getter és setter függvényekre van szükség az adattagok lekérdezésére és beállítására. Változáskor change vagy change:attr eseményeket küldenek.

Forráskód
ok( book1.get('title') === 'JavaScript Patterns', 'A get() metódus szolgál lekérdezésre');
book1.on('change:title', function (obj) {
    console.log('A könyv címe megváltozott: ', obj)
});
book1.set('title', 'JavaScript Patternity');

Alapértelmezett értékek

A modellkonstruktor létrehozásakor meghatározhatjuk, hogy új objektumok készítésekor azok milyen alapértelmezett értéket kapjanak, ha az nincs expliciten meghatározva.

Forráskód
var Book = Backbone.Model.extend({
    defaults: {
        title: 'Generic book title',
        author: 'Anonymous'
    }
});
var book1 = new Book({
    title: 'JavaScript Patterns'
});
ok( book1.get('author') === 'Anonymous', 'Alapértelmezett értékű a szerző');

Modell inicializálása

A modellnek megadható egy függvény, amely mindig lefut új modellobjektumok létrehozásakor.

Forráskód
var Book = Backbone.Model.extend({
    initialize: function () {
        console.log('Inicializálás')
    }
});
var book1 = new Book();

Modell adatainak másolata

A modellben tárolt adatokról másolatot a toJSON() metódus segítségével kaphatunk.

Forráskód
var book1 = new Book({
    title: 'JavaScript Patterns',
    author: 'Stoyan Stefanov'
});
console.log(book1.toJSON());

Modell mentése

A Backbone lehetőséget ad arra, hogy a modell adatait egy háttérrendszernek elküldve perzisztáljuk (a modell toJSON() metódusát használva). Erre a modell save() vagy sync() metódusa való. Mentéskor automatikusan meghívódik a modell validate() metódusa, amin keresztül a modelladatok ellenőrzésére is lehetőség van. Ezeknek részletes tárgyalása azonban a tananyag keretein kívül esik.

A gyűjtemény

A gyűjtemények Backbone-ban nem mások, mint bizonyos modelleknek a tömbje. Nagyon hasznos absztrakció, amin keresztül modellek sorozatát kezelhetjük. Létrehozásuk a Backbone.Collection kiterjesztésével lehetséges. Alapértelmezett a Backbone.Model konstruktort használja a modellek létrehozására, de ez felüldefiniálható a model opció megadásával.

Forráskód
var Books = Backbone.Collection.extend({
    model: Book
});

Modellek hozzáadása és elvétele

Egy gyűjteményt feltölteni modellekkel vagy létrehozáskor lehet, paraméterként megadva a modellek tömbjét, vagy később az add() metódus segítségével. Eltávolításra a remove() utasítás szolgál.

Forráskód
var book1 = new Book({title: 'JavaScript Patterns'}),
    book2 = new Book({title: 'JavaScript: The Good Parts'}),
    book3 = new Book({title: 'Maintainable JavaScript'});
var books = new Books([book1, book2]);
books.add(book3);
books.add({title: 'Testable JavaScript'});
books.remove(book3);

Modell lekérdezése

A gyűjteményből a get() metódussal lehet egy modellt kiolvasni. Minden modellnek van egy id és egy cid attribútuma. Az előbbi a modellt azonosítja, az utóbbit a Backbone generálja (hasznos pl. akkor, ha még nincs id-ja a modellnek, pl. amiatt, mert nem mentettük még el). A get() metódusnak vagy egy id-t, vagy egy cid-et kell megadni.

Forráskód
var model = books.get(1);

A gyűjtemény mint megfigyelhető objektum

A gyűjtemények is megfigyelhető objektumok, és mint ilyenek megfelelő eseményekkel tudatják környezetüket állapotváltozásaikról. Ilyen például többek között az add, remove és change esemény.

Forráskód
books.on('add', function (book) {
    console.log(book);
})

Egész gyűjtemény beállítása

Az egyesével való hozzáadás és törlés helyett egyszerre több modellt is a gyűjteményhez adhatunk. A set() metódussal modellek tömbje adható meg a gyűjteménynek, ráadásul ez azt is figyeli, hogy ugyanaz a modell ne kerüljön kétszer a gyűjteménybe. Az egész gyűjtemény lecserélésére a reset() metódus szolgál, ami ugyancsak modellek tömbjét várja paraméterül. A gyűjtemény törlésére is ez utóbbi metódus szolgál; ekkor ugyanis paraméter nélkül meghívva a kívánt eredményt érjük el.

Tömbfüggvények

A Backbone az Underscore könyvtárból kölcsönöz rengeteg metódust, amivel a gyűjtemények feldolgozása könnyebbé válhat. (Itt érdemes megjegyezni, hogy eredetileg az Underscore is a Backbone része volt, később vált ki külön függvénykönyvtárként belőle. Mindkettejük írója a DocumentCloud cégnél dolgozó Jeremy Ashkenas.) A legtöbb Underscore függvény manapság már elérhető az EcmaScript 5 nyelvi bővítéseinek köszönhetően, de vannak köztük egyediek is.

Forráskód
//Csak a címek kigyűjtése
var titles = books.pluck('title');
console.log(titles);
//Kiválogatás
var filtered = books.filter(function (book) {
    return book.get('title').indexOf('Pa') > -1;
});
console.log(filtered);

A nézet

Egy Backbone alkalmazásban a nézetek töltik be a fő vezérlő szerepet. Amellett, hogy a megjelenítéshez szükséges logikát tárolják, ők kezelik a felhasználói felületről érkező eseményeket, azokat feldolgozó metódusokhoz rendelve. A Backbone nézet tehát egyszerre tölti be a klasszikus MNV-minta vezérlő és nézet szerepét. Maga a nézet HTML kódot nem tartalmaz, az tipikusan valamilyen sablonnyelvben van megadva, és annak feldolgozóján keresztül történik a nézetbeli felhasználása. A Backbone nézetek erősen építenek a jQuery-re, vele oldják meg az elemek dinamikus kezelését, és az eseménykezelést is (valamint a modellek mentésekor az Ajax műveleteket).

Új nézet létrehozása és a megjelenítés

Új nézet létrehozásához a Backbone.View konstruktorfüggvény kiterjesztése szükséges. Ahogy a modelleknek és a gyűjteményeknek, úgy a nézetnek is meg lehet adni egy initialize() függvényt, mely a nézetobjektum létrejöttekor fut le, és tipikusan a nézet működéséhez szükséges kezdeti lépések elvégzése, mint például a modell eseményeire feliratkozás, valamint az első kirajzolás szerepel benne. A nézet felületének megjelenítéséért általában egy render() metódus felel, ez adja át a nézethez tartozó modellt a nézet mögött álló sablonnak.

Forráskód
var BookView = Backbone.View.extend({
    el: '#bookDiv',
    template: $('#book-template').html(),
    model: book1,
    initialize: function () {
        this.listenTo(this.model, 'change', this.render);
    },
    render: function () {
        this.$el.html(this.template(this.model.toJSON()));
    }   
});

A nézetben speciális szerepet tölt be az el és a $el tulajdonság. Az el azt az elemet jelöli ki az oldalon, amelyhez a nézet tartozik, és amelyen belül a megjelenítés történik. Az elem kijelölése CSS szelektorral történik. Mivel általában jQuery metódusokat használunk rajta, ezért a keretrendszer eltárolja a jQuery függvény által visszaadott objektumot; ez a $el. Ha nincs az oldalon még az az elem, amihez csatolni szeretnénk a nézetet, akkor megkérhetjük, hogy készítse el nekünk. Ehhez meg kell adni a tagName, className, id attribútumokat (az utóbbi kettő opcionális), és ezekből a keretrendszer elkészíti a megfelelő elemet, és az el tulajdonságon keresztül elérhetővé teszi. Mivel általában a nézeten belüli elemekre hivatkozunk, ezért a Backbone a this.$() metóduson keresztül a nézet kontextusában lefutó jQuery függvényt bocsát rendelkezésünkre.

A fenti példában a nézet a listenTo() metódussal feliratkozott a modell change eseményére a render() metódusával, így az abban bekövetkező bármilyen változáskor a nézet újra kirajzolja magát. A listenTo() metódus nem más, mint a megfigyelő oldaláról megfogalmazott megfigyelési igény, az on() metódus egyik variánsa.

Eseménykezelés

A felhasználói felületről érkező eseményeket az events objektumban megadott leképezéssel rendelhetjük a nézet metódusaihoz. Kulcsként az esemény típusát kell megadni szóközzel elválasztva az utána jövő szelektorral, ami az el elemen belül értelmezendő. Ehhez a kulcshoz kell megadni az esemény bekövetkeztekor lefutó metódust, amit vagy függvényliterálként vagy metódusnévként adunk meg szöveges formában. A háttérben a keretrendszer az események delegálását használja, azaz minden eseményt az el elemhez köt, és a szelektoroknak megfelelően hívja meg az adott elem kontextusában. Szelektor híján az el elemen hívódik meg az esemény.

Forráskód
var BookView = Backbone.View.extend({
    el: '#bookDiv',
    events: {
        'click #button': 'save',
        'keyup input': 'writing'
    },
    save: function (e) { /*...*/ },
    writing: function (e) { /*...*/ },
});

Az útvonalválasztó

Backbone-ban a router feladata az URL-ben bekövetkező változásokkor a megfelelő metódus meghívása. Általában alkalmazásonként egy router elegendő e feladat elvégzéséhez. Háttérben a Backbone a böngészők History API-ját használja, ha elérhető. Ha nem érhető el, akkor egy régebbi módszerre tér vissza, amely # karaktereket használ az URL-ben. A router központi eleme a routes leképezés, amely a különböző URL mintákhoz rendel metódusokat. Ennek külön paraméterezése lehetséges, amelynek tárgyalása nem e tananyag keretei közé valók. A hivatalos dokumentáció ezzel foglalkozó része jól eligazítja az ez iránt érdeklődőt.

Forráskód
var BookRouter = Backbone.Router.extend({
    routes: {
        "about" : "showAbout",
        /* Például: http://example.com/#about */
        "book/:id" : "getBook",
        /* Például: http://example.com/#todo/5 */
    },
    showAbout: function(){
    },
    getBook: function(id){
        console.log("A keresett könyv: " + id);
    }
});
var bookRouter = new BookRouter();
Backbone.history.start();

Példaalkalmazás bemutatása Backbone.js segítségével

A minimvc keretrendszerünk bemutatásakor írt alkalmazást elkészítettük Backbone keretrendszer segítségével is. Talán a fenti bemutatásból kiderült, hogy a két keretrendszer sok mindenben hasonlít. Természetesen a Backbone sokkal gazdagabb tárházát nyújtja a funkcionalitásoknak, mint a minimvc, így benne egy kicsit könnyebben készíthető el az alkalmazás.

A HTML sablon

A sablont illetően nem történt változás, ugyanazzal a HTML szerkezettel dolgozunk. Egyedül a felhasználandó szkriptek köre egyszerűsödött le:

Forráskód
<script type="text/javascript" src="../objects/jquery.js"></script>
<script type="text/javascript" src="../objects/underscore.js"></script>
<script type="text/javascript" src="../objects/backbone.js"></script>
<script type="text/javascript" src="book_backbone.js"></script>

A modell

A modell továbbra is egy title és egy author mezőből áll.

Forráskód
var BookModel = Backbone.Model.extend({
    defaults: {
        title: '',
        author: ''
    }
});
var book = new BookModel({
    title: 'JavaScript Patterns',
    author: 'Stoyan Stefanov'
});

Az űrlap nézet

Mivel Backbone-ban a nézetek töltik be a vezérlő szerepét is, így itt jelent meg a megjelenítésen túl az eseménykezelők definiálása is.

Forráskód
var FormView = Backbone.View.extend({
    el: '#bookForm',
    model: book,
    template: _.template($('#bookForm-template').html()),
    events: {
        'keyup input': 'writing'
    },
    initialize: function() {
        this.listenTo(this.model, 'change', this.updateView);
        this.render();
    },
    render: function() {
        this.$el.html(this.template(this.model.toJSON()));
    },
    updateView: function(data) {
        //console.log(arguments);
        this.$('#txtTitle').val(data.get('title'));
        this.$('#txtAuthor').val(data.get('author'));
    },
    writing: function(e) {
        this.model.set('title', $('#txtTitle').val());
        this.model.set('author', $('#txtAuthor').val());
    }
});

A statikus nézet

Forráskód
var PanelView = Backbone.View.extend({
    el: '#bookPanel',
    model: book,
    template: _.template($('#bookPanel-template').html()),
    initialize: function() {
        this.listenTo(this.model, 'change', this.render);
        this.render();
    },
    render: function() {
        this.$el.html(this.template(this.model.toJSON()));
    }
});

A „főprogram”

Az egész alkalmazás indításához egy újabb nézetet, az AppView-t hoztuk létre. Erre csupán formálisan van szükség, ugyanis az alkalmazás a fenti két nézet példányosításából áll. Ezt a logikát foglalja egybe az AppView initialize() metódusa. Az alkalmazás indítása így az AppView példányosításából áll.

Forráskód
var AppView = Backbone.View.extend({
    initialize: function() {
        var formView = new FormView();
        var panelView = new PanelView();
    }
});
new AppView();

További példák

Hivatkozások

Magas szintű programtervezési minták

Flash lejátszó letöltése

Magas szintű programtervezési minták

Vissza a tartalomjegyzékhez

Új Széchenyi terv
A projekt az Európai Unió támogatásával, az Európai Szociális Alap társfinanszirozásával valósul meg.

A tananyag az ELTE - PPKE informatika tananyagfejlesztési projekt (TÁMOP-4.1.2.A/1-11/1-2011-0052) keretében valósult meg.
A tananyag elkészítéséhez az ELTESCORM keretrendszert használtuk.