Od czasu wydania biblioteki AngularJS przez firmę Google, programiści decydujący się na jej użycie zauważają, że struktura i sposób tworzenia aplikacji internetowych gwałtownie się zmienia. Sukces tej biblioteki (a w zasadzie frameworka) można przypisać funkcjom, których braki w zwykłym JavaScripcie powodowały nieefektywne pisanie ciężko-utrzymywalnych aplikacji. Ważną cechą Angulara jest to, że duża część logiki niezwiązanej z bezpośrednim przetwarzaniem danych utrzymywana jest po stronie klienta. Pozwala to na odciążenie serwera, którego głównym zadaniem jest wystawianie danych w stylu REST oraz szablonów html, interpretowanych przez bibliotekę już po stronie klienta. Oczywiście, nie zwalnia to programisty od walidacji danych przychodzących do serwera.
W poniższym artykule spróbujemy stworzyć prostą aplikację w opraciu o AngularJS. Na potrzeby tutoriala będę zakładał, że już masz uruchomiony serwer aplikacyjny z którym będzie przeprowadzana komunikacja. Jak uruchomić taki serwer korzystając z frameworku Spring Boot dowiesz się na naszym blogu.
Instalacja
Pierwszym krokiem prowadzącym do sukcesu jest pobranie samej biblioteki i wgranie jej do zasobów serwerowych tak, aby była dostępna dla użytkowników naszej nowej strony. W trakcie pisania tego bloga aktualna i stabilna wersja Angulara to 1.3.15, więc pobieramy go stąd: https://code.angularjs.org/1.3.15/. Możemy pobrać całego zipa ze wszystkimi modułami, albo tylko takie, które chcemy - na potrzeby tego tutoriala wymagany jest plik angular.js oraz angular-route.js.
Po wgraniu zasobów na serwer, sprawdzamy czy na pewno jest on dostępny z poziomu internetu. Zakładając, że wszystkie biblioteki zewnętrzne trzymamy na serwerze w folderze lib, a biblioteki dotyczące Angulara w folderze angularjs po wejściu w link http://localhost:port/lib/angularjs/angular.js
powinniśmy zobaczyć wnętrze cuda, z którego korzystamy. W razie problemów należy sprawdzić, czy dobrze skonfigurowaliśmy scieżkę do zasobów w naszym frameworku czy też narzędziu automatyzującym budowę oprogramowania (maven lub gradle).
Pierwsza strona
Mając solidne zaplecze możemy zacząć tworzyć szablon strony. Tworzymy więc plik index.html i umieszczamy go w naszym frameworku tak, aby ten szablon był serwowany jako 'root' strony. W tym momencie można zauważyć już pierwszą różnicę - serwer serwuje statyczne pliki html, javascript, a nie jak przypadku większości technologii (JSP, JSF, PHP), gdzie po stronie serwera generowany jest gotowy plik html, uwzględniający stan użytkownika wizytującego na naszej stronie. Zadaniem Angulara będzie dynamiczna zmiana zawartości DOM html'a. Poniżej przedstawiony kod wklejamy do pliku index.html (z dokładnością ze scieżkami do skryptów):
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>Hello world</title> <script src="/lib/angularjs/angular.js"></script> <script src="/lib/angularjs/angular-route.js"></script> </head> <body> "Hello world" </body> </html>
Po ponownym uruchomieniu serwer'a, a następnie po wejściu w link http://localhost:port/ powinniśmy zobaczyć napis Hello world. Dodatkowo upewniamy się, że poprawnie pobierane są biblioteki angularowe - konsola Javascript'owa przeglądarki powinna zalogować możliwe problemy.
Mając taką podstawę, możemy przejść do możliwości jakie daje nam AngularJS.
Angular w akcji
Gratuluję wytrwałości - najtrudniejsze kwestie konfiguracyjne są za nami. Teraz nic nie powinno przeszkodzić przy tworzeniu pierwszej aplikacji w Angularze.
Pisanie w Angularze nie przypomina już typowego procesu tworzenia kodu w samym Javascriptcie lub przy pomocy biblioteki jQuery. Za taką zmianę odpowiada wprowadzenie do biblioteki mechanizmu wstrzykiwania zależności. W skrócie, mechanizm ten jest odpowiedzialny za dostarczanie obiektów w różnych miejscach aplikacji, umożliwiając na zwiększenie modularności aplikacji. O stworzenie wybranych obiektów, usunięcie, kontrolę i ich kontekst użycia dba Angular - my musimy jednak rozumieć gdzie i jak możemy ich użyć.
Tak więc obiekty Angularowe można podzielić na:
To było proste, prawda? Po załączeniu bilioteki Angular'owej mamy dostęp do "faktorii" angular, dzieki której tworzymy nowe moduły. Jako pierwszy argument podajemy nazwę modułu. Taka konwencja jest utrzymana w całym Angularze, co pozwala na identyfikację obiektów i wstrzykiwanie zależności po nazwie. Co więcej Angular sprawdza, czy poprawnie podaliśmy nazwę obiektu w innym miejscu, i w przypadku błędu dowiadujemy się wszystkiego z konsoli JavaScript w przeglądarce:
Teraz musimy po stronie widoku zdefiniować, że chcemy skorzystać z modułu searchApp. Dodatkowo musimy dołączyć nasz nowy skrypt - u mnie znajduje się w katalogu scripts:
I tak mamy już omówioną kwestię modułu, teraz przejdziemy do mechaniki działania naszej aplikacji , za co odpowiadają kontrolery.
Tak więc obiekty Angularowe można podzielić na:
- moduły, zawierające i obejmujące kontekst całej aplikacji (tzw. root scope)
- kontrolery, specyfikujące zachowanie dla wybranej przez nas części aplikacji, każdy kontroler działa niezależnie od drugiego
- serwisy, dostarczające ogólne zachowanie, funkcje w każdym miejscu jednego modułu (Angular ma własne serwisy ogólnego przeznaczenia, z których możemy skorzystać)
Powyższy przegląd obiekt jest pobieżny, ale wystarczający do tego, aby zacząć tworzyć aplikację w Angularze.
Moduł
Moduł definiowany jest dla głównego szablonu html naszej aplikacji (co nie oznacza, że nie możemy mieć takich szablonów więcej). Aby nie dawać użytkownikowi wrażenia przeładowanej strony (czyli tym samym korzystać jedynie z technologii AJAX) warto zdecydować się na jeden moduł dla całej aplikacji - tutaj musimy podjąć decyzję architektoniczną, na ile złożona jest nasza aplikacja. Zawsze możemy też stworzyć hierarchię modułów, jeżeli chcemy utrzymać modularność aplikacji.
Nie czekając dłużej stwórzmy pierwszy moduł - do tego musimy dodać nowy plik w naszym projekcie np. app.js i w nim dodajemy:
/** * Główny moduł aplikacji. * * @type {module} */ var searchApp = angular.module("searchApplication", ['ngRoute']);
To było proste, prawda? Po załączeniu bilioteki Angular'owej mamy dostęp do "faktorii" angular, dzieki której tworzymy nowe moduły. Jako pierwszy argument podajemy nazwę modułu. Taka konwencja jest utrzymana w całym Angularze, co pozwala na identyfikację obiektów i wstrzykiwanie zależności po nazwie. Co więcej Angular sprawdza, czy poprawnie podaliśmy nazwę obiektu w innym miejscu, i w przypadku błędu dowiadujemy się wszystkiego z konsoli JavaScript w przeglądarce:
Uncaught Error: [$injector:modulerr] Failed to instantiate module searchApcplication due to:Drugim argumentem jest tablica napisów oznaczających jakie inne moduły mają być zainicjalizowane dla naszej aplikacji i zarazem używane w stworzonym module.Error: [$injector:nomod] Module 'searchApcplication' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.
Teraz musimy po stronie widoku zdefiniować, że chcemy skorzystać z modułu searchApp. Dodatkowo musimy dołączyć nasz nowy skrypt - u mnie znajduje się w katalogu scripts:
<html ng-app="searchApcplication"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>Search Application</title> <script src="/lib/angularjs/angular.js"></script> <script src="/lib/angularjs/angular-route.js"></script> <script src="/scripts/app.js"></script> </head>
I tak mamy już omówioną kwestię modułu, teraz przejdziemy do mechaniki działania naszej aplikacji , za co odpowiadają kontrolery.
Kontroler
Kontrolery definiują działania dla pewnego wydzielonego fragmentu aplikacji - dostarczają dane, reagują na zdarzenia, a także walidują poprawność działania użytkownika czy też wprowadzanych danych. Tym samym realizują wzorzec MVC, gdzie widokiem są szablony html, kontrolerami one same, a modelem serwer, z którym się komunikują aby pobierać dane. W tym momencie, dla uproszczenia, model będzie się znajdował w kontrolerze.
W app.js dodajemy następujący kod:
W app.js dodajemy następujący kod:
var mainController = searchApp.controller('mainController', ['$scope', function($scope) { $scope.yourSearch; $scope.userInput; $scope.hello = "What are you looking for?"; $scope.changeSearch = function() { if ($scope.userInput !== undefined && $scope.userInput.length !== 0) { $scope.yourSearch = "You are looking for " + $scope.userInput + "!"; } else { $scope.yourSearch = "You seek: nothing?"; } } }]);
Dla naszego modułu searchApp zdefiniowaliśmy tym samym kontroler o nazwie mainController. Dodatkowo wstrzyknęliśmy obiekt $scope tworzony tylko i wyłącznie dla danego kontrolera. Ostatnim elementem tablicy jest funkcja, która przyjmuje wstrzykiwany obiekt - $scope. Obiekt $scope pozwala na zdefiniowanie dostępnych zachowań oraz obiektów dostępnych w widoku. Naszym zamiarem jest wywołanie funkcji changeSearch, za każdym razem kiedy zachodzi interakcja z użytkownikiem. Pole yourSearch ma się aktualizować wraz z każdą zmianą i wyświetlać w przeglądarce użytkownika.
Teraz musimy podpiąć nasze kontrolery do szablonu html. Poniższy kod pokazuje jak to zrobić:
Jak widać, do tagu <body> dołączyliśmy definicję ng-controller="mainController". Kwestia, w którym miejscu chcemy skorzystać z kontrolera zależy od nas - tylko tam będzie dostępny. W przypadku dołączenia definicji kontrolera do tagu <div>, kontroler dostarczy danych i zachowań jedynie dla zawartości tego diva. Możemy też zagnieżdżać wiele kontrolerów, czyli dla tagu <div> możemy zdefiniować inny, drugi. Powoduje to jednak inny problem - trzeba precyzyjnie określić do którego z nich się odnosimy.
Do obiektu $scope po stronie szablonu html możemy odnieść się na dwa sposoby - {{nazwa_parametru}} lub w specjalnych atrybutach tagów jak ng-bind, ng-change, czy też ng-model.
W miejscu:
Tym samym omówiliśmy w jaki sposób działa angularowy kontroler. Teraz musimy zdefiniować skąd będzie pobierać nasze dane. Do tego potrzebny jest jeden z wielu dostępnych serwisów 'ad hoc' - $http.
Zmienne i metody prywatne możemy zawrzeć przed klauzulą return, tak aby nie były widoczne dla użytkownika serwisu.
Zauważmy, że serwis jest na tyle elastyczny, że może wymagać 'wstrzyknięcia' innych serwisów. W naszym przykładzie wykorzystujemy dwa, wbudowane w Angulara serwisy - $http i $location. Pierwszy z nich pełni rolę wymiany danych z serwerem, drugi odpowiedzialny jest za stan kontekstu przeglądarki w stosunku do adresu URL i udostępnia metody umożliwiające przekierowania (bez przeładowania strony!).
Podstawowe użycie tych dwóch serwisów możemy zobaczyć w powyższym przykładzie. Mnogość funkcji, które udostępniają przekracza zakres tego tutoriala - na razie warto zapoznać się z podstawowymi funkcjami.
Teraz musimy podpiąć nasze kontrolery do szablonu html. Poniższy kod pokazuje jak to zrobić:
<body ng-controller="mainController"> {{hello}} <span ng-bind="yourSearch"></span> <div> <span>Search:</span> <input ng-change="changeSearch()" ng-model="userInput" type="text" /> </div> </body>
Jak widać, do tagu <body> dołączyliśmy definicję ng-controller="mainController". Kwestia, w którym miejscu chcemy skorzystać z kontrolera zależy od nas - tylko tam będzie dostępny. W przypadku dołączenia definicji kontrolera do tagu <div>, kontroler dostarczy danych i zachowań jedynie dla zawartości tego diva. Możemy też zagnieżdżać wiele kontrolerów, czyli dla tagu <div> możemy zdefiniować inny, drugi. Powoduje to jednak inny problem - trzeba precyzyjnie określić do którego z nich się odnosimy.
Do obiektu $scope po stronie szablonu html możemy odnieść się na dwa sposoby - {{nazwa_parametru}} lub w specjalnych atrybutach tagów jak ng-bind, ng-change, czy też ng-model.
W miejscu:
<span ng-bind="yourSearch"></span>Mówimy Angularowi, że jeżeli nastąpi zmiana pola yourSearch w naszym kontrolerze, to ma zawartość span'a ma się odświeżyć. Znów w tagu input:
<input ng-change="changeSearch()" ng-model="userInput" type="text" />Definujemy zachowanie dla zmian w tym polu (ng-change), co skutukuje wywołaniem funkcji kontrolera - changeSearch. Dodatkowo oznaczamy, że pole userInput w naszym kontrolerze ma się aktualizować przy zmianach zachodzących w tym polu.
Tym samym omówiliśmy w jaki sposób działa angularowy kontroler. Teraz musimy zdefiniować skąd będzie pobierać nasze dane. Do tego potrzebny jest jeden z wielu dostępnych serwisów 'ad hoc' - $http.
Serwis
Serwisy pełnią rolę Singletonów dostarczających usług dla różnych obiektów tworzonych przez nas w Angularze. Istotne jest to, że powinny jedynie zmieniać stan globalny aplikacji, same nie mogą posiadać stanu. Typowym zastosowaniem serwisu może być proces logowania użytkownika na stronie, albo dostarczenie mechanizmu paginacji wśród wyników wyszukiwania.
transApp.factory('authorizationService', ['$http', '$rootScope', '$location', function($http, $rootScope, $location) { function updateAuthorizationData(data) { // metoda prywatna } // metody publiczne. udostępniane na świat return { /** * Sprawdza czy użytkownik uwierzytelniony. * * @returns {boolean} */ isUserAuthorized : function() { return $rootScope.userAuthorized; }, /** * Uwierzytelnia użytkownika. * * @param email * @param password * @returns {boolean} true, jeśli uwierzytelnienie się powiodło lub już uwierzytelniony, false w przeciwnym przypadku */ authorizeUser : function(email, password) { if ($rootScope.userAuthorized === true) { return true; } return $http.post('/tryLogin', "email=" + email + "&password=" + password, { headers : { 'Content-Type' : 'application/x-www-form-urlencoded'}}) .success(function(data, status, headers, config) { if(data.code==401) $rootScope.errors = data.value; updateAuthorizationData(data); }) .error(function(data, status, headers, config) { }); }, /** * Sprawdza, czy użytkownik zalogowany. Jeśli nie, przenosi go do strony logowania */ tryAuthorize : function() { if($rootScope.userAuthorized !== true) { $location.path('/login'); } }, /** * Wylogowuje użytkownika. * * @returns {boolean} true, jeśli wylogowanie się powiodło lub już wylogowany, false - wystąpił błąd */ logout : function() { if ($rootScope.userAuthorized === false) { return true; } $http.get('/logout') .success(function() { $rootScope.userAuthorized = false; return true; }) .error(function() { return false; }); } }]);Serwis tworzymy dla naszego modułu searchApp wywyołując metodę factory. Pierwszą charakterystyczną cechą dla serwisu jest to, że zwraca obiekt zawierający funkcje (lub parametry). Nie mamy tu obiektu $scope, natomiast możemy skorzystać z globalnie dostępnego $rootScope - trzeba jednak panować nad danymi, które tam przechowujemy. Angular większość swoich obiektów zaczyna od znaku '$, aby nie doszło do konfliktów w nazwach, które tworzymy.
Zmienne i metody prywatne możemy zawrzeć przed klauzulą return, tak aby nie były widoczne dla użytkownika serwisu.
Zauważmy, że serwis jest na tyle elastyczny, że może wymagać 'wstrzyknięcia' innych serwisów. W naszym przykładzie wykorzystujemy dwa, wbudowane w Angulara serwisy - $http i $location. Pierwszy z nich pełni rolę wymiany danych z serwerem, drugi odpowiedzialny jest za stan kontekstu przeglądarki w stosunku do adresu URL i udostępnia metody umożliwiające przekierowania (bez przeładowania strony!).
Podstawowe użycie tych dwóch serwisów możemy zobaczyć w powyższym przykładzie. Mnogość funkcji, które udostępniają przekracza zakres tego tutoriala - na razie warto zapoznać się z podstawowymi funkcjami.
Koniec?
Wręcz przeciwnie - to dopiero początek. Na temat Angulara i jego możliwości powstało wiele artykułów, tutoriali, a ten jest tylko jednym z nich. Po więcej informacji polecam odnieść się do strony: https://docs.angularjs.org/guide - znajdziecie tam wiele ficzerów, z którymi trzeba się zapoznać, żeby wykorzystać moc Angulara w 100%. W niedalekiej przyszłości, na naszym blogu mogą pojawić się następne artykuły związane ze światem Angulara.
Pozdrawiam i życzę owocnej nauki.
Super wprowadzenie, bardzo jasno napisane :-)
OdpowiedzUsuńDobrze widać jak JS zatacza koło. Ludzie już zapomnieli, że "onClick" w kodzie HTML jest zły i teraz śmiało stosują dziwadła typu ng-change="changeSearch()" tylko dlatego, że gigant Google to wprowadził. Macie wy godność człowieka? :-p
Każde rozwiązanie ma swoje plusy dodatnie i plusy ujemne ;) Moim zdaniem, lepiej jest stworzyć w html'u takie dyrektywy jakie mamy w angularze, niż mieszać JSa ze stylami za pomocą 'class' lub 'id' - często pojawiają się problemy "a czy ta klasa jest gdzieś używana w JS". Założeniem Angulara było być jak najbardziej przejrzystym i chyba tym do tego dąży :).
OdpowiedzUsuńRacja, nie ma doskonałego rozwiązania, ale są lepsze i gorsze.
OdpowiedzUsuńMoim zdaniem lepiej byłoby używać chociażby klas css z przedrostkiem "js-" lub bez, bo taki kod HTML:
- przechodziłby walidację W3C (angularowy całkiem się wysypuje w walidatorze),
- korzystałby z już istniejącego rozwiązania w nowy sposób.
Bardzo interesujące. Pozdrawiam serdecznie.
OdpowiedzUsuń