W aplikacji AngularJS dane przetwarzane są w kontrolerach. Jeśli coś wpiszemy np. w formularzu w jednym widoku (na jednej podstronie), dane dostępne są w kontrolerze przypisanym do niego.
Czasem jednak zachodzi potrzeba, aby dane były dostępne na wielu zakładach (czyli w wielu kontrolerach i widokach). Jak widzimy w naszej aplikacji, jeśli na głównej zakładce w formularzu wpiszemy dane, a następnie przełączymy się na inną i powrócimy, to zobaczymy, że dane z formularza zostały utracone (były w tym jednym widoku i po przełączeniu on się przeładował).
Jak sobie z tym poradzić? Moglibyśmy za każdym razem zapisywać dane do bazy danych, a po powrocie na daną zakładkę przywracać je. Tylko rodzi to kilka problemów. Kiedy zapisywać dane (po kliknięciu na inną zakładkę? po każdorazowym naciśnięciu klawisza)? Po co obciążać serwer? Dane każdorazowo musiałyby być wczytywane, co spowalniałoby działania aplikacji. Nie jest to dobre rozwiązanie w tym przypadku. Dane powinny być przechowywane po stronie klienta.
Serwisy w AngularJS
AngularJS udostępnia nam gotowy mechanizm, aby rozwiązać ten problem: serwisy. Jest to dodatkowa warstwa logiki aplikacji. Na początku tworzony jest obiekt, w którym będą przechowywane i przetwarzane dane (dostępne w całej aplikacji), a w kontrolerach i widokach będziemy się do niego tylko odwoływać.
Są trzy rodzaje serwisów:
- Factory – zwracamy obiekt z polami (zmienne lub funkcje), użycie
return
, - Service – przypisanie zmiennych lub funkcji do pól obiektu za pomocą słowa kluczowego
this
, - Provider – serwis, który można skonfigurować w sekcji
config
, zwracanie obiektu poprzez funkcję$get
.
W tutorialu, do którego wcześniej linkowałem, są opisane wszystkie trzy serwisy: Factory vs Service vs Provider. Również w książce AngularJS. Pierwsze kroki, w rozdziale Dependency Injection — wstrzykiwanie zależności: DI w praktyce (str. 37) są opisane wszystkie rodzaje serwisów.
Który wybrać? Factory jest najprostsze. Jednak konieczność używania return jest trochę niewygodne (chociaż kto co lubi). Według mnie lepiej użyć Service, gdyż dostęp do pól obiektu poprzez słówko this
jest wygodniejszy (tylko przypisujemy tam wartości i jest już dostępne, nie musimy nic zwracać). Jeśli mamy potrzebę konfiguracji serwisu przed załadowaniem aplikacji, wybierzmy Provider (w prostych aplikacjach zazwyczaj nie ma takiej potrzeby). Jednak jego użycie jest trochę bardziej skomplikowane.
Dlatego na nasze potrzeby wybierzmy serwis typu Service.
Serwis typu Service
Ogólna składnia wygląda tak:
1 2 3 |
nazwaAplikacji.service('nazwaSerwisu', function() { this.nazwaPolaSerwisu = 'wartośćPola'; }); |
Polami mogą być zmienne (również, a nawet przede wszystkim obiekty) oraz funkcje. Należy pamiętać, że jeśli polem będzie zmienna typu prymitywnego (np. napis, liczba), to będziemy mogli z niej korzystać, jednak bindowanie w AngularJS nie będzie działało prawidłowo. Dlatego polami serwisu muszą być obiekty (A dopiero w nim prymitywne zmienne), żeby można było zbindować referencję.
W naszym przykładowym serwisie będziemy mieli obiekt model
z jednym polem myValue
(pól może być więcej) oraz funkcję myText
.
1 2 3 4 5 6 7 8 9 10 11 |
app.service('MyService', function() { this.model = { 'myValue': '' }; this.myText = function () { return "Hello " + this.model.myValue + "!"; }; }); |
Utworzymy dwa kontrolery, żeby sprawdzić czy wymiana działa oraz dwa pola input
, aby sprawdzić czy prawidłowo działa bindowanie:
1 2 3 4 5 6 7 8 9 |
app.controller('myCtrl1', function($scope, MyService) { $scope.model = MyService.model; $scope.myText = MyService.myText; }); app.controller('myCtrl2', function($scope, MyService) { $scope.model = MyService.model; $scope.myText = MyService.myText; }); |
Jak widzimy, aby skorzystać z naszego serwisu, wystarczy go wstrzyknąć (umieścić nazwę serwisu w funkcji kontrolera). Jest to tzw. DI (Dependency Injection czyli wstrzykiwanie zależności). Chcemy z danych z serwisu skorzystać w widoku, więc musimy umieścić je w $scope
poprzez kontroler.
1 2 3 4 5 6 7 8 9 10 11 |
<div ng-controller="myCtrl1"> <h3>Controller 1</h3> <input type="text" ng-model="model.myValue"> {{myText()}} </div> <div ng-controller="myCtrl2"> <h3>Controller 2</h3> <input type="text" ng-model="model.myValue"> {{myText()}} </div> |
Podczas wpisywania danych w pola tekstowe, informacje powinny być przenoszone pomiędzy kontrolerami (czyli przepisywane do drugiego pola) oraz funkcja powinna być obliczana na bieżąco (obok dopisywanie słowa Hello
).
Oczywiście jest to tylko przykład. Oba kontrolery zostały umieszczone na jednej podstronie (więc aby uzyskać ten sam efekt, można by trzymać wszystko w jednym kontrolerze). Jednak ta wiedza przyda nam się na przyszłość. Nic nie stoi na przeszkodzie, aby teraz kontrolery rozdzielić na dwie podstrony i wspóldzielić pomiędzy nimi dane poprzez serwis.
Serwis typu Factory
Pokażę jeszcze rozwiązanie tego samego zadania z wykorzystaniem serwisu typu Factory. Według mnie jest mniej wygodne, ale dosyć często używane, więc warto z nim się zaznajomić.
Kontrolery i widoki zostają identyczne. Zmienia się tylko definicja serwisu.
Ogólna składnia wygląda tak (tworzymy pole i potem je zwracamy):
1 2 3 4 5 6 |
app.factory('nazwaSerwisu', function() { var lokalnaNazwaPolaSerwisu = 'wartośćPola'; return { nazwaPolaSerwisu : lokalnaNazwaPolaSerwisu } }); |
Lub szybciej tak (od razu zwracamy pole):
1 2 3 4 5 |
app.factory('nazwaSerwisu', function() { return { nazwaPolaSerwisu : 'wartośćPola' } }); |
W naszym serwisie będzie to wyglądało w ten sposób:
1 2 3 4 5 6 7 8 9 10 |
app.factory('MyService', function() { return { model : { 'myValue': '' }, myText : function () { return "Hello " + this.model.myValue + "!"; } } }); |
I działający przykład:
Serwis typu Provider
Została nam jeszcze najtrudniejsza wersja serwisu czyli Provider. Ogólna jego składnia wygląda tak:
1 2 3 4 5 6 7 |
app.provider('nazwaSerwisu', function() { this.$get = function () { return { nazwaPolaSerwisu : 'wartośćPola' } } }); |
Widzimy, że podobnie zwraca się pola jak w Factory. Jednak tu musimy dodatkowo skorzystać z funkcji $get
. Nie jest to zbyt wygodne, ale jeśli chcemy mieć serwis, który będziemy mogli skonfigurować przed uruchomieniem aplikacji, będziemy musieli skorzystać z tej wersji (Provider).
Czyli w naszym przykładzie będzie to wyglądać tak (sama definicja serwisu Provider, bez jego konfiguracji):
1 2 3 4 5 6 7 8 9 10 11 12 |
app.provider('MyService', function() { this.$get = function () { return { model : { 'myValue': '' }, myText : function () { return "Hello " + this.model.myValue + "!"; } } } }); |
I przykład działania:
Pingback: AgendaEditor: wymiana danych pomiędzy zakładkami poprzez serwisy | Marcin Kowalczyk – Blog IT
Właśnie takich informacji szukałem. Raczkuje dopiero z Angularem. Dzięki wielkie.