Strona główna / Artykuły

Renderowanie po stronie serwera aplikacji React 18 z użyciem Express i Babel

Przekształć projekt Create React App na renderowanie po stronie serwera za pomocą Express i Babel: renderToString, hydrateRoot, MemoryRouter, a następnie zobacz, jak Remix i Gatsby realizują SSR.

Wydawca: REACTAPP.TOP7 min czytania1712 słów

Źródła: A Hands-on Guide for a Server-Side Rendering React 18 App

Renderowanie po stronie serwera w Reactu bardzo się zmieniło od pojawienia się React 18. Poniższe instrukcje są skierowane do React 18, Create React App 5 oraz React Router 6 i wychodzą od standardowego projektu Create React App, który jest stopniowo modyfikowany. Renderowanie musi odbywać się na serwerze, dlatego potrzebny jest proces Node. Zwykły Node wystarcza, ale nodemon jest przydatny podczas rozwoju, ponieważ restartuje aplikację za każdym razem, gdy zmieniony zostanie jakiś plik. Globalna instalacja wygląda tak:

% npm install -g nodemon

CSR versus SSR w kilku zdaniach

Przy renderowaniu po stronie klienta (CSR) przeglądarka otrzymuje niemal pustą stronę HTML, a JavaScript uzupełnia jej treść. Pierwsze załadowanie może być wolne, ale późniejsza nawigacja jest szybka, a serwer ma niewielki obciążenie, ponieważ prace wykonywają biblioteki JavaScript. Słabością jest optymalizacja pod kątem wyszukiwarek, ponieważ boty nie znajdują żadnej statycznej treści do przeczytania.

Przy renderowaniu po stronie serwera przeglądarka otrzymuje natomiast kompletną stronę HTML, którą serwer już wyrenderował. Dzięki temu uzyskuje się korzyści z SEO, a pierwsze odświeżenie strony jest szybkie. Wadą jest to, że późniejsze zmiany zazwyczaj wymagają pełnego ponownego załadowania strony, co może zwiększyć obciążenie serwera.

Pomogą tu kilka powiązanych terminów:

  • Aplikacja jednostronicowa (SPA) wykorzystuje CSR. Zamiast serwować oddzielne pliki HTML dla każdej trasy, dynamicznie renderuje każdą trasę wewnątrz przeglądarki.
  • Universal JavaScript działa zarówno na stronie klienta, jak i serwerze. Renderuje HTML w przeglądarce jak SPA, renderuje ten sam HTML na serwerze i wysyła go do przeglądarki.
  • Kod React jest z natury uniwersalny, więc kod napisany dla CSR może być wykorzystywany do SSR przy użyciu tej samej bazy kodu.
  • SSR jest w rzeczywistości starsze od CSR. Powróciło w nowoczesnej formie opartej na universal JavaScript, i to właśnie ma się na myśli, mówiąc obecnie o SSR.

    Bazowy aplikacja CSR

    Stwórz nowy projekt:

    % yarn create react-app react-ssr
    % cd react-ssr
    

    Rozpocznij od wywołania npm start. W karcie Elements przeglądarki obracający się logo oraz tekst pojawiają się jako HTML wygenerowany przez JavaScript.

    To jest klasyczny przykład CSR: skrypty generują treść HTML. Karta Network pokazuje, co faktycznie wysłał serwer.

    Ciało strony zawiera plik z JavaScriptem, a nie żadną rzeczywistą treść, więc boty indeksujące nie mają prawie nic istotnego do zapisania.

    Dostarczanie wersji produkcyjnej

    SSR wymaga wersji produkcyjnej, więc pierwszym krokiem jest mały serwer Express, który dostarcza skompilowany wynik Reacta. Wstępna konfiguracja Reacta gotowa do użycia w produkcji stwarza niezbędne podstawy. Plik serwera istnieje w dwóch wersjach: jedna to CommonJS, server/index.js, a druga to moduł ES, server/index.mjs.

    Zejdź na poziom npm run build, aby utworzyć plik budowlany, a następnie uruchom serwer za pomocą nodemon server lub nodemon server/index.mjs. Karta Network pokazuje teraz jeden plik JavaScript (linia 12) oraz pusty kod markup (linia 17). Na razie nie odbywa się żadna renderizacja na serwerze, więc nadal mamy do czynienia z CSR.

    Trzy kroki do SSR w Express

    Aby SSR działało w serwerze Express, potrzebne są trzy kroki.

    Krok 1: nawodnienie na stronie klienta. ReactDOM.hydrateRoot() jest podobny do ReactDOM.createRoot(), przy czym jego sygnatura to hydrateRoot(container, element[, options]). Służy do kontenera, którego HTML został już wygenerowany przez ReactDOMServer. Ponieważ węzeł zawiera już kod renderowany na serwerze, React go zachowuje i dodaje jedynie obsługę zdarzeń, co przyspiesza początkowe ładowanie. Wywołanie to znajduje się na linii 7 w pliku src/index.js.

    Krok 2: renderowanie na serwerze. React dostarcza obiekt ReactDOMServer, który przekształca komponenty w statyczny kod, dzięki czemu przeglądarka otrzymuje stronę już wypełnioną danymi. Ponieważ kod Reacta to uniwersalny JavaScript, te same komponenty działają zarówno po stronie klienta, jak i serwera.

    Krok 3: obsługa specyficznych potrzeb strony. Każda trasa może wymagać osobnej uwagi, jak pokazano w późniejszych sekcjach.

    Uczymy Node rozumieć JSX

    Istnieje kilka pakietów, które mogą uruchomić SSR. @babel/register należy do najprostszych. Zainstaluj go razem z babel-plugin-transform-assets:

    % yarn add -D @babel/register
    % yarn add -D babel-plugin-transform-assets
    

    Oba trafiają do devDependencies:

    "devDependencies": {
      "@babel/register": "^7.18.9",
      "babel-plugin-transform-assets": "^1.0.2"
    }
    

    @babel/register kompiluje pliki w czasie rzeczywistym i można go skonfigurować do transformacji JSX oraz zasobów. Istnieje jednak jedna ograniczenie: nie może on kompilować natywnych modułów ES Node.js w czasie rzeczywistym, ponieważ nie ma jeszcze stabilnej API do obsługi ładowania modułów ES. Dlatego eksperymenty z SSR tutaj wykorzystują pliki CommonJS.

    React 18 przepracował swoje API serwerowe i przeniósł je do folderu react-dom/server. Funkcja renderToString nadal działa, choć jej obsługa Suspense jest ograniczona. Nowsze API, w tym renderToPipeableStream, pełnie obsługują Suspense na serwerze w połączeniu z przepływowym SSR.

    Przejrzenie serwera SSR

    Zaktualizowany plik server/index.js jest wyjaśniony poniżej według numerów wierszy.

    • Wiersze od 1 do 15 konfigurują Babel za pomocą hooka require, dzięki czemu pliki są kompilowane w momencie ich ładowania.
  • Linię 2 zawiera dwa presety. @babel/preset-env umożliwia kodowi korzystanie z najnowszej wersji JavaScript bez konieczności ręcznego wyboru transformacji składniowych dla każdego środowiska docelowego. @babel/preset-react automatycznie importuje funkcje, do których kompiluje JSX.
  • Linię 3 do 14 służy konfiguracja babel-plugin-transform-assets, który określa, w jaki sposób pliki multimedialne o statycznej strukturze są przekształcane. Bez niego tag svg powoduje błąd SyntaxError: Unexpected token '<'.
  • Linia 16 wymaga biblioteki React, linia 17 – ReactDOMServer, a linia 18 – wartości domyślnej eksportowanej z pliku src/App.js.
  • Tutaj kolejność ma znaczenie. Wyrażenie app.use() z linii 47 musi zostać wywołane po app.get() z linii 25 do 45. Gdyby zostało wywołane pierwsze, serwowałoby pliki statyczne, w tym index.html, dla ścieżki głównej, a app.get() nigdy nie dostałoby szansy na wykonanie.

    Na linii 26 zapisywany jest adres każdej żądania. W przypadku budowy aplikacji Create React App lista wygląda w ten sposób:

    Request URL = /
    Request URL = /static/js/main.b92cc87b.js
    Request URL = /static/css/main.073c9b0a.css
    Request URL = /static/media/logo.06e73328.svg
    Request URL = /static/css/main.073c9b0a.css.map
    Request URL = /static/js/main.b92cc87b.js.map
    Request URL = /static/media/logo.6ce24c58023cc2f8fd88fe9d219db6c6.svg
    Request URL = /favicon.ico
    Request URL = /manifest.json
    Request URL = /logo192.png
    

    Liny 27 do 29 zapewniają, że tylko trasa główna jest renderowana przez app.get(), a pliki statyczne trafiają do następnego middleware na linii 47.

    Lina 30 wywołuje ReactDOMServer.renderToString(element), aby wygenerować HTML na serwerze. Naturalne byłoby napisanie bezpośrednio ReactDOMServer.renderToString(), ale wtedy JSX musiałoby znajdować się w innym pliku niż ten, w którym skonfigurowano @babel-register, stąd oddzielny element.

    Lina 31 wyświetla markup wygenerowany przez serwer:

    <div class="App"><header class="App-header"><img src="static/media/logo.06e73328.svg" class="App-logo" alt="logo"/><p>Edit <code>src/App.js</code> and save to reload.</p><a class="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">Learn React</a></header></div>
    

    Linię 33 używa się do załadunku wersji produkcyjnej pliku index.html, a linie 34 do 44 odczytują jego zawartość. Jeśli nie wystąpi błąd, wygenerowana struktura markup (linia 42) jest wstawiana do tagu korzeniowego, a gotowy plik index.html staje się odpowiedzią na pierwszą prośbę.

    Sprawdzenie poprawności działania

    Zainstaluj nodemon server i ponownie sprawdź zakładkę Network. Pobrany dokument zawiera teraz strukturę markup wygenerowaną przez serwer. Linie 17 do 27 sekcji body zawierają całą stronę, co daje narzędziom SEO rzeczywisty materiał do analizy.

    Aby przeprowadzić bardziej rygorystyczną próbę, otwórz chrome://settings/content/javascript i wyłącz JavaScript.

    Strona nadal renderuje się bez JavaScriptu.

    To właśnie jest SSR.

    Dodawanie tras

    Aplikacja jednostronicowa jest prosta w realizacji, więc następnym krokiem są multiple trasy. Zacznij od zainstalowania biblioteki react-router-dom:

    % yarn add react-router-dom
    

    Dodaje się to do dependencies:

    "dependencies": {
      "react-router-dom": "^6.11.2"
    }
    

    Następnie edytuj plik src/App.js. Wiersz 1 importuje bibliotekę React, która jest potrzebna do SSR. Wiersz 2 importuje MemoryRouter zamiast BrowserRouter. Router przeglądarki opiera się na API historii HTML5 pushState, którego Node.js nie dostarcza. MemoryRouter przechowuje historię URL w pamięci bez wpływu na pasek adresu, co jest przydatne podczas testów oraz w środowiskach innych niż przeglądarka. Inną opcją jest StaticRouter z pakietu react-router-dom/server, choć nie nadaje się on do złożonych tras. Wiersze od 34 do 40 służą do uruchomienia MemoryRouter.

    Zbuduj projekt ponownie za pomocą polecenia npm run build, uruchom nodemon server i odwiedź adres http://localhost:8080.

    SSR działa, choć istnieje jeden problem: adres w pasku adresu się nie aktualizuje.

    Gdzie ręczne implementowanie SSR staje się trudne

    Routing funkcjonuje przy tym zastrzeżeniu, a inne problemy wciąż pozostają nierozwiązane, takie jak pobieranie danych i Redux. Praca po stronie serwera jest mniej prosta niż po stronie klienta, a każda strona może wymagać specjalnego potraktowania.

    Nie ma potrzeby rozwiązywania tego wszystkiego ręcznie, ponieważ kilka frameworków i bibliotek obejmuje SSR. Remix to framework webowy typu full-stack, który wychodzi od podstaw webu i obejmuje elementy aż po interfejs użytkownika, w tym SSR i inne funkcje, bez konieczności pisania kodu szablonowego. Jego kod klienta wywołuje funkcję hydrateRoot na linii 4, a kod serwera wywołuje renderToPipeableStream na linii 4.

    W React 18 wprowadzono funkcję renderToPipeableStream(element, options). Podając do niej element React, generuje on początkowy kod HTML i zwraca strumień. Strumień ten posiada metodę pipe(res) do przesyłania wyniku oraz abort() do anulowania żądania. Pełnie wspierany jest mechanizm Suspense, a kod HTML z opóźnionym treścią generuje strumienie bez efektu „pop-in”, ponieważ tagi <script> umieszczone wewnątrz tekstu wypełniają puste miejsca później.

    Na karcie Network ciało strony głównej Remix już zawiera pełną treść strony, więc jest to SSR.

    Gatsby, Next.js, NuxtJS, Quasar i SvelteKit również oferują SSR oraz powiązane funkcje od razu, bez konieczności pisania dodatkowego kodu, chociaż każdy z nich definiuje SSR nieco inaczej. Strona główna Gatsby pokazuje taką samą pełną treść na karcie Network.

    Gatsby to szybki i niezawodny generator stron statycznych, który wykorzystuje React do renderowania treści statycznej i umożliwia modyfikację statycznego HTML podczas jego renderowania na serwerze.

    SSR czy renderowanie statyczne

    Gdy strona zawiera wyłącznie dane statyczne, renderowanie statyczne jest szybsze. Jeśli odpowiedź jest dynamiczna, lepszym wyborem jest SSR. Czasami najlepiej sprawdza się rozwiązanie hybrydowe. SSR odbywa się na żądanie, za każdym razem gdy jest żądany plik, natomiast renderowanie statyczne następuje tylko raz podczas budowania aplikacji. Oba podejścia są przyjazne dla SEO, ponieważ w obu przypadkach treść znajduje się w ciele pliku HTML.

    Podsumowanie

    Ustawienie SSR w Create React App polega na trzech elementach:

    1. Wyświetlenie markupu renderowanego na serwerze za pomocą ReactDOM.hydrateRoot().
    2. Renderowanie komponentów do statycznego markupu za pomocą obiektu ReactDOMServer, którego interfejsy służą zarówno do SSR, jak i renderowania statycznego.
  • Zajmij się tym, czego konkretnie potrzebuje każda strona.
  • W przypadku rzeczywistego projektu rozsądnym wyborem jest framework lub biblioteka, która już to robi.

    Want to Connect?
    
    If you are interested, check out my directory of web development articles.
    

    Powiązane materiały