Wróć do strony głównej
Angular

Ciemna strona server side renderingu – renderowanie strony pod stronie serwera

Server side rendering staje się coraz bardziej popularny i powszechny. Co nam daje, jak działa i z czym się to je opisał doskonale Tomek tutaj. W teorii dokumentacji Angulara odpalimy jedną komendę, zwrócimy uwagę na kilka rzeczy i gotowe – w praktyce okazuję się to nie takie proste i łatwe. W tym artykule postaram się zajrzeć w rejony i problemy, z którymi część was może się spotkać, a których rozwiązanie nie jest aż takie trywialne i przy tym znalezienie odpowiedzi w internecie czasem nawet niemożliwe. 

W pierwszej części spojrzymy na problem obiektu window na etapie builda, zainteresujemy się guardami i blokowaniem dostępu do aplikacji po stronie serwera np. w przypadku braku informacji o autoryzacji. W kolejnych rzucimy okiem na problemy initial state, zobaczymy jak pozbyć się podwójnie ładowanych animacji, dowiemy się jak przekazywać informacje między aplikacją kliencką i serwerową oraz jak radzić sobie z CSSami sterowanymi z poziomu kodu np. przy okazji media query i zajrzymy do obiektu request. No to jazda!

“Przecież mam mocka window, dlaczego to nie działa” – problemy na etapie buildowania aplikacji.

Jak powszechnie wiadomo po stronie serwera nie mamy przeglądarki więc nie mamy np. obiektu window – oczywiste, choć nie zawsze. Zdarzają się takie problemy z bibliotekami, którym nie pomaga zmockowanie window np. Hammer.js czy animate-css-grid. 

Przykład:

Prosty serwis konfiguracyjny hammera:

 

No i co może się wysypać, przecież nie ma tutaj żadnego obiektu window. Odpalamy build aplikacji, a konsola na to:

 

 

Pierwszy pomysł – nie ma obiektu window, zapomniałem go zmockować. Sprawdzamy nasz server.ts – wszystko na swoim miejscu, domino skutecznie mockuje nasz window. Próżno szukać rozwiązań w google, które nie wymagają ingerencji choćby w webpacka, a nawet w kod źródłowy biblioteki. Można jednakowoż temu zaradzić:

 

 

To rozwiązanie  pozwala załadować potrzebne biblioteki na żądanie. W kodzie sprawdzamy czy nasz window jest dostępny i ładujemy bibliotekę. Niby proste, ale potrafi napsuć krwi. Zapraszam również do przeczytania artykułu, ktory opisuje, jak można wykorzystać to rozwiązanie do lazy loadingu komponentów oraz co daje użyty powyżej komentarz

 

“Skąd mam wiedzieć czy jestem zautoryzowany” – czyli guardy po stronie serwera i wykluczanie ścieżek z SSR:

Kolejny problem na który można natrafić to wykluczenie ścieżki z konieczności SSR. Przykład –  jakaś część aplikacji wymaga autoryzacji, więc nie da się tego zrobić po stronie serwera lub potrzebujemy jakiegoś obiektu przeglądarki w naszym komponencie np. storage’a. Rozwiązań tego problemu jest kilka, ale moim zdaniem żadne nie daje w 100% zadowalającego rezultatu.

Pierwszy sposób to wykorzystanie naszego serwera i w przypadku odwołań do określonych ścieżek zwracanie po prostu index.html np.:

W moim przypadku pomysł w ogóle się nie sprawdził, przede wszystkim dlatego, że wszystkie ścieżki w pliku index.html muszą być bezwzględne, stąd wymaga to od nas jakkolwiek modyfikacji ścieżek w tymże pliku lub kombinowanie z renderowaniem.

Drugi sposób to wystawienie dwóch aplikacji – tej serwowanej przez SSR i  drugiej  serwowanej w sposób tradycyjny, a co za tym idzie odpowiednie kierowanie ruchu. Pomysł również nad wyraz średni, jeżeli nie powiedzieć zły.

Jeżeli ktoś decyduje się na któreś z powyższych rozwiązań musi wziąć pod uwagę jeszcze jeden problem – co jeżeli pojawi się wymaganie tłumaczenia routingu? Wtedy dla każdej ścieżki będzie trzeba robić odpowiednie przekierowanie, pomnożone przez liczbę obsługiwanych tłumaczeń, co dodatkowo dyskredytuje oba pomysły.

Nieidealny, ale moim zdaniem najprostszy i chyba najmniej inwazyjny jest sposób trzeci. On również posiada jedną zasadniczą wadę o czym jeszcze wspomnę. Ale wracając do sedna – dlaczego nie przekierować użytkownika na stronę tymczasową, która po wyrenderowaniu aplikacji przekieruje nas w odpowiednie miejsce? Przykład:

  1. Tworzymy sobie tymczasową stronę / komponent, który zobaczy użytkownik niech to będzie SsrRedirectComponent:

     
  2. Następnie stwórzmy guarda, który zadziała tylko po stronie serwera i przekieruje nas na odpowiednio zdefiniowaną ścieżkę:

    App.-routing.module

3. Powyższy guard przekieruje wersję serwerową naszej aplikacji  do naszego komponentu tymczasowego, natomiast nie zmieni routingu, dzięki czemu po załadowaniu aplikacji po stronie przeglądarki użytkownik nie zostanie na naszej stronie tymczasowej, ale zostanie przekierowany na stronę docelową.

Na naszym komponencie tymczasowym można wyświetlić chociażby loader i użytkownik nie zorientuje się, że jest w czymś na kształt poczekalni, szczególnie że w pasku adresu przeglądarki zobaczy adres docelowy, a aplikacja w międzyczasie zrobi robotę i albo nas wpuści dalej, albo przekieruje na stronę logowania czy też wyświetli odpowiedni popup. No właśnie wyświetli, albo nie wyświetli, ale o tym w drugiej części artykułu.

“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ć.

 

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.

2 komentarzy

  1. Pingback: Ciemna strona server side renderingu cz.2 - Angular.love

  2. Pingback: The dark side of server side rendering part 2 - Angular.love

Dodaj komentarz

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