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.
Ź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.
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.
@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.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 '<'.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:
- Wyświetlenie markupu renderowanego na serwerze za pomocą
ReactDOM.hydrateRoot(). - Renderowanie komponentów do statycznego markupu za pomocą obiektu
ReactDOMServer, którego interfejsy służą zarówno do SSR, jak i renderowania statycznego.
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
- Wyjaśnienie serwerów MCP: narzędzia, zasoby, polecenia i kiedy warto dodać bramkę — prosty przegląd serwerów MCP: jak łączą się host, klient i serwer, w czym MCP różni się od API, co można z nim robić oraz kiedy bramka jest przydatna.
- React Hooks i zarządzanie stanem: praktyczny przewodnik dla programistów — dlaczego hooks powodują ponowną renderizację i stają się przestarzałe, jak różnią się Context, Zustand, Jotai i Redux, a także jak używać useTransition, useDeferredValue oraz hooków w React Native.