Kurs AngularJS #13 – Providers

Każda aplikacja którą tworzysz zbudowana jest z obiektów które ze sobą współgrają żeby zrobić rzeczy do których zostały przeznaczone. Obiekty te muszą zostać stworzone i połączone ze sobą w jeden działający projekt. Większością z nich automatycznie zajmuje się odpowiednia usługa – injector.

Tworzy ona dwa rodzaje obiektów: usługi i wyspecjalizowane obiekty (komponenty, dyrektywy itd.). Jeśli chodzi o usługi to są to obiekty których API zdefiniowane jest przez programistę. Reszta obiektów posiada API narzucone im przez AngularJS.

Injector musi wiedzieć jak tworzyć te obiekty. Możesz mu to powiedzieć poprzez stworzenie odpowiedniego “przepisu”. Jest ich pięć typów.

Najbardziej złożonym i wszechstronnym jest własnie provider. Pozostałe cztery: Value, Factory, Service i Constant są po prostu dodatkami. Z góry też przepraszam za to przewlekanie angielskich nazw ale szczerze mówiąc nie chce się tutaj bawić w słowotwórstwo zwłaszcza, że jeśli będziesz dalej poszerzał swoją wiedzę z innych źródeł będą one zapewne anglojęzyczne i możesz wtedy nie do końca wiedzieć o co chodzi lub mylić terminologię a nie chciałbym wprowadzać Cię w błąd.

W tym artykule przyjrzymy się więc tym pięciu typom informowanie injectora. Zaczniemy od najprostszego możliwego przypadku kiedy w różnych miejscach w naszej aplikacji zachodzi potrzeba udostępnienia jednego ciągu znaków i zrobimy to poprzez Value.

Krótka wzmianka o modułach

Jak wspomniałem injector musi wiedzieć jak tworzyć i łączyć ze sobą poszczególne elementy naszej aplikacji i potrzebuje do tego odpowiedniego przepisu. Każdy z nich identyfikuje odpowiedni obiekt i przepis na niego.

Każdy taki przepis należy również do odpowiedniego modułu. Moduł jest bowiem również pojemnikiem na przepisy. Ponieważ ręcznie śledzenie zależności w modułach do najfajniejszych nie należy, moduł sam trzyma o nich informacje.

Kiedy aplikacja w Angularze rozpoczyna swoje działanie, Angular tworzy nową instancję injectora, który zbiera wszystkie przepisy i tworzy z nich swój rejestr zdefiniowany dla nadrzędnego modułu, wszystkich pozostałych i ich zależności. Injector następnie kiedy zachodzi taka potrzeba sprawdza sobie odpowiednie przepisy kiedy potrzebuje stworzyć jakiś obiekt w naszej aplikacji.

Value

Tak jak wcześniej ustaliśmy chcemy mieć jakąś wartość która może być dostępna w różnych miejscach naszej aplikacji i być udostępniana jako zewnętrzne API. Zdefiniujemy ją w ten sposób:

Zauważ jak stworzyliśmy moduł myApp i określili jako pojemnik na przepis dla stworzenia clientId który w tym przypadku to zwykły ciąg znaków.

Kiedy będziemy chcieli wyświetlić go gdzieś w naszym projekcie zrobimy to tak:

W tym przykładzie zwyczajnie stworzyliśmy clientId i nadaliśmy mu jakąś wartość a następnie dostarczyli w miejsce które wymagało wyświetlenia tej wartości. Czas więc na coś bardziej skomplikowanego.

Factory

Przepis na Value jest bardzo prosty, nie ma jednak wielu ważnych funkcjonalności których często potrzebujemy kiedy tworzymy jakąś usługę. Spójrzmy więc na Factory – znacznie bardziej zaawansowane podejście. Daje nam ono dodatkowe możliwości:

  • Możliwość używania innych usług (posiadanie zależności)
  • Zainicjowanie usługi tak jak nam to odpowiada

Factory tworzy funkcje z zerową ilością parametrów (jeśli już jakieś są to są to zależności) i zwraca nam funkcje która jest instancją usługi stworzoną przez ten przepis.

Zauważ, że wszystkie factory w Angularze są singletonami (o tym wzorcu projektowym pisałem w kursie poświęconym JS). Oznacza to, że injector używa każdego z tych przepisów tylko raz żeby stworzyć dany obiekt. Następnie wynik jest cachowany i gotowy do użycia kiedy przyjdzie potrzeba stworzenia dokładnie takiej samej usługi.

Przepiszmy więc naszą usługę na Factory.

Biorąc jednak pod uwagę, że wartość zwracana przez factory jest nadal jedynie ciągiem znaków Value wydaje się tutaj odpowiedniejsze.

Powiedzmy jednak, że chcielibyśmy stworzyć usługę która oblicza jakiś prosty ciąg znaków używany do autoryzacji jakiegoś zewnętrznego API. Ten ciąg znaków będzie się nazywał apiToken i będzie obliczany właśnie na podstawie clientId oraz innej sekretnej wartości przechowywanej w pamięci podręcznej przeglądarki.

W powyższym kodzie widzimy, że usługa apiToken została zdefiniowana z pomocą Factory i jest zależna od clientId. Dobrą praktyką jest tutaj nazywanie funkcji która zwraca nam usługę w konwencji nazwaFactory. Tak jak w przypadku Value, Factory potrafi stworzyć usługę każdego typu: prostego typu, tablicy, obiektu czy funkcji a nawet jakiegoś niestandardowego typu.

Service

Programiści JavaScript często używają niestandardowych typów, żeby pisać zorientowany obiektowo kod. Powiedzmy, że nasz przykład będzie polegał na wysłaniu rakiety w kosmos poprzez usługę rocketLauncher która jest instancją niestandardowego typu.

Jesteśmy już gotowi do wysłania rakiety w kosmos ale zauważmy, że zależy to również od apiToken. Możemy więc dostarczyć tę zależność poprzez Factory.

To jednak jest przypadek kiedy Service jest rozwiązaniem znacznie bardziej przydatnym.

Przepis Service tworzy usługę tak jak dzieje się to w przypadku Value czy Factory ale również wywołuje konstruktor z parametrem new. Konstrukor oczywiście może przyjmować zero lub więcej parametrów które jeśli się pojawią to są z automatu jego zależnościami.

Kiedy tak naprawdę mamy już konstruktor dla naszego rocketLauncher możemy zastąpić Factory z pomocą Service w taki sposób:

Sam musisz przyznać, że to znacznie prostsze rozwiązanie.

Provider

Jak zostało to wspomniane już na wstępie Provider posiada przepis który jest, można by powiedzieć, podstawą dla wszystkich innych a wszystkie inne tylko dodatkami dla niego. Jest to najbardziej rozległy przepis z większością funkcjonalności ale dla wielu usług to najzwyczajniej przesada.

Przepis Providera to tak naprawdę niestandardowy typ który implementuje $get. Ta metoda jest funkcją dokładnie taką jakiej używaliśmy w Factory. Tak naprawdę jeśli definiujemy Factory pusty Provider z funkcją $get jest automatycznie tworzony pod przykrywką.

Providera powinieneś używać tylko wtedy kiedy chcesz udostępnić API do konfiguracji aplikacji która musi być zrobiona jeszcze przed rozpoczęciem działania wszystkiego. Przeważnie przydaje się jedynie dla używanych wielokrotnie usług których zachowanie nieznacznie się różni pomiędzy poszczególnymi aplikacjami.

Powiedzmy, że nasz rocketLauncher jest tak dobry, że korzysta z niego wiele aplikacji. Domyślnie usługa to wysyłała rakiety w kosmos bez żadnej osłony. Ale na niektórych planetach atmosfera jest na tyle gęsta, że wejście w nią może kończyć się poważnymi uszkodzeniami. Dobrzy by więc było wyposażyć się w dodatkową osłonę kiedy zachodzi taka potrzeba. Możemy więc uczynić tą opcje konfigurowalną.

Żeby włączyć dodatkowe osłony w naszej rakiecie musimy napisać funkcję konfigurującą w API modułu i wstrzyknąć w to RocketLauncherProvider.

Zauważ, że Provider odpowiedzialny za odpalanie rakiet jest w bloku konfiguracyjnym naszego modułu. Ta zależność została wstawiona przez provider injectora a nie zwykłego injectora, dzięki temu wszystkie instancje są tworzone i łączone z jego pomocą to właśnie providery.

Podczas ładowania się aplikacji, przed tym jak Angular przystąpi do tworzenia i rejestrowania wszystkich usług, konfiguruje i tworzy instancję wszystkich providerów. Podczas tej fazy poszczególne usługi nie są jeszcze dostępne gdyż najzwyczajniej nie zostały jeszcze stworzone.

Kiedy ta faza konfiguracyjna dobiega końca providery nie są już dłużej dostępne i rozpoczyna się proces tworzenia usług. Nazywane jest to fazą wykonawczą i wykonywane jest kod z bloków run().

Constant

Dowiedzieliśmy się właśnie jak przebiegają poszczególne fazy konfiguracyjne i wykonawcze w aplikacji Angulara oraz tego jak możemy dostarczyć i skonfigurować provider w fazie konfiguracyjnej naszej aplikacji. Od momentu kiedy ta faza wystartuje nie ma dostępu do żadnych wartości gdyż najzwyczajniej jeszcze nie istnieją, nawet do takiej zdefiniowanej przy pomocy Value.

Czasem jednak niektóre rzeczy są nam potrzebne zarówno w fazie konfiguracyjnej jak i później. Własnie po to istnieje przepis Constant.

Powiedzmy, że na każdej z naszych rakiet ma zostać wygrawerowana nazwa planety na którą są wysyłane. Nazwa ta musi więc być dostępna dla nas już w fazie konfiguracyjnej ale również i później bo wiele innych funkcji w aplikacji dalej z niej korzysta. Zdefiniujemy więc tę nazwę w następujący sposób:

Wtedy możemy swobodnie skonfigurować nasz rocketLauncher.

A ze względu na fakt iż Constant czyni tę wartość ogólnie dostępną możemy również używać jej w naszym kontrolerze czy widoku.

Wyspecjalizowane obiekty

Na samym początku pojawiła się wzmianki, że oprócz zwykłych usług są też wyspecjalizowane obiekty. Rozszerzają one framework jak pluginy i muszą implementować interfejsy narzucone im przez Angulara. Są nimi kontrolery, dyrektywy, filtry i animacje.

Przepis mówiący injectorowi jak obchodzić się z tymi obiektami używa w głębi Factory.

Zbudujemy prosty komponent poprzez dyrektywę zależną od planetName która wyświetlać będzie w naszym przypadku “Nazwa planety: Mars”.

Odkąd dyrektywy rejestrowane są poprzez Factory możemy używać tutaj takiej samej składni.

Używając przepisu Factory możesz również zdefiniować filtry i animacje ale kontrolery to już trochę inna bajka. Tworzysz je jako specjalne typy które definiują swoje zależności jako argumenty dla funkcji konstruującej. Następnie taki kontroler rejestrowany jest w module. Spójrzmy na przykładowy kontroler:

Jest on inicjowany poprzez swój konstruktor, za każdym razem kiedy aplikacja potrzebuje instancji DemoController (w naszym prostym przykładzie dzieje się to tylko raz). Więc w przeciwieństwie do usług, kontrolery nie są singletonami. Konstruktor wywoływany jest ze wszystkimi potrzebnymi zależnościami – w naszym przypadku jest to jedynie clientId.

Podsumowanie

Sumując to wszystko o czym powiedzieliśmy i zbierając to w jakieś konkrety:

  • Injector używa przepisów żeby tworzyć dwa rodzaje obiektów: usługi i wyspecjalizowane obiekty
  • Jest pięc typów przepisów które definiują jak tworzyć obiekty: Value, Factory, Service, Provider, Constant
  • Factory i Service są najczęściej używanymi przepisami. Jedyną różnicą pomiędzy nimi jest fakt, że Service działa lepiej dla obiektów o niestandardowym typie, podczas gdy Factory tworzy typy proste i funkcje.
  • Provider jest podstawowym przepisem a cała reszta jedynie dodatkami do niego
  • Provider jest najbardziej złożonym przepisem. Nie potrzebujesz go tak naprawdę chyba że tworzysz kod który ma być wykorzystywany wiele razy i potrzebuje globalnej konfiguracji
  • Wszystkie wyspecjalizowane obiekty poza kontrolerami są tworzone dzięki Factory