Wybierz region
pl
  • PL
  • EN
Wydrukuj

Czy aplikacje też potrzebują nawigacji?

Wyruszając w podróż w nowe, nieznane rejony najczęściej zabieramy ze sobą kieszonkowego doradcę i przewodnika jakim jest mapa. W dzisiejszych czasach zdecydowanie przeważa ta w formie aplikacji, która nie tylko pozwala nam zorientować się w okolicy, ale i również ponawiguje nas i pomoże dotrzeć do upragnionego celu. Czemu więc nie zapewnić naszym aplikacjom takiego komfortu?

Pomimo że dużą popularność skradły aplikacje typu single-page, gdzie wszystkie informacje prezentowane są na jednej długiej stronie, a linki w menu odsyłają nas niczym windą do odpowiedniej kotwicy, wciąż jednak istnieje potrzeba budowania nieco bardziej skomplikowanych stron. Przykładem mogą być strony bankowe, gdzie oprócz informacji o ofertach banku, mamy również dostęp do indywidualnej bankowości internetowej, która pozwala nam wykonać przelewy, przejrzeć historię transakcji, zmienić ustawienia personalne czy nawet wnioskować o kredyty. Wyobrażasz sobie mieć to wszystko na jednej stronie i latać po niej w górę i w dół…?

W przypadku aplikacji angularowych pierwsze zetknięcie z nawigacją może wprowadzić dozę konsternacji, ale spokojnie, nie taki diabeł straszny. Angular dostarcza nam wbudowany moduł Router, który posiada wiele przydatnych narzędzi, jak na przykład klasę Routes niezbędną do tworzenia ścieżek, po których później użytkownik będzie się poruszał, czy dyrektywę routerLink do wskazania trasy linkom.

Naszą podróż musimy koniecznie zacząć właśnie od stworzenia swoistej mapy aplikacji, czyli zaprojektowania ścieżek, byśmy mogli następnie przenosić nimi użytkownika po naszej aplikacji. Generując aplikację angularową z wykorzystaniem Angular CLI warto dodać flagę --routing=true, dzięki czemu dostaniemy od razu gotowy plik AppRoutingModule, który będzie podstawą naszej mapy. W jego skład wchodzi między innymi obiekt routes klasy Routes pochodzącej z modułu Router, który jest tablicą ścieżek prowadzących do różnych komponentów naszej aplikacji. Każda zadeklarowana w nim ścieżka musi składać się z dwóch podstawowych parametrów, path oraz component, czyli kolejno nazwa trasy oraz cel podróży. Określana tu nazwa jest tym, co użytkownik będzie widział w adresie url strony, a także odwołaniem, którym będziemy posługiwać się we wszelkich miejscach, gdzie będą znajdowały się odnośniki przekierowujące, czyli np. menu, czy przyciski i linki odsyłające nas na tę trasę.

Jeśli już mowa o menu i odnośnikach to moduł Router zapewnia nam również dyrektywę routerLink, którą wykorzystujemy do przypięcia trasy do linków tworzonych w szablonach komponentów. Nie ma w tym większej filozofii, do linku musimy dodać tę dyrektywę, a jako jej wartość wpisujemy trasę, do której chcemy się odwołać i voilà! W taki oto prosty sposób możemy utworzyć menu, które posłuży użytkownikowi do przemieszczania się po naszej aplikacji.

Musimy jednak być świadomi gdzie tak naprawdę jesteśmy przenoszeni po kliknięciu w link. Instynktownie może się wydawać, że przenosimy się na zupełnie nowy obszar aplikacji, podczas gdy tak naprawdę pozostajemy wciąż w tym samym komponencie. Moduł Router dostarcza powiem kolejne praktycznie narzędzie jakim jest router-outlet. Użycie tego selektora w szablonie html powoduje, że gdy klikniemy w link przenoszący nas na przykład do komponentu ustawień w rzeczywistości ten komponent zastąpi router-outlet. Daje to wspaniałą możliwość zbudowania szablonu aplikacji w postaci górnej belki z menu, footera, bocznych dodatkowych paneli i tym podobnych stałych elementów strony, które zadeklarujemy raz w jednym miejscu i zmieniając ścieżki de facto podmieniamy jedynie fragment strony.

Warto tu również zaznaczyć, że router-outlet może mieć przypisaną nazwę. Jako że ustaliliśmy już, że przechodząc na wybraną ścieżkę niekoniecznie zmieniamy całą zawartość strony, możemy zechcieć mieć na jednym obszarze więcej komponentów wołanych przez routing. Musimy jednak wówczas wskazać aplikacji, który komponent ma być przypisany do którego outletu.

I tak na przykład możemy mieć komponent Main, który posiada dwójkę dzieci, Characters oraz Weapons i oba te komponenty mają się w nim wyświetlić obok siebie. Dzięki parametrowi ścieżki children możemy zadeklarować dla naszej trasy tablicę z jego dziećmi i tam dla każdej pozycji dodać kolejny parametr, outlet, który będzie się pokrywał z nazwami router-outlet­ów zadeklarowanymi w szablonie html.

Odważni mogą zechcieć w tych komponentach dodać kolejne outlety, a w nich kolejne i mimo że nic nie stoi na przeszkodzie, żeby zbudować taką incepcję, to jednak należy pamiętać, że im bardziej komplikujemy kod, tym oczywiście trudniej się w nim później odnaleźć i nim zarządzać.

Niekiedy bywa również tak, że chcielibyśmy jakieś proste informacje przekazać komponentowi w postaci parametru w adresie url, to może być na przykład numer id czy jakaś unikatowa nazwa obiektu, którego szczegóły chcemy wyświetlić, co pozwoli użytkownikowi na skopiowanie linku bezpośrednio do interesującego go np. przepisu. Twórcy modułu również to przewidzieli i umożliwili przekazywanie danych w nazwie ścieżki – wystarczy dodać / oraz :, a tuż po nich identyfikator parametru, np. character/:id.

Ta nazwa parametru jest nam potrzebna do wyciągnięcia go ze ścieżki w miejscu docelowym. By to zrobić musimy do docelowego komponentu wstrzyknąć w konstruktorze klasę ActivatedRoute (czyli z angielskiego aktywna droga, a więc ścieżka, na której się aktualnie znajdujemy) i a następnie odwołać się do jej atrybutu params. Params jest typu Observable, co zobowiązuje nas do zasubskrybowania się na zwracane przez niego wartości (oczywiście należy pamiętać o zakończeniu subskrypcji, gdy już nie będzie nam potrzebna – więcej o tym możesz poczytać we wpisie  Asynchroniczność w Angularze i jak ją ugryźć). Observable ten zwraca nam obiekt typu Params, który de facto jest tablicą parametrów ścieżki. I tu właśnie wracamy do identyfikatora naszego parametru, ponieważ musimy się odwołać po jego nazwie, by dostać się do schowanej w nim wartości, czyli w naszym przykładzie będzie to params[‘id’].

Kiedy użytkownik może trafić na zakaz wjazdu?

Może się również zdarzyć, że z różnych przyczyn nie chcemy dopuścić użytkownika do jakiegoś obszaru aplikacji. Powody takiej blokady mogą być różne, począwszy od niezalogowanego użytkownika, poprzez brak stosownych uprawnień, a jeśli mamy taki kaprys, to możemy uznać, że nie wejdzie do wybranego komponentu, jeśli jego login zaczyna się od litery S. W takiej sytuacji Angular również pędzi nam z pomocą i dostarcza kolejny parametr, który możemy dodać do budowanych tras, czyli canActivate. Może i jego użycie wymaga od nas nieco dodatkowej pracy, ale śmiem twierdzić, że efekty są tego warte.

Zanim będziemy mogli wykorzystać możliwości tego parametru, musimy utworzyć wewnątrz aplikacji klasę implementującą pochodzącą z modułu Router klasę CanActivate. Dostarcza nam ona metodę o takiej samej nazwie, czyli canActivate() i wewnątrz niej tworzymy warunki, jakie użytkownik musi spełnić, by móc przejść dalej – czyli innymi słowy właśnie tutaj możemy zweryfikować, czy jest zalogowany lub posiada odpowiednią role, itp., itd.

Następnie tego swoistego strażnika możemy przypiąć do parametru canActivate w wybranej ścieżce. Warto pamiętać, że parametr ten przyjmuje wartości w postaci tablicy, a co za tym idzie możemy do danej ścieżki przypisać wielu strażników.

Dzięki tym zabiegom za każdym razem, gdy użytkownik będzie próbował dostać się na wybraną ścieżkę, czy to przez kliknięcie linku, czy wpisanie adresu url w przeglądarce, zanim zostanie przekierowany uruchomi się w pierwszej kolejności nasz strażnik, który zweryfikuje, czy dany użytkownik może faktycznie uzyskać dostęp do wybranych zasobów. Jeśli spełni wszystkie zadeklarowane przez nas warunki, jest bezpiecznie przepuszczany i może kontynuować swoją podróż. Jeśli zaś okaże się, że nie był w stanie spełnić naszych wymogów strażnik zablokuje mu drogę niczym Gandalf oznajmiający Balrogowi „you shall not pass”.

W tym drugim przypadku możemy po prostu zwrócić false i nic się nie wydarzy, gdy użytkownik będzie chciał się dostać na zablokowaną ścieżkę albo możemy wykorzystać kolejne narzędzie, tym razem z klasy Router wstrzykniętej w konstruktorze strażnika i użyć jego metody navigate() do przekierowania użytkownika na inną trasę – może to być na przykład strona informująca, dlaczego jego podróż została przerwana lub po prostu powrót do strony logowania.

 

Czemu komponenty bywają leniwe?

Chociaż mogłoby się wydawać, że lenistwo to domena jedynie organizmów żywych, możemy się natknąć na to określenie również w kontekście modułów aplikacji. Na nasze szczęście jest to tylko umowne określenie i komponenty nie powiedzą nam nagle, że właściwie to nie mają dzisiaj ochoty pracować i zostają w łóżku. Lazy-loading, bo o tym właśnie będziemy mówić, jest to praktyczny wzorzec projektowy, który służy do odsunięcia momentu inicjalizacji komponentu do momentu, gdy faktycznie będzie on potrzebny. Domyślnie wszystkie moduły aplikacji ładowane są w chwili jej otwarcia, ale nie zawsze jest to praktyczne, ponieważ na przykład możemy mieć ścieżkę ze strażnikiem, której z powodu postawionych warunków użytkownik nigdy nie odwiedzi. Trochę się więc mija z celem ładowanie na dzień dobry zasobu, który i tak nie zostanie wykorzystany.

To trochę tak, jak z zostawieniem samochodu u mechanika. Jak wielki by nie był magazyn warsztatu jest fizycznie niemożliwe, żeby mechanik posiadał na stanie wszystkie części do wszystkich istniejących modeli samochodów. Gdy więc trafia do niego samochód, w którym na przykład konieczna jest wymiana amortyzatorów, wówczas mechanik składa zamówienie właśnie na amortyzatory. Jeśli się okaże, że do wymiany są również klocki hamulcowe wtedy składa dodatkowe zamówienie na nie. Czyli de facto uruchamia proces załadowania części wówczas, gdy następuje potrzeba ich użycia.

Na takiej samej zasadzie działa właśnie lazy-loading modułów w Angularze – dany moduł jest doładowywany do aplikacji dopiero, gdy użytkownik próbuje się dostać na prowadzącą do niego ścieżkę (natomiast w przypadku gdy na ścieżce ustawiony jest strażnik, ładowanie następuje dopiero po spełnieniu warunku).

By osiągnąć takie działanie musimy nieco zmodyfikować konfigurację ścieżki i zamiast odwoływać się bezpośrednio do komponentu, który będzie wstrzykiwany do router-outletu musimy wskazać cały moduł (ale nie zapominamy całkowicie o wskazywaniu komponentów, o tym za chwilę). Zdecydowanie ułatwione będziemy mieli zadanie, jeśli wcześniej zadbamy o modularyzację naszej aplikacji, czyli podzielimy funkcjonalności na moduły (czyli na przykład komponenty dotyczące ustawień będą opakowane w moduł Settings, a komponenty skoncentrowane na częściach zamiennych spakowane w moduł Parts, a wszystkie te nasze moduły zadeklarowane w głównym AppModule, który dzięki temu nie będzie jednym wielkim workiem deklaracji komponentów).

Gdy już będziemy mieli aplikację zmodularyzowaną możemy wrócić do naszych ścieżek i wówczas w miejscu parametru component używamy funkcji loadChildren, której musimy wskazać położenie modułu i zaszczepić odwołanie do niego w następujący sposób: () => import(‘./master-chamber/master-chamber.module’).then(m => m.MasterChamberModule).

Niestety to jeszcze nie wszystko, ponieważ o ile dzięki takiemu zabiegowi nasz moduł zostanie poprawnie załadowany przez przeglądarkę, to aplikacja nie będzie wiedziała, co tak właściwie ma wyświetlić użytkownikowi. I tutaj wracamy do wskazania komponentu, od czego jak widać, nie uda nam się jednak uciec.

Moduł, który ładujemy poprzez lazy-loading musi posiadać własny RoutingModule, który w momencie dopięcia modułu do aplikacji jest łączony z głównym RoutingModule całej aplikacji. Nie pozostaje nam zatem nic innego jak skonfigurować ścieżki dla tego modułu w znany nam już sposób, czyli deklarując nazwę ścieżki i wskazując komponent, którym chcemy zastąpić router-outlet. Tak jak wcześniej, możemy się tu pokusić o jakiś dodatkowych strażników, czy parametry, czy też możemy użyć dodatkowego parametru redirectTo i przekierować użytkownika na inną ścieżkę.

Na pierwszy rzut oka tworzenie niby prostej nawigacji może się wydawać skomplikowanym procesem, ale szybko zauważysz, że narzędzia dostarczane przez Angulara mają dużo zalet, a trzymanie mapy aplikacji w klasie RoutingModule zdecydowanie upraszcza pracę, gdy chcemy łatwo i szybko odszukać nazwy deklarowanych ścieżek. Przyjemnym pomysłem jest również przypisanie ścieżek do wartości enuma, dzięki czemu nie będziemy nigdy mieć kłopotu z linkiem niedziałającym z powodu drobnej literówki. Zachęcam do eksperymentowania z modułem Router i zabawę w projektanta tras, tylko zawsze pamiętaj, że im bardziej zamotana będzie Twoja trasa, tym łatwiej będzie się zgubić Twojemu zespołowi…


Paulina Schoenfeld

Full Stack Developer, za czasów studiów sceptycznie nastawiona do programowania, aktualnie fanka Angulara i frontendu, który okazał się być naprawdę fajną i satysfakcjonującą zabawą.


Wydrukuj