Agenda
W tym artykule postaram się przedstawić jeden z najważniejszych aspektów pracy z Angularem, czyli w jaki sposób możemy łączyć się z naszym serwerem, które wystawia API w stylu REST, wspierając nas dodatkowym mechanizmem routingu. Nie mamy się czego obawiać - Angular wprowadza bardzo przyjazny sposób realizacji tych zadań, a co więcej, większość kwestii możemy sami dostosować do własnych potrzeb.
Jak już wspominaliśmy we wcześniej artykułach, Angular najlepiej współpracuje z serwerem, który wystawia statyczne html'e interpretowane przez niego samego oraz API REST, który w najprostszym i najlepszym przypadku ma postać JSONa, mającego bezpośrednią reprezentację w JS-ie. Tym sposobem tworzymy aplikację typu SPA (Single Page Application). Taki styl aplikacji internetowej skuteczenie odciąża serwer od generowania dynamicznych stron, a zarazem pozwala na bardzo elastyczne zmiany, nie obciążające deweloperów w dalszej konserwacji i utrzymaniu. Podsumowując mamy nowoczesną architekturą aplikacji internetowych, którą łatwo możemy skalować i tworzyć w odpowiedzi na szybko zmieniające się standardy technologii web'owych.
Pierwsze kroki z routingiem
Nie owijając w bawełnę przejdźmy do sedna sprawy. Co jest do przewidzenia, powinniśmy już mieć działający serwer, gotowy do rozpoczęcia realizowania zadania (jak go postawić, możemy zobaczyć w poprzednich artykułach na tym samym blogu). Nie jest dla mnie istotne, czy wolisz korzystać z technologii pokroju Javy EE czy też ze Springa, a w szczególności Spring Boota, który docelowo został stworzony do szybkiego dewelopmentu takich aplikacji. Dodatkowo zakładam, że macie ściągnięty moduł ng-route, dostępny na stronie Angular'a : angular-route.jsRouting w Angularze
Rozczajanie rozpoczniemy od podstawowej funkcji jaką pełni serwis routingu w Angularze. Trudno jest nie skorzystać z takich możliwości, jakie zapewnianiają:
- uniezależnienie ścieżek - urli jakie widzi użytkownik w swojej przeglądarce od ich reprezentacji na serwerze, który typowo ma wystawiać REST, którego użytkownik nie powinien "przez przypadek" widzieć;
- zachowanie stanu aplikacji Javascript, która w normalnych warunkach przeładowuje się w przypadku przejścia do nowego adresu URL;
- działanie standardowych przycisków prev/next w oknie przeglądarki internetowej;
- przekierowanie użytkownika, gdy ten wejdzie na nieistniejącą stronę;
- obsługę parametrów żądania;
Każda z powyżej wymienionych kwestii jest istotna dla tworzenia strony SPA, gdzie pomimo braku przeładowania strony, dajemy użytkownikowi poczucie używania zwykłych serwisów, dodatkowo oferując dynamizm.
Niestety, przez to, że JS jest nadal wrażliwy na przeładowanie zachodzące w przeglądarce (np. gdy użytkownik odświeży stronę za pomocą F5) to programista musi dbać o dobrą inicjalizację strony. Przydatne jest w tym to, że routing w Angularze oferuje przypisanie kontrolera do ścieżki w URLu i jest automatycznie uruchamiany w przypadku przejścia do powiązanego kontekstu. Warto, żeby ścieżka zawierała wszystkie parametry niezbędne do poprawnej inicjalizacji w kontrolerze, dzięki czemu użytkownik może przesłać swoim znajomym linka do naszej wspaniałej strony i jesteśmy pewni, że oni zobaczą identyczną treść.
Moduł ng-route
Moduł jest oferowany przez oddzielny plik Javascript. Dołączamy go do naszego głównego szablonu, typu index.html, który już wcześniej tworzyliśmy:<html ng-app="searchApplication"> <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>
W pliku app.js do modułu tworzymy moduł searchApp:
var searchApp = angular.module("searchApplication", ['ngRoute']);
Teraz musimy zdecydować się jak wyglądać będzie hierarchia naszego serwisu. W moim przykładzie zakładamy, że:
- na głównej stronie będzie wyświetlona wyszukiwarka zawierające możliwe wynikami poszukiwań użytkownika -
http://localhost:8080/#/;
- po wejściu w szczegóły wybranego artykułu przejdziemy pod link -
http://localhost:8080/#/article/{articleId}.
config
, której argumentem jest funkcję realizująca konfigurację modułu aplikacji. My konfigurujemy routing, pozostawiamy więc Angularowi wstrzyknięcie nam obiektu $routeProvider
.
searchApp.config(function($routeProvider) { $routeProvider .when('/', { templateUrl : '/searcher.html', controller : 'searchController' }) .when('/article/:articleId', { templateUrl : '/article.html', controller : 'articleController' }) .otherwise({ templateUrl : '/searcher.html', redirectTo: '/' }); });
W powyższym kodzie definiujemy - kiedy (when) jesteśmy w podanym kontekście ('/' lub '/article/:articleId') to ma zostać wyświetlony wybrany szablon oraz uruchomiony określony kontroler, w innym przypadku (otherwise) zawsze przekierowujemy pod kontekst '/' i wyświetlamy podany szablon. W przypadku przejścia pod ścieżkę article wymagamy identyfikatora artykułu, co pozwoli na poprawną inicjalizację stanu. Brak tego parametru powoduje wykonanie klauzuli 'otherwise'. Nic nie stoi na przeszkodzie, aby zbudować artykuły, które są opcjonalne - definiuje to znak zapytania '?' po nazwie parametru (np. '/article/:articleId?').
Żeby przetestować, że to co napisaliśmy rzeczywiście działa, musimy zdefiniować miejsce, gdzie nasz szablon ma się pojawić, gdy użytkownik przejdzie po dany adres. Tag <body> w pliku index.html będzie miał następującą zawartość:
<body ng-controller="mainController"> {{hello}} <div ng-view></div> </body>
Możemy tu zawuażyć charakterystyczną deklarację div-a zawierającego dyrektywę ng-view. Dyrektywa ta przypisana do jakiegoś tagu spowoduje wypełnienie jego zawartości szablonami określonym wcześniej w konfiguracji routingu. Teraz dodatkowo musimy stworzyć dwa pliki szablonów, które będą ładowane w przypadku odwiedzenia odpowiadającego adresu. searcher.html - jego zawartość i sposób działania była omawiana we wcześniejszych artykułach.
<body> <span ng-bind="yourSearch"></span> <div> <span>Search:</span> <input type="text" ng-model="userInput" ng-change="changeSearch()"/> </div> </body>
article.html - jak na razie poglądowy szablon:
<body> {{articleInformation}} </body>
Dla naszych szablonów potrzebujemy jeszcze kontrolerów. Do app.js załączamy:
var mainController = searchApp.controller('mainController', ['$scope', function($scope) { $scope.hello = "What are you looking for?"; }]); var searchController = searchApp.controller('searchController', ['$scope', '$http', function($scope, $http) { $scope.yourSearch; $scope.userInput; $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?"; } if ($scope.userInput.length > 2) { } } }]); var articleController = searchApp.controller('articleController', ['$scope', '$http', '$routeParams', function($scope, $http, $routeParams) { $scope.articleId = $routeParams.articleId; $scope.articleInformation = "You are looking at invisible article with id = " + $scope.articleId; }]);
Kontroler
articleController
pokazuje, jak prosto możemy wybrać parametry przekazywane przez url - odnosimy się do nich przez obiekt $routeParams
. Budujemy i uruchamiamy ponownie aplikację. Jak możemy zaobserwować odwiedzając wyspecifikowane przez nas szablony, na razie nie dzieje się nic wielkiego, natomiast możemy zobaczyć mechanizm działania routingu. Bardzo dobrze radzi sobie w przypadku, gdy użytkownik stara się wejść na nieistniejący kontekst aplikacji. Przycisk back w przeglądarce także działa poprawnie. Podając różne identyfikatory artykułów, angular automatycznie stan kontroler dzięki czemu wyświetla różne napisy. Czego nam tu teraz potrzeba, to komunikacji z serwerem!
Komunikacja z serwerem
Większość komunikacji z serwerem będziemy realizować przy użyciu serwisu udostępnianego nam przez Angular'a - $http. Istotne do zrozumienia jest to, że zapytanie do serwera odbywa się w sposób asynchroniczny. Najczęściej używane funkcje tego serwisu, czyli post i get, zwracają obiekt typu 'promise', co oznacza, że w przyszłości obiekt będzie zawierał odpowiedź na zapytanie. Na obiekcie zwracanym przez funkcje naszego serwisu możemy więc wywołać główne dwie metody: success lub error, tym samym przekazując im funkcję jako argument, która zostanie wywołana po otrzymaniu odpowiedzi z serwera (lub nie).
Protokół komunikacji
Przed przystąpieniem do realizacji API dobrze jest określić protokół komunikacji, czyli w jaki sposób i jakim stylem będzie odpowiadał nasz serwer. Dobrze jest zgeneralizować sposób odpowiedzi, czyli stworzyć uniwersalny sposób tworzenia odpowiedzi na zapytanie, dzięki czemu utrzymamy jednolitą konwencję nazewnictwa, porządek i tym samym mniej problemów dla innych aplikacji korzystających z naszego API. Przecież w przyszłości z naszego API mogą korzystać programiści dla których serwer będzie 'czarną skrzynką' i sposób odpowiedzi poznają na podstawie samych odpowiedzi!
Tym samym najprościej jest stworzyć klasę realizującą protokół komunikacyjny. W Javie możemy posłużyć się typem wyliczeniowym, który dodatkowo dostarczy funkcji użytkowych:
Można się zastanawiać, dlaczego zwracamy typowe kody standardu protokołu HTTP - ma to jednak zastosowanie utrzymaniu porządku samej aplikacji i informacji dlaczego ten kod występuje w naszym kontekście. Jeżeli uważamy to za niepotrzebną nadmiarowość, nie musimy tego implementować akurat w taki sposób, pomoże to jednak ludziom, którzy skorzystają z tego API, bez możliwości debugowania jego zawartości.
Warto wspomnieć, że dla każdej odpowiedzi wystawianej przez API powinniśmy zazwyczaj stwrozyć inną klasę, która zwróci pola istotne dla żądania, a nie bezpośrednie obiekty z bazy danych. Oddzieli to logikę biznesową jaką pełni API od logiki modelu danych obiektów trzymanych w bazie.
W zależności od poprawności zapytania wysłanego do API odpowiadamy typem BAD_REQUEST lub OK. Opis, zawarty w pierwszym z nich, pozwoli łatwo zdiagnozować problem po stronie klienta API.
Sposób wyświetlenia pól $scope.title i $scope.content w szablonie html pozostawiam czytelnikowi. Powyższy kod jest na tyle przejrzysty, że chyba nie muszę go opisywać. Bardzo podobnie wygląda obsługa innych metod HTTP - post, delete itd. Nie musimy definiować obsługi na odpowiedź z serwera, lecz są to bardzo rzadkie przypadki.
Inna postać odwoływania się do obiektu $http korzysta z wbudowanych funkcji:
Tym samym najprościej jest stworzyć klasę realizującą protokół komunikacyjny. W Javie możemy posłużyć się typem wyliczeniowym, który dodatkowo dostarczy funkcji użytkowych:
/** * Standard kodów odpowiedzi wysyłanych przez aplikację. * * @author Mateusz Kamiński */ public enum ResponseCodes { OK("200", "OK"), BAD_REQUEST("400", "Bad Request"); private String code; private String value; private String json; ResponseCodes(String code, String value) { this.code = code; this.value = value; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("{ \"code\" : ") .append(code) .append(", \"value\" : \"") .append(value) .append("\" }"); this.json = stringBuilder.toString(); } public String getJson() { return json; } public String getJson(String additionalJsonAttribute) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("{ \"code\" : ") .append(code) .append(", \"value\" : \"") .append(value) .append("\", \"attribute\" : \"") .append(additionalJsonAttribute) .append("\"}"); return stringBuilder.toString(); }; public String getJsonWithData(String additionalJsonData) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("{ \"code\" : ") .append(code) .append(", \"value\" : \"") .append(value) .append("\", \"data\" : ") .append(additionalJsonData) .append("}"); return stringBuilder.toString(); } }
Można się zastanawiać, dlaczego zwracamy typowe kody standardu protokołu HTTP - ma to jednak zastosowanie utrzymaniu porządku samej aplikacji i informacji dlaczego ten kod występuje w naszym kontekście. Jeżeli uważamy to za niepotrzebną nadmiarowość, nie musimy tego implementować akurat w taki sposób, pomoże to jednak ludziom, którzy skorzystają z tego API, bez możliwości debugowania jego zawartości.
REST API w Springu
W poniższym kodzie dołączony jest kontroler, który jest kontrolerem restowym - co zapewnia adnotacja
@RestController
. Implementując publiczne metody określamy, aby zwracały String, który będzie naszym JSON-em. Metodę oznaczamy adnotacją @RequestMapping
, przy czym deklarujemy przy jakiej metodzie HTTP będzie wywoływana (u nas GET), pod jakim kontekstem ('/list/document') i jakiego typu odpowiedź zwraca ('application/json').@RestController public class DocumentController { @RequestMapping(value = "/list/document", method = RequestMethod.GET, produces = "application/json") public String getDocument(@RequestParam(value = "documentId") String documentId) { Principal userPrincipal = httpServletRequest.getUserPrincipal(); if ( /** documentId nie jest liczbą *?) { return ResponseCodes.BAD_REQUEST.getJson("documentId parameter is not a digit!"); } else { Gson gson = new Gson(); Document document = new Document(); return ResponseCodes.OK.getJsonWithData(gson.toJson(document)); } } // klasa reprezentująca dokument na potrzeby tutoriala public static class DocumentBean { private Integer documentId; private String title; private String content; // settery i gettery } }Powyższy przykład wam nie zadziała, ponieważ sami musicie określić skąd pobieracie obiekt typu
Document
, a to wykracza poza zakres tego tutoriala (możecie się z takimi aspektami zapoznać w artykule o bibliotece jooq na naszym blogu). W obsłudze żądania używamy biblioteki Gson, stworzonej przez Google, która w prosty sposób serializuje klasy Javowe na napisy odpowiadające JSON-owe. Jeżeli budujemy nasz projekt przy użyciu mavena lub gradle'a przyda się dodać tam zależność do tej biblioteki.Warto wspomnieć, że dla każdej odpowiedzi wystawianej przez API powinniśmy zazwyczaj stwrozyć inną klasę, która zwróci pola istotne dla żądania, a nie bezpośrednie obiekty z bazy danych. Oddzieli to logikę biznesową jaką pełni API od logiki modelu danych obiektów trzymanych w bazie.
W zależności od poprawności zapytania wysłanego do API odpowiadamy typem BAD_REQUEST lub OK. Opis, zawarty w pierwszym z nich, pozwoli łatwo zdiagnozować problem po stronie klienta API.
Angular
Teraz przyszła kolej na klienta naszego API. W naszym kontrolerze article dodajemy dodatkową inicjalizację polegające na pobranie artykułu z serwera. Jeżeli realizacja zapytania trwałaby zbyt długo, warto wyświetlić użytkownikowi "ładowaczkę", aby wiedział, że coś się dzieje asynchronicznie. W przypadku błędu odpowiedni należy wyświetlić komunikat z błędem (i przeprosinami!).
$http({ method: 'GET', url: '/list/document?documentId' + $scope.documentId, headers: {'Content-Type': 'application/x-www-form-urlencoded'} }) .success(function(data, status, headers, config) { if (data.code === 200) { $scope.title = data.data.title; $scope.content = data.data.content; } else (data.code === 400) { $scope.message = "documentId was not a digit - please do not hack our REST API!"; } }) .error(function(data, status, headers, config) { $scope.message = "Sorry, there was an error while getting document!"; });
Sposób wyświetlenia pól $scope.title i $scope.content w szablonie html pozostawiam czytelnikowi. Powyższy kod jest na tyle przejrzysty, że chyba nie muszę go opisywać. Bardzo podobnie wygląda obsługa innych metod HTTP - post, delete itd. Nie musimy definiować obsługi na odpowiedź z serwera, lecz są to bardzo rzadkie przypadki.
Inna postać odwoływania się do obiektu $http korzysta z wbudowanych funkcji:
$http.get('/list/document?documentId' + $scope.documentId {headers: {'Content-Type': 'application/x-www-form-urlencoded'}})
I co dalej?
Ten artykuł przedstawił jedynie podstawy podstaw. Zbudowanie dobrego API nie jest proste, pewne regulacje zawiera 'standard 'HATEOAS' o którym warto poczytać - odsyłam na https://spring.io/understanding/HATEOAS. Budowanie architektury opartej o REST to realizacja wzorca mikroserwisów - http://en.wikipedia.org/wiki/Microservices. Angular staje się standardem, dzięki naturalnej współpracy z aplikacjami w takiej architekturze.
Angular fajnie sprawdza się jako typowy frontend. Dokładając api naprawdę można zdziałać dużo. Mi podoba się to że bez problemu możemy podpiąć płatność https://www.cashbill.pl/integracja/integracja-platnosci-online-shoper
OdpowiedzUsuń