Wróć do strony głównej
Angular

Ciemna strona server side renderingu cz.2

Po “krótkiej” przerwie czas na kontynuację. Co było ostatnio? Sami zobaczcie tutaj. Co dzisiaj? Przede wszystkim skupimy się na tym, za co tak naprawdę odpowiada flaga initial state w routerze Angulara oraz zainteresujemy się transferem danych między klientem a serwerem.

“Jak powiedzieć przeglądarce, że zrobiłem coś na serwerze” – czyli TransferState w praktyce:

 

Na pewno zdarzyło wam się pobierać dane na serwerze czy znaleźć w sytuacji kiedy musieliście przesłać coś z aplikacji serwerowej do klienckiej. Wystarczy trochę poszperać i dostajemy informację o TransferState. Cóż to takiego?  Podążając za dokumentacją:

A key value store that is transferred from the application on the server side to the application on the client side.

A więc co potrzebujemy żeby z niego skorzystać – przede wszystkim zacznijmy od zaimportowania ServerTransferStateModule po stronie serwerowej i BrowserTransferStateModule po stronie klienckiej tak, aby móc korzystać ze wspomnianego serwisu. Teraz mamy wszystko żeby używać TransferState w naszej aplikacji. Przykład – trywialny komponent który pokazuje randomowy kolor na ekranie.

Proste? Proste. Ale powoduje to jeden problem – aplikacja serwerowa zwróci inny kolor niż aplikacja kliencka. Jak temu zaradzić? Wykorzystać TransferState i przesłać kolor z serwera. Zmieńmy trochę nasz kod:

 

Co tutaj się zadziało? Po pierwsze w konstruktorze wstrzyknęliśmy sobie nasz identyfikator platformy oraz nasz TransferState. Następnie wygenerowaliśmy sobie klucz wykorzystując metodę makeStateKey – dzięki niej mówimy, że do naszego state’a zapisujemy obiekt o kluczu ‘random_kolor’ będący typem string. Następnie na serwerze wygenerowaliśmy kolor, który zapisujemy i przekazujemy go do przeglądarki. Po załadowaniu aplikacji klienckiej po prostu pobieramy wartość o danym kluczu z naszego TransferState’a. Metoda get przyjmuje 2 parametry – pierwszy to klucz jakiego chcemy użyć do pobrania wartości, drugi to defaultowa wartość na wypadek gdyby nie udało się znaleźć podanej wartości klucza. Pisania dużo – działanie bardzo proste. 

Do czego można to wykorzystać? Tak naprawdę do wszystkiego, czego potrzebujemy. W swoich aplikacjach używałem tego np. do dodawania informacji o kraju użytkownika pobranej z headera requesta. Najczęściej jednakowoż stosuje się ten mechanizm do cache’owania zapytań http, aby nie musieć ich ponownie pobierać w aplikacji klienckiej. Spiesząc z odpowiedzią – nie musicie tego robić sami, taki mechanizm jest już gotowy.  

W jaki sposób przesyłane są dane do aplikacji klienckiej? 

Gdzie id skryptu to nazwa podana w naszym app.module: BrowserModule.withServerTransition({appId: 'serverApp’}),

 

“Gdzie mój widok?” –  czyli initial state i jego problemy:

Pewnie każdy z was zauważył że użycie SSR powoduje pewną zmianę w root module routingu, a mianowicie ustawienie flagi initial navigation na enabledBlocking:

No i po co nam to? A no po to, żeby ustalić kolejność zdarzeń – czy najpierw rozpinamy aplikację, a potem inicjalizujemy routing, czy też czekamy wykonanie akcji routingu, a potem rozpinamy aplikację. Rzut oka na dokumentację:

’enabledNonBlocking’ – (default) The initial navigation starts after the root component has been created. The bootstrap is not blocked on the completion of the initial navigation. 

Czyli defaultowe zachowanie, które obrazowo przedstawia się następująco:

Jak widzimy najpierw odbyło się bootsrapowanie komponentu, a następnie akcje routingu.

’disabled’ – The initial navigation is not performed. The location listener is set up before the root component gets created. Use if there is a reason to have more control over when the router starts its initial navigation due to some complex initialization logic.

Tutaj nie zatrzymamy się długo – sami decydujemy kiedy zainicjalizować router. Przykład użycia – paczka ngx-translate-router

’enabledBlocking’ – The initial navigation starts before the root component is created. The bootstrap is blocked until the initial navigation is complete. This value is required for server-side rendering to work.

Ta flaga interesuje nas najbardziej, czyli najprościej mówiąc najpierw odbywa się inicjalizacja routingu, a dopiero po jej pomyślnym zakończeniu rozpinanie komponentów. Dlaczego tak jest? Dzieje się to po to, aby uniknąć podwójnego ładowania strony czy też migania (flickeringu) aplikacji przy przejściu z wersji serwerowej na wersję kliencką. Jest to szczególnie zauważalne kiedy używa się lazy loadingu dla modułów – wtedy widzimy najpierw komponent serwerowy, mignięcie i komponent kliencki. I tutaj można byłoby postawić kropkę, gdyby nie kilka niuansów. 

Jeżeli komponenty rozpinane są w późniejszej kolejności może się okazać, że jakiś komponent, który jest potrzebny jeszcze nie został stworzony po stronie klienta. Przykład z życia wzięty i jak pewnie się domyślacie zajawki poprzedniego artykułu. Tworzycie sobie coś na wzór rozwiązań mikrofrontendowych i autoryzacja odbywa się za pośrednictwem aplikacji w iframe, który znajduje się w app-componencie. Idąc dalej – posiadacie guarda, który nie pozwala na przejście do danej podstrony bez autoryzacji, która odbywa się właśnie za pośrednictwem wspomnianego iframe’a. I co w przypadku włączenia serwerowego initialNavigation? Nie mamy dedykowanej strony do autoryzacji, gdyż robimy to w iframe znajdującym się nieistniejącym jeszcze komponencie, wklejamy deep link oczekując popupa do logowania, a ten się nie pojawia ponieważ app component zostanie rozpięty po pomyślnej inicjalizacji routingu, a ta nie zakończy się pomyślnie ponieważ guard oczekuje na callback z popupa, który się nie pojawi. Uff – dużo tego w zamkniętym kole. Musimy mieć to na uwadze przy okazji projektowania aplikacji tego typu. Jak można rozwiązać ten problem?

Sposób 1:

Najprościej – zrobić dedykowaną stronę do logowania i odpowiedni serwis, ale zakładamy, że nie tego oczekujemy albo nie do końca tego.

Sposób 2: 

Spróbować flagi enabledNonBlocking. Trzeba się liczyć z tym, że przechodzenie między wersja serwerową a kliencką może być irytujące dla użytkownika – aplikacja może dziwnie migać, pozycja scrolla może się zmienić – odradzam. 

Sposób 3:

Kolejnym podejściem zakrawającym o podejście mikrofrontendowe jest wykorzystanie custom elements – temat był już poruszany na naszym blogu.  Co nam to daje? Przede wszystkim nasz komponent autoryzacyjny znajduje się poza aplikacją angularową więc na nic nie czeka.  Z poziomu guarda możemy mu przesłać informację, że chcemy się zalogować i czekać na odpowiedź. Rozwiązanie na pewno warte uwagi, aczkolwiek wymagające trochę pracy.

Sposób 4:

Istnieje również inny, myślę najciekawszy sposób –  wykorzystanie wspomnianych już wcześniej mechanizmów, a więc guarda serwerowego i TransferState. Proste flow:

  1. W guardzie autoryzacyjnym po stronie serwera zapisujemy do naszego state’a docelowy route
  2. Następnie po stronie klienta sprawdzamy czy nasz state zawiera klucz, jeżeli tak, to przekierowujemy użytkownika do naszej poczekalni. W tym momencie guard kończy swoje działanie, a więc aplikacja zostaje rozpięta.

    3. W ostatnim kroku – w naszej poczekalni pobieramy wartość klucza z punktu 1, usuwamy wpis i robimy ponowne przekierowanie na adres zapisany w state

 

Trochę skomplikowane, ale w praktyce działa bardzo dobrze. Użytkownik nie zorientuje się, że coś takiego się wydarzyło, bo na ekranie cały czas będzie widział naszą poczekalnię, nie musimy kombinować z customowymi elementami i wszystkimi problemami, które za sobą niosą, a i tak osiągamy upragniony rezultat.

Znalezienie powyższego problemu jest trudne albo wręcz niemożliwe – kończy się wielogodzinnym debugowaniem kodu, gdyż próżno szukać rozwiązań w internecie (przynajmniej mnie nie udało się tego zrobić). Trzeba być naprawdę świadomym działania Angulara, żeby wiedzieć, że coś takiego może się wydarzyć.

Podsumowanie

Krótko podsumowując – dziś trochę ciekawostek i zagadnień mało oczywistych. O ile znalezienie TransferState w internecie jest dość proste, o tyle zgłębienie initial state jest o wiele trudniejsze i nie zawsze gdziekolwiek opisane. Co dalej w świecie SSR? W następnym wpisie trochę tips and tricks – jedne serwerowe, drugie dość uniwersalne. Zapraszam! 

 

O autorze

Kamil Puczka

Stroniący od CSSów fullstack developer w pełni odnajdujący się w świecie Microsoft i .NET. Raczej grzebiący w architekturze aplikacji, aniżeli jej wizualnej części. Z Angularem od zarania dziejów. Po godzinach gitarzysta death metalowy i fan ciężkiego brzmienia.

Zapisz się do naszego newslettera. Bądź na bieżąco z najnowszymi trendami, poradami, meetupami i stań się częścią społeczności Angulara w Polsce. Rynek pracy docenia członków społeczności.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *