Přejít k navigační liště

Zdroják » JavaScript » Zlepšite svoje jQuery – OOP, menné priestory, pub-sub

Zlepšite svoje jQuery – OOP, menné priestory, pub-sub

Články JavaScript, Různé

Denne využívam JS framework jQuery k svojej práci. Napriek nepopierateľným výhodám, ktoré tento framework do života webdevelopera prináša, nepredstavuje klasická štruktúra jQuery kódu ideálny spôsob písania väčšieho projektu. V tomto screencaste vám chcem predstaviť odlišnú štruktúru JS aplikácie, ktorá vám umožní „vyrásť“.

K natočeniu screencastu ma viedlo objavenie jednoduchého riešenia, ktoré predstavuje kompromis medzi používaním nových a zaužívaných postupov. Na jednej strane využívanie OOP, menných priestorov a event programing. Z druhého konca potom zužitkovanie znalosti jQuery a eliminovanie potreby učiť sa nejakú inú knižnicu. Celé to dáva dohromady minimálny krok, ktorý môžu frontend developeri podstúpiť, aby nestratili kontakt s modernými riešeniami a zároveň  nemuseli podstupovať dlhé školenia.

live demo zdrojové kódy

Klasické písanie s jQuery

Keď dnes počujeme niekoho povedať „na stránke je použité jQuery“, nemyslí tým nič iné ako napráskanie celej JS aplikácie do jednej $(document).ready(function(){ /* code_here*/ }); Ukážka z live dema použitého v screencaste:

$(function(){

        var uniqId = function() {
                return Math.random() * Math.pow(10, 17) + Math.random() * Math.pow(10, 17) + Math.random() * Math.pow(10, 17);
        };

        var $searchForm = $('#searchForm');
        var $searchResults = $('#searchResults');
        var $searchLogger = $('#searchLogger');

        $searchForm.submit(function(ev){
                ev.preventDefault();

                var rqid = uniqId();
                var q = $searchForm.find('[name="q"]').val();

                $searchLogger.append('<li class="loading" data-rqid="'+ rqid +'"><a href="#">' + q + '</a></li>');
                $.ajax({
                        url             : 'http://query.yahooapis.com/v1/public/yql',
                        type    : 'POST',
                        dataType: 'json',
                        data    : {
                                callback        : '',
                                format          : 'json',
                                env                     : 'store://datatables.org/alltableswithkeys',
                                q                       : 'SELECT title, href FROM digg.search.search WHERE query="' + q + '" LIMIT 5'
                        },

                        beforeSend: function($xhr, settings) {
                                $xhr.rqid = rqid;
                        },

                        success: function(data, state, $xhr) {
                                var html = '';

                                $searchLogger.find('[data-rqid="' + $xhr.rqid + '"]')
                                 .removeClass('loading')
                                 .removeClass('error')
                                 .addClass('ok');

                                $.each(data.query.results.stories, function(idx, obj){
                                        html += '<li><a href="' + obj.href + '">' + obj.title + '</a></li>';
                                });

                                $searchResults.html(html);
                        },
                        error: function(xhr){
                                $searchLogger.find('[data-rqid="' + $xhr.rqid + '"')
                                 .removeClass('loading')
                                 .removeClass('ok')
                                 .addClass('error');
                        }
                });
        });

});

Aj keď kód napíšete najčistejšie ako sa len dá (o čo som sa snažil v ukážke), dostanete v podstate procedurálny kód bez štruktúry. Čerešničkou na torte je, že kód je mix synchrónnych a asynchrónnych blokov.

Nezriedka sa stane, že vám projekt rastie pod rukami (požiadavkami od zákazníka). Keď skúsite takúto aplikáciu škálovať, rýchlo stratíte podporu zo strany jQuery. Chtiac-nechtiac musíte podstúpiť buď návrat k raw Javascriptu, alebo siahnuť po sofistikovanej knižnici (Dojo, Google Closure), čo stojí čas/peniaze. Medzi základne ingrediencie pre zlepšenie vášho Javascriptu patria:

Menné priestory

O výhodách používania menných priestorov by vám mohli rozprávať členovia Nette Framework komunity, ktorí switchovali v minulom roku a pol. Nenájdete jedného, ktorý by tvrdil, že to bol chybný krok, alebo že sa chce vrátiť späť. V rovnakom období začala potreba používať menné
priestory aj na obyčajné webštúdiá/fre­elancerov.

Základné informácie o menných priestoroch v Javascripte zhrnul Martin Malý tu na Zdrojáku, v článku Základní vzory pro vytváření jmenných prostorů v JavaScriptu. Menné priestory si automaticky vynútia nový spôsob organizácie kódu. Už si nevystačíme s jediným súborom. Ideálne je začať používať štandard PSR-0, ktorý definuje umiestnenie súboru s kódom podľa schémy <ApplicationName>/(<Namespace>)*/<Class Name>.

web_root/js/

Lorem/
  Form/
    Search.js

  SearchLogger.js
  SearchResults.js

Okrem toho budeme potrebovať nejaký súbor pre základnú inicializáciu aplikácie. Najdôležitejšou sekciou je kód s definíciou globálneho namespace kontainera.

<script scr="path/to/jquery.js"></script>
<script scr="path/to/jquery.pubsub.js"></script>

<script scr="path/to/Lorem/bootstrap.js"></script>

web_root/js/Lo­rem/bootstrap­.js

var Lorem = Lorem || {}; // definujeme základný namespace kontajner

Lorem.uniqId = function() { // pridáme do namespace statickú utility metódu
    return Math.random() * Math.pow(10, 17) + Math.random() * Math.pow(10, 17) + Math.random() * Math.pow(10, 17);
}

Potom načítame súbory s definíciami tried našej aplikácie (viď. ďalší krok OOP v Javascripte).

<script scr="path/to/jquery.js"></script>
<script scr="path/to/jquery.pubsub.js"></script>

<script scr="path/to/Lorem/bootstrap.js"></script>
<script scr="path/to/Lorem/Form/Search.js"></script>
<script scr="path/to/Lorem/SearchLogger.js"></script>
<script scr="path/to/Lorem/SearchResults.js"></script>
<script scr="path/to/Lorem/app.js"></script>

Samotné spustenie aplikácie príde nakoniec v samostatnom súbore. Môžete využiť $(document).ready(function) aby sa aplikácia nespustila pred skonštruovaním DOM. Aplikáciu obalíme vlastným scope, do ktorého vstriekneme namespace.

web_root/js/Lo­rem/app.js

;jQuery(document).ready(function() {

    ;(function($, undefined){

        // this === Lorem

        // vas kod

    }).call(Lorem, jQuery);

});

OOP v Javascripte

jQuery nás učí aplikáciu abstrahovať do jQuery kolekcií – objektov získaných cez CSS3 selektory. Takýto prístup nie je z dlhodobého hľadiska vhodný. Nie je možné jednoducho encapsulovať do takéhoto objektu nejaké metódy a výsledok znovuvyužiť v inom (alebo dokonca to istom) projekte.

Na server-side však väčšina z vás ovládla class-based OOP. Javascript žiadne triedy neponúka. Razí si cestu takzvaným prototypovým OOP. Túto tému na Zdrojáku výborne spracoval Daniel Steigerwald, v druhom diely jeho seriálu ukazuje rovnaký spôsob emulácie tried, aký používa CoffeeScript. Rozhodne si ho prečítajte, než budete pokračovať.

Najjednoduchším spôsobom, ako na prototypové OOP naskočiť, je na stránke identifikovať jednotlivé funkčné entity a tie abstrahovať ako Javascript objekty. Nebudeme ich však definovať ako objektové literály, ale necháme si vytvoriť živé inštancie pomocou takzvanej konštrukčnej funkcie. Jej definíciu umiestnime na požadované miesto v namespace.

web_root/js/Lo­rem/Form/Sear­ch.js

Lorem.Form.Search = function($node) {
    this.node = $node;
};

web_root/js/Lo­rem/app.js

var searchForm = new Lorem.Form.Search($('#searchForm'));

Každé volanie konštrukčnej funkcie vráti nový objekt, ktorý odpovedá jej vnútornému scope. Vrátenie tohto objektu netreba explicitne definovať.

console.log(searchForm);             // { node=[form#searchForm] }
console.log(searchForm.constructor); // function() - Lorem.Form.Search

Nezabúdajte, že aj funkcie sú v Javascripte objekt. Každá funkcia, definovaná ľubovoľným spôsobom, obsahuje property prototype čo je tiež objekt.

console.log(Lorem.Form.Search.prototype);  // Object { }

Do tohoto prázdneho objektu je možné priradiť ľubovoľné properties (najčastejšie metódy). Na tie potom vedie skrytý link z inštancie, ktorú konštrukčná funkcia vytvorí.

Lorem.Form.Search.prototype.validate = function() {};
Lorem.Form.Search.prototype.submit = fucntion() {};

console.log(Lorem.Form.Search.prototype.validate === searchForm.validate); // true

Inštancia neobsahuje vlastnú kópiu metódy, ale skrytý link vedie do prototype konštrukčnej funkcie. Metóda je tak v pamäti iba 1×.

Volanie metód skrýva malý zádrheľ – metódy veľmi ochotne preberú scope volajúceho kódu. V praxi to znamená, že ak v metóde odkazujete pomocou this, nie vždy ukazuje na nášu inštanciu. Našťastie zjednať nápravu je jedoduché pomocou metódy bind, ktorá vráti funkciu so správne nastaveným scope. V odkazovanom článku nájdete aj tzv. polyfil, ktorý vám bind sprístupní, aj keď ju váš JS engine nepodporuje.

web_root/js/Lo­rem/Form/Sear­ch.js

Lorem.Form.Search = function($node) {
    this.node = $node;
    this.node.submit(this.validate.bind(this));
};

Pub-Sub

Asynchrónne vykonávanie kódu je takmer základným paradigmom v Javascripte (obzvlášť v browseri). Kedykoľvek klepnete na element, stlačíte klávesu alebo zascrollujete stránkou, JS engine na pozadí oznamuje tieto udalosti. V javascripte si môžete napísať tzv. event-handler – funkciu, ktorá sa zavesí na udalosť (event) a je spustená v potom, čo udalosť nastane. Funkcia pri spustení dostane od prostredia tzv. event-objekt, ktorý event popisuje.

Málokto sa však stretol s tým, že je možné vyvolávať vlastné udalosti, ktoré na rozdiel od tých natívnych (dalo by sa povedať hardwarových) sú plne vo vašich rukách. Dobre zvládnutá knižnica vám umožní implementovať vzor observer a na jeden event navesiť ľubovoľný počet poslucháčov (subscriberov). Vaša aplikácia získa nový rozmer – publisher eventu môže filtrovať natívne eventy, predať subscriberom dodatočné parametre a pod.

jQuery samotné umožňuje techniku pub-sub implementovať, jej tragédiou je však pevné zviazanie s DOM elementom, čo neskutočne degraduje výkon. Pre bežné prípady je vhodnejší jednoduchý jQuery plugin.

$.publish('topic', [param1, param1]);

// na inom mieste
var handle = $.subscribe('topic', function topicHandler(params) {

});

// odhlasenie z /odposluchu/ udalosti
$.unsubscribe([handle]);

V aplikácii je už potom hračkou navesiť na eventy metódy vašich objektov:

web_root/js/Lo­rem/SearchLog­ger.js

Lorem.SearchLogger = function($node) {
    this.node = $node;

    $.subscribe('Form.Search.submit', this.handleSubmit.bind(this));
    $.subscribe('Form.Search.results', this.handleOk.bind(this));
    $.subscribe('Form.Search.error', this.handleError.bind(this));
};

Malé triky

Snažte sa zo svojích aplikácií odstrániť a ďalej nezavádzať tight coupling – pevné závislosti na kód mimo aktuálny modul. Závislosti vždy predávajte zvonka (cez konštruktor, alebo setter metódy), alebo sa spoliehajte na dohodnutú statickú metódu vo vašom namespace. Nikdy však priamo nevolajte metódy 3rd-party knižnice. Tú môže ktokoľvek vymeniť. Práve vtedy oceníte, že nebudete musieť prechádzať polovicu projektu a opravovať volania API.

V predchádzajúcom príklade sme takúto tesnú závislosť vytvorili – metódu subscribe() voláme priamo z knižnice. Správnejšie bude, keď na to v namespace vyhradíme nejakú utility metódu

web_root/js/Lo­rem/bootstrap­.js

Lorem.publish = function(topic, args) {
        $.publish(topic, args);
};
Lorem.subscribe = function(topic, cb) {
        $.subscribe(topic, cb);
};

web_root/js/Lo­rem/SearchLog­ger.js

Lorem.SearchLogger = function($node) {
    this.node = $node;

    Lorem.subscribe('Form.Search.submit', this.handleSubmit.bind(this));
    Lorem.subscribe('Form.Search.results', this.handleOk.bind(this));
    Lorem.subscribe('Form.Search.error', this.handleError.bind(this));
};

Podobnú logiku uplatňujeme aj v AJAX komunikácii. Ak sa už cítite sebaistejší s prototypovým OOP, napíšte si nejaký AJAX adaptér, ktorý predáte všetkým potomkom nejakého BaseModule. Nikdy však nevystrkujte ruky do globálneho scope zo svojích objektov!

Zhrnutie a zdroje

V screencaste (tutoriáli) som vám predviedol drobné kroky, ktoré môžete začať robiť ihneď a nevzdať sa pritom sebaistoty, ktorú vám poskytujú znalosti jQuery. Nastúpite, myslím si, na správnu cestu k spravovateľnejším Javascript aplikáciám, nad ktorými nestrávite desiatky minút snahou pochopiť polroka starý kód.

Komentáře

Subscribe
Upozornit na
guest
22 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
blizz

pekný článok ja mám ešte jedno vylepšenie, súbor JQuery nechať samostatne(kôli kešovaniu) a všetky ostatné JSká spojiť do jedného súboru a použiť na ne minifier (ja mám na to vlastnú aplikáciu na správu projektu ktorá mi to robí automaticky) takto môže mať projekt kľudne aj 50 JS súborov, ale počet HTTP požiadavkov (ktoré zbytočne spomalujú načítanie stránky) sa zníži na dve.

blizz

a inak jQuery wrapper nad XmlHttpRequest – JQuery.ajax obsahuje bugy (v opere funguje nekorektne) preto je lepšie používať priamo XmlHttpRequest.

Petrrr

Mohl by jsi to nejak rozvest?

blizz

v Opere sa namiesto objektu typu XmlHttpRequest občas vytvorí XmlSerializer.

Oldis

Ano, ses takovy maly objevitel ameriky.

Alois.Janicek

To je chyba na mém přijímači, nebo ten screencast má 480p a já tam hovno kódu přečtu, když je to rozmazaný jak prase…?!

Čelo

Tak snad se na YT ještě objeví. Když nahrávám sám videa na YT, tak jsou HD verze dostupné o něco později.

Martin Malý

Omlouvám se, zjistím kde je problém a kdyžtak reuploadnu. Zatím prosím využijte odkazů pod videem. Děkuji za strpení.

Martin Malý

Video v HD kvalitě nahozeno. Omlouvám se, už by mělo být vše OK

František Kučera

No sláva, konečně to hraje i v prohlížeči a bez Flashe :-)

Pilgrim

Troufám si o sobě říct, že po mnoha letech práce s jQuery jej ovládám na vysoké úrovni. Proto chci jen dodat a potvrdit, že pokud někdo chce psát něco s pomocí jQuery, musí bezprostředně znát dokonale samotný JavaScript. I když je jQuery framework, tak stejně nepokryje vše, co by měl velký projekt mít.
Dnes už nepíšu nic klasickym stylem jQuery, ale každá funkce je psaná jako tzv. plugin do jQuery, abych mohl využít plně objekty a DOM.

maryo

Cool, ten trik s .bind jsem neznal.
Jen malej bezvyznamnej detail. Ma nejakej vyznam proc this.node = $node a ne this.$node = $node?

Jadro007

Moc pěkné video, díky.

Jenom bych upozornil na výslovnost „event“, čte se to jinak :)

Daniel Steigerwald

PubSub pro objekty v jQuery funguje, a nevím nic o tom, že by byl pevně svázaný s elementem, nebo že by snad byl výkonostně problematický.
Příklad http://jsfiddle.net/fReSv/
Nevidím žádný smysl v tom, reimplementovat PubSub. Naopak, díky jQuery můžeme těžit z toho, že mám e.preventDefault např. i na syntetických eventech.

Daniel Steigerwald

Jinak článek se mi libí, jen bych doplnil.
1) Pro definici namespace bych použil nějakou takovouhle funkci http://stackoverflow.com/questions/527089/is-it-possible-to-create-a-namespace-in-jquery

2) Rozhodně bych ručně neudržoval seznam skriptů v HEADeru. Raději bych použil RequireJS. Investovat hoďku času za to stojí. Hlavně díky optimizeru.

maryo

Taky jsem s tim mel nekdy driv problem a musel jsem ten event zachytavat na DOM elementu. Mozna, ze to teda opravili.

juzna.cz

Vubec se mi nelibi, jak retezis funckce validate a submit. Funkce by mela mit vystizne jmeno podle ktereho by melo byt jasne co dela. Kdyz se podivas na funkci validate, co asi dela? Je ti hned jasne, ze „validuje a odesila formular“? Radeji bych pridal dalsi funkci (bud anonymni, nebo validateAndSubmit), aby to bylo jasne.

Eda

Díky za fajnový screencast. Takovýto materiál v češtině/slovenštině se tak často nevidí. Jeden tip za druhým. Obzvlášť pro začátečníky v JS velmi přínosné :-)

Už se nemůžu dočkat pokračování.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.