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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import { Injectable } from '@angular/core'; import { HammerGestureConfig } from '@angular/platform-browser'; import * as Hammer from 'hammerjs'; @Injectable() export class HammerConfig extends HammerGestureConfig{ overrides = { swipe: { direction: Hammer.DIRECTION_ALL } }; } |
No i co może się wysypać, przecież nie ma tutaj żadnego obiektu window. Odpalamy build aplikacji, a konsola na to:
1 2 3 4 |
(window, document, 'Hammer'); ^ ReferenceError: window is not defined |
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ć:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@Injectable() export class HammerConfig extends HammerGestureConfig { hammer: any; overrides = { swipe: { direction: 30 } }; constructor() { super(); this.getHammer().then(_=>console.log('Hammer ready')); } async getHammer(): Promise<any> { if (typeof window !== undefined) { // conditional include because window is undefined on build this.hammer = await import( /* webpackPrefetch: true */ 'hammerjs' ); } } |
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.:
1 |
app.get('/auth-path/**', (req, res) => { res.sendFile(join(DIST_FOLDER, 'browser', 'index.html')); }); |
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:
- Tworzymy sobie tymczasową stronę / komponent, który zobaczy użytkownik niech to będzie SsrRedirectComponent:
1234567@Component({selector: 'app-ssr-redirect',template: `<h1>Waiting for initialization!</h1>`,changeDetection: ChangeDetectionStrategy.OnPush})export class SsrRedirectComponent{}
- Następnie stwórzmy guarda, który zadziała tylko po stronie serwera i przekieruje nas na odpowiednio zdefiniowaną ścieżkę:
12345678910111213141516@Injectable({providedIn: 'root'})export class SsrRedirectGuard implements CanActivate {constructor(@Inject(PLATFORM_ID) private readonly platformId: unknown, private readonly router: Router) {}canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree {if (isPlatformServer(this.platformId)) {return this.router.parseUrl(`/${ROUTE_SLUGS.ssrAuth}`);}return true;}}
App.-routing.module
1 2 3 |
const routes: Routes = [... { path: ROUTE_SLUGS.ssrAuth, component: SsrRedirectComponent } ...] |
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ą.
1 2 3 4 |
const routes: Routes = [... { path: ROUTE_SLUGS.ssrAuth, component: SsrRedirectComponent }, { path: ROUTE_SLUGS.protectedPath, component: ProtectedComponent, canActivate:[SsrRedirectGuard, AuthGuard]} ...] |
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.
Podsumowanie
Podsumujmy, co udało się nam się dziś dowiedzieć. Po pierwsze – jak poradzić sobie z problemem brakujących obiektów w bibliotekach trzecich na etapie buildowania aplikacji bez konieczności ingerowania w ich kod. Po drugie dowiedzieliśmy się jak można w prosty sposób wykluczać ścieżki z Server Side Renderingu czy też radzić sobie z autoryzacją. Co dalej? Zapraszam do przeczytania kolejnych wpisów już niedługo.
Pingback: Ciemna strona server side renderingu cz.2 - Angular.love
Pingback: The dark side of server side rendering part 2 - Angular.love