Raz na jakiś czas, czyli zwykle przed rozpoczęciem nowego projektu, stajemy przed wyborem stosu technologicznego dla planowanej aplikacji. Jeśli mamy za zadanie stworzyć bogatą, “jedno-stronicową” (single-page??) aplikację webową, zwykle też wybieramy framework JavaScript. Co daje nam taki framework? Ano rzecz jasna oszczędza nam czasu i pieniędzy na pisanie wielu niezbędnych funkcji od zera - dobry framework MVC JavaScript umożliwia nam na przykład łatwe ładowanie zależności czy też wstrzykiwanie kawałków HTML’a trzymanych w oddzielnych plikach do drzewa elementów DOM.

W dzisiejszym wpisie postanowiłem więc spróbować przybliżyć Wam możliwości trzech najpopularniejszych obecnie na rynku frameworków JavaScript. Mowa o tytułowych: AngularJS, BackboneJS oraz EmberJS. Postaram się także pokazać, który z nich jest najlepszym wyborem w zależności od naszych potrzeb - w końcu każdy ma trochę inne wymagania, a i specyfika danego projektu może determinować ostateczną decyzję.

Dlaczego akurat Angular, Backbone i Ember?

No właśnie, takie pytanie pewnie ciśnie Wam się na usta. Po części jest to mój subiektywny wybór - na potrzeby tego wpisu, nie mogę omawiać wszystkich dostępnych na rynku frameworków. Postanowiłem więc wybrać dwa najbardziej popularne i dojrzałe (AngularJS i BackboneJS) i dorzucić do tego “wschodzącą gwiazdę” czyli EmberJS.

Żeby nie było… do tej pory, tak konkretniej miałem do czynienia tylko z AngularJS. W jednym z projektów, który “dotykałem” używaliśmy też połączenia KnockoutJS i RequireJS ale to nie to samo - te dwa narzędzia, nawet w tandemie nie rozwiązują wszystkich tych problemów, które rozwiązują te, opisywane w dzisiejszym poście dlatego je pominę. No nic, przejdźmy do rzeczy - myślę, że najlepiej będzie jak zacznę od przybliżenia Wam, każdego z tych frameworków…

Wybieramy framework JavaScript - najważniejsze kryteria

…jednak zanim omówię pierwszy framework, warto jeszcze napisać parę słów na temat tego, jakie cechy powinien posiadać dobry framework MVC JavaScript. Ogólnie mówiąc, frameworki takie tworzone są po to, aby zapewnić programistom środowisko i narzędzia, dzięki którym mogą oni skupić się na tworzeniu samego rozwiązania, a nie na wymyślaniu jak dane zadanie wykonać. Możemy wyszczególnić kilka głównych cech/narzędzi, które powinien dostarczać dobry framework MVC JavaScript:

  • binding danych pomiędzy HTML’em, a obiektem modelu po stronie klienta (w tym aktualizacja danych w obie strony)
  • szablony widoków w rozumieniu “placeholder’ów”, w które wstrzykiwane są dane z modelu (dzielimy je na deklaratywne czyli za pomocą atrybutów HTML oraz na tekstowe czyli odpowiednio sformatowany tekst jako “placeholder”)
  • routing URL czyli “udawanie” przed przeglądarką, że aplikacja ma więcej niż jedną stronę ;)
  • składowanie danych rozumiane zwykle jako narzędzia do “strzelania” AJAX’em do serwera (w niektórych przypadkach realizowane może być nawet automatycznie przy każdej zmianie danych modelu)

Przy tym wszystkim należy zaznaczyć, że nie każdy framework zawiera wszystkie te elementy. Autorzy niektórych z nich pominęli część z tych cech, cedując je na inne dołączane biblioteki. Ma to swoje “plusy dodatnie” i “plusy ujemne”… Z jednej strony fajnie mieć wszystko w jednym miejscu, dostarczone w jednym pakiecie. Z drugiej strony brak danej funkcji we frameworku może być zbawienny, ponieważ w takim przypadku sami możemy zdecydować jakiej biblioteki użyć by pokryć nasze wymagania.

BackboneJS - największa dojrzałość

Framework JavaScript - Backbone

BackboneJS jest chyba najdojrzalszym z omawianych frameworków. Jego autorem jest Jeremy Ashkenas, który jest też autorem języka CoffeScript oraz biblioteki Underscore.js, a więc jak widać referencje ma całkiem niezłe :)

Sam framework, jako najbardziej dojrzały cieszy się też jedną z największych społeczności użytkowników. Nie ma więc raczej problemu ze znalezieniem informacji na temat tego jak coś zrobić. Jest też kogo zapytać w razie problemów co na pewno zmniejsza ryzyko jego zastosowania w swoim projekcie. Drugim z benefitów tej dojrzałości Backbone’a jest z pewnością jego stabilność oraz dostęp do dokumentacji technicznej i wielu przykładów (niestety wiele z nich jest już nieaktualnych ponieważ framework z czasem się zmieniał i rozwijał). Myślę, że wymienione zalety potwierdzają też liczne, profesjonalne aplikacje internetowe dostępne na rynku, a stworzone za pomocą BackboneJS. Pod tym linkiem znajdziecie listę przykładów takich aplikacji.

Kolejną z niezaprzeczalnych zalet BackboneJS jest jego rozmiar - całość mieści się w plikach o łącznym rozmiarze 7,28kB (wersja 1.1.2). Oczywiście jest osiągnięte kosztem dostępnych funkcjonalności frameworka - to może zmusić nas do używania Backbone wraz z dodatkowymi bibliotekami. Dla przykładu, aby w pełni wykorzystać możliwości opisywanego frameworka (biblioteki?), należy dołączyć do projektu także bibliotekę underscore.js oraz jquery.

Przejdźmy więc teraz do omówienia konkretnych możliwości BackboneJS. Zgodnie z tym co napisałem w poprzednim akapicie, każdy framework JavaScript powinien w jakiś sposób dostarczać cztery podstawowe funkcjonalności. Opiszę teraz, w jaki sposób spełnia je BackboneJS.

Binding danych w Backbone

I tutaj od razu mamy przykład frameworka, któryzostawia programistom wybór sposobu implementacji bindingu danych. Oto co na ten temat możemy przeczytać w oficjalnej dokumentacji frameworka:

“Two way data-binding” is avoided. While it certainly makes for a nifty demo, and works for the most basic CRUD, it doesn’t tend to be terribly useful in your real-world app. Sometimes you want to update on every keypress, sometimes on blur, sometimes when the panel is closed, and sometimes when the “save” button is clicked. In almost all cases, simply serializing the form to JSON is faster and easier. All that aside, if your heart is set, go for it.

Jak więc widać, filozofia twórców tego frameworka jest taka, że w większości przypadków lepiej jest zrobić to zadanie ręcznie. Jeśli jednak mimo wszystko tego potrzebujemy, podsuwają nam dwa rozwiązania (linki w cytacie), a konkretnie rivets.js oraz backbone.stickit.

Szablony widoków w Backbone

Jeśli mówimy o szablonach widoków, mamy na myśli przede wszystkim sposób, w jaki wstrzykujemy dane modelu do kodu HTML, tak by odpowiednio wyświetlały się na ekranie. Generalnie rozróżniamy dwa sposoby realizacji tego zadania: deklaratywne, za pomocą atrybutów znaczników HTML oraz tekstowe.

W przypadku Backbone, po pierwsze mamy wsparcie dla tekstowych szablonów, a po drugie jest to znów realizowane przez zewnętrzną bibliotekę (może to być na przykład underscore.js, mustache lub handlebars.js) - w końcu, tak jak pisałem założeniem autorów jest mały rozmiar frameworka (wielu uważa, że jest to raczej biblioteka - w sumie racja, jednak jeśli połączymy go z innymi bibliotekami, otrzymujemy framework pełną gębą…) i jest to osiągnięte właśnie za pomocą scedowania wielu zadań na zewnętrzne narzędzia.

Skoro jesteśmy przy szablonach tekstowych, to może jeszcze małe wyjaśnienie dla osób, które nie miały wcześniej do czynienia z opisywaną tematyką. Jak wspomniałem, jest to sposób na “spięcie” bindingu danych z widokiem HTML. Służy zarówno do wyświetlania samych danych jak i do podpinania zdarzeń itp. Szablony tekstowe polegają na podmienianiu odpowiednich, odpowiednio oznaczonych “placeholder’ów” tekstowych. Najczęściej taki “placeholder” definiujemy mniej więcej w taki sposób:

<div class="entry">
  <h1></h1>
  <div class="body">
    
  </div>
</div>

W powyższym przykładzie, takimi “placeholderami” są oraz (przykład ze strony projektu handlebarsJS).

Składowanie danych w Backbone - automatyczna synchronizacja

BackboneJS zakłada, że po stronie serwera mamy serwis REST’owy i automatycznie dokonuje zapisu zmian, jakie zachodzą w modelu. “Pod spodem” oczywiście wykonywany jest zwykły “strzał” AJAX’em za pomocą jQuery.

W przypadku takiego sposobu zapisu danychważna jest kolejność wykonywania operacji - w BackboneJS, najpierw wysyłane jest żądanie, a dopiero potem aktualizowane są dane po stronie klienta. Na pewno ułatwia to synchronizację danych jednak zmniejsza to “responsywność” aplikacji oraz nie sprzyja tworzeniu aplikacji mobilnych, które zakładają pracę w trybie offline - na pewno warto brać to pod uwagę wybierając framework JavaScript.

Routing URL w Backbone

Bardzo ważnym składnikiem każdego frameworka MVC JavaScript jest routing URL. Tak jak napisałem wyżej, w przypadku tworzenia “one-page app”, zawsze stajemy przed problemem: jak taka strona będzie indeksowana przez roboty wyszukiwarek internetowych takich jak Google czy Bing - przecież taka aplikacja wygląda dla nich jak pojedyncza strona!

I właśnie routing URL jest rozwiązaniem tego problemu poprzez symulowanie działania linków - kiedy klikamy link na stronie, mimo że wykonywane jest żądanie asynchroniczne i strona nie przeładowuje się, to i tak w pasku adresu pojawia się właściwy link. Tak samo jeśli wpiszemy link do przeglądarki lub klikniemy link na stronie, otworzy się nasza strona w odpowiednim stanie. Dzięki temu wyszukiwarki potrafią prawidłowo indeksować taką stronę. Oprócz tego, mamy dodatkowo zapewnioną obsługę przycisku wstecz - skoro adres się aktualizuje, to aktualizuje się również historia przeglądania, więc jeśli cofniemy się “back buttonem” to wrócimy na stronę w odpowiednim stanie.

W BackboneJS jest to tym razem rozwiązanie wbudowane we framework… i myślę, że tyle wystarczy ;) Pora przejść do omówienia kolejnego frameworka MVC JavaScript.

AngularJS - demon szybkości rozwoju

Framework JavaScript - Angular

Pora przejść do najszybciej rozwijającego się frameworka MVC JavaScript, a więc do AngularJS. Powstał on w 2009 roku w Google, a jego autorami są Misko Hevery oraz Adam Abrons. Obecnie Abrons nie pracuje już Google, a projekt razem z Hevery’m “ciągnięty” jest przez Igora Minara i Vojta Jina.

AngularJS, podobnie jak opisany wcześniej BackboneJS jest również dojrzałym, stabilnym projektem, a oprócz tego zaobserwować można znaczny wzrost jego popularności w ostatnich latach. Dlatego też można stwierdzić, żeposiada on podobne zalety jak Backbone, jeśli chodzi o społeczność i ilość informacji na jego temat, znajdujących się w internecie. Jak przystało na dojrzały framework, którego patronem jest Google, posiada on też całkiem dobrą i aktualną dokumentację - z osobistych doświadczeń mogę stwierdzić, że czasem nie jest ona do końca zrozumiała ale to się z czasem poprawia :) Poza tym, ilość przykładów dostępnych w sieci internet, jest na tyle duża, że często można poradzić sobie i bez niej.

Jeśli chodzi o rozmiar projektu to tutaj nie jest już tak różowo jak w przypadku BackboneJS, ponieważ całość źródeł ściągniętych na dysk zajmuje ok 18MB. Z drugiej strony, w przeciwieństwie do Backbone, AngularJS jest swego rodzaju kombajnem, który zawiera wbudowane wszystkie niezbędne składniki. Teoretycznie można by się nawet obejść bez jQuery, ponieważ Angular zawiera jego zmniejszoną wersję czyli jQLite. AngularJS ma nawet wbudowane zarządzanie zależnościami JavaScript, nie trzeba więc stosować takich bibliotek jak na przykład RequireJS. Jest też zaprojektowany w taki sposób aby umożliwić łatwe testowanie tworzonego kodu. Jak więc widać jest to framework “wszystkomający” ;)

To tyle tytułem wstępu, analogicznie jak w przypadku Backbone’a, przejdę teraz do opisu tego, w jaki sposób AngularJS implementuje podstawowe funkcjonalności każdego frameworka MVC JavaScript.

Binding danych w AngularJS

W przypadku AngularJS, w nasze ręce trafia w pełni wbudowany mechanizm bindowania danych. Jak można się domyślić, użycie tego mechanizmu jest dziecinnie proste - wystarczy przypisać do obiektu $scope (zgodnie z konwencją) właściwość lub funkcję. Od tego momentu staje się ona (ta właściwość lub funkcja) obiektem obserwowanym. Jeśli jest podpięta pod widok, jest on automatycznie powiadamiany o jej zmianie w kontrolerze (w AngularJS mamy typowy podział MVC po stronie klienta) i na odwrót - jeśli zmiana nastąpi w kontrolerze, widok natychmiast o tym wie (“two-way data binding”).

Szablony widoków w AngularJS

Tutaj również, AngularJS różni się od BackboneJS. Po pierwsze szablony widoków nie są realizowane za pomocą zewnętrznej biblioteki tylkosą wbudowane we framework. Po drugie, zastosowano tutaj połączenie szablonów tekstowych i deklaratywnych, tzn. używane są zarówno “placeholdery” znane z HandlebarsJS jak i anotacje za pomocą atrybutów HTML - wszystko w zależności od potrzeb. Zresztą najlepiej spojrzeć na przykład:

<div>
  <label>Name:</label>
  <input type="text" ng-model="yourName" placeholder="Enter a name here">
  <hr>
  <h1>Hello !</h1>
</div>

W powyższym kawałku kodu HTML, w linii 3 widzimy przykład podpięcia właściwości modelu yourName do textBox'a za pomocą atrybutu ng-model. Spowoduje to, że jeśli użytkownik wprowadzi jakiś tekst do tego pola, model zostanie automatycznie zaktualizowany.

Z kolei w linii 5 widzimy w jaki sposób podpięta jest ta sama właściwość modelu do wnętrza elementu h1. Tym razem użyto szablonu tekstowego, podobnie jak ma to miejsce w HandlebarsJS. W momencie aktualizacji modelu, w jego miejsce wstawiona zostanie odpowiednia wartość.

Za pomocą opisanych powyżej atrybutów, realizowane jest też podpinanie innych zdarzeń do elementów widoku. To w znaczący sposób odróżnia rozwiązania oparte tylko na szablonach tekstowych, od tych opartych na atrybutach. Które rozwiązanie jest lepsze? W tym opartym na atrybutach HTML, mogą wystąpić problemy wydajnościowe, ponieważ kosztem wygody programisty, konieczne jeststałe obserwowanie wszystkich elementów DOM podpiętych pod zdarzenia. Z drugiej strony, szablony tekstowe są mniej prawidłowe semantycznie ponieważ, aby podpiąć zdarzenie do elementu DOM konieczne jest dodanie “placeholder’a” w miejsce atrybutu - coś w tym stylu:

<img {{bind-attr src=logoUrl}} alt="Logo">

Myślę, że warto się zastanowić, które rozwiązanie jest dla nas bardziej odpowiednie podczas wyboru frameworka MVC JavaScript.

Składowanie danych w AngularJS

W przypadku AngularJS mamy dwie możliwości. Standardowo, framework dostarcza serwis $http, który pozwala na wywoływanie żądań asynchronicznych do serwera (autorzy zakładają przecież, że używając Angulara nie potrzebne nam jest jQuery). Poniżej przykład takiego wywołania:

$http({method: 'GET', url: '/someUrl'}).
  success(function(data, status, headers, config) {
    // this callback will be called asynchronously
    // when the response is available
  }).
  error(function(data, status, headers, config) {
    // called asynchronously if an error occurs
    // or server returns response with an error status.
  });

Drugim sposobem jest podejście “RESTowe”. Do tego celu służy serwis $resource, który nie znajduje się w “core’owej” części frameworka. Więcej na ten temat w dokumentacji API Angulara.

Routing URL w AngularJS

Czym jest routing napisałem już przy omawianiu BackboneJS. Co do samej implementacji, nie ma sensu wdawać się tutaj w szczegóły bo to temat na osobny wpis. Myślę, że wystarczy jak napiszę, że AngularJS również dostarcza odpowiedni do tego celu mechanizm, dostarczany przez moduł ngRoute. Więcej na ten temat w dokumentacji API Angulara.

EmberJS - wschodząca gwiazda

Framework JavaScript - Ember

No i nadszedł czas na najmłodszego przedstawiciela frameworków MVC JavaScript w moim małym zestawieniu, czyli do EmberJS. Jego autorami są Yehuda Katz oraz Tom Dale, a jego pierwsze wydanie nastąpiło w 2011 roku. Jednak pierwsze produkcyjne wydanie wersji 1.0 nastąpiło dopiero w sierpniu 2013 roku.

Jeśli chodzi o “community” tonie jest ono jeszcze tak liczne (z racji młodzieńczego wieku) jak w przypadku dwóch pozostałych, omawianych tutaj frameworków, jednak stale rośnie **(podobnie jak liczba aplikacji, które zostały zbudowane za jego pomocą). Więc i tutaj nie powinno być problemów ze znalezieniem rozwiązania problemów, które możemy napotkać. Należy jednak z tym uważać, ponieważ przez te dwa lata przed wydaniem wersji 1.0 frameworka, ukazywały się wcześniejsze **niestabilne jego wersje i możemy się na różnych forach i grupach natknąć na przykłady, które nie są już aktualne. Dlatego lepiej zdać się na dokumentację API dostępną na stronie projektu - jest obszerna i wydaje się pokrywać wszystkie aspekty frameworka (poprawcie mnie jeśli się mylę…).

Porównując EmberJS do Angulara i Backbone możemy łatwo stwierdzić, że jest on bardziej podobny do tego pierwszego - pod tym względem, że stara się dostarczyć wszystkie funkcjonalności **dobrego frameworka MVC JavaScript (w odróżnieniu od Backbone, które jak już wspomniałem **jest raczej biblioteką niż pełnoprawnym frameworkiem). Mimo to, aby w pełni wykorzystać jego możliwości, oprócz samego Embera należy do projektu dołączyć jeszcze biblioteki jQuery oraz handlebarsJS. Zresztą o tym w kolejnych podpunktach :)

Binding danych w EmberJS

W EmberJS, binding danych jest “dwustronny” podobnie jak w przypadku AngularJS. Generalnie, w Ember tworzy się obiekty Ember.Object.extend(). Wszystkie właściwości tak utworzonego obiektu są automatycznie obserwowane, więc jeśli teraz podpięlibyśmy taką właściwość do widoku, jej zmiana w jednym miejscu jest od razu aktualizowana w innym miejscu itd.

Szablony widoków w EmberJS

Tutaj, mamy do czynienia z sytuacją podobną do tej z BackboneJS, ponieważ EmberJS do obsługi szablonów widoków wymaga biblioteki handlebarsJS. Jest to więc realizowane w opisany już wcześniej sposób i jest to rozwiązanie oparte na“placeholderach” tekstowych.

Składowanie danych w EmberJS

Jeśli chodzi o sposób komunikacji z serwerem, to EmberJS dostarcza własne rozwiązanie (RESTAdapter) wymagające do działania serwisu “RESTowego”. Spójrzmy na przykład:

App.PhotoRoute = Ember.Route.extend({
  model: function(params) {
    return this.store.find('photo', params.photo_id);
  }
});

W linii 3 widzimy wywołanie funkcji find w poszukiwaniu zdjęcia o podanym identyfikatorze. Aby to zadziałało, wymagane jest teraz aby serwer zwrócił odpowiednie informacje w formacje JSON pod adresem /photos/123. Oprócz find, dostępne są jeszcze funkcje findall, update, create oraz delete.

Routing URL w EmberJS

Routing jest bardzo mocną stroną frameworka EmberJS. W trakcie prac, jeszcze przed wydaniem wersji 1.0, autorzy mocno wzięli sobie do serca opinie społeczności i mocno przeprojektowali ten aspekt frameworka. W efekcie dostarczono nam bardzo rozbudowany i w pełni konfigurowalny (w przeciwieństwie do większości innych frameworków, w których jego funkcjonalność jest tylko podstawowa) moduł routingu. Zresztą wystarczy spojrzeć jak rozbudowana jest dokumentacja tego modułu.

No więc jak wybrać framework JavaScript?

To tyle jeśli chodzi o opis cech wybranych przeze mnie frameworków MVC JavaScript. Myślę jednak, że kiedy wybieramy framework JavaScript na nasz wybór może wpłynąć znacznie więcej aspektów niż same porównanie dostępnych opcji. Co jeszcze powinniśmy brać pod uwagę?

  • Doświadczenie zespołu - to na pewno istotna sprawa, w końcu jeśli wszyscy programiści w teamie mają doświadczenie w jednym konkretnym rozwiązaniu, to może warto wykorzystać ich wiedzę i wybrać framework pod nich? Na pewno projekt pójdzie wtedy sprawniej i może mniej kosztować bo nie będziemy musieli uczyć się nowego narzędzia…
  • Cenimy bardziej wolność wyboru czy rozwiązania “wszystko w jednym”? - tak jak opisałem gdzieś wyżej, BackboneJS jest bardziej biblioteką i dopiero po połączeniu go z innymi bibliotekami otrzymujemy pełnoprawny framework. Na pewno na plus jest to, że te biblioteki możemy dobrać dość dowolnie. Z kolei Angular i Ember to “wypasione” kombajny, w których mamy w zasadzie wszystko co potrzeba i co ważne, utrzymane w jednej konwencji…
  • Jakie są nasze rzeczywiste potrzeby? - to się łączy z poprzednim punktem. Jeśli nasza aplikacja nie potrzebuje wszystkich bajerów dostępnych w pełnoprawnych frameworkach to może lepiej wybrać bibliotekę z niezbędnymi dodatkami? A może projektujemy potężną aplikację i biblioteki nie spełnią wszystkich naszych wymagań lub będzie trzeba użyć ich sporo?
  • Wydajność - to chyba oczywisty aspekt i nie muszę go wyjaśniać :)
  • Testowalność - j.w.
  • Coś jeszcze? :)

Na pewno jest tego więcej… Jeśli przychodzi Ci coś do głowy co pominąłem - pisz w komentarzach! Dobra, dość już tego pisania… Powstał chyba jeden z moich najdłuższych wpisów na tym blogu, a jego napisanie zajęło mi prawie tydzień… :D