Serverseitiges Rendering einer React 18-Anwendung mit Express und Babel
Ein Create React App-Projekt in Server-Side-Rendering mit Express und Babel umwandeln: renderToString, hydrateRoot, MemoryRouter – anschließend zeigen, wie Remix und Gatsby SSR umsetzen.
Quellen: A Hands-on Guide for a Server-Side Rendering React 18 App
Die Server-seitige Rendering-Technologie für React hat sich seit dem Erscheinen von React 18 stark verändert. Die unten beschriebene Einrichtung richtet sich auf React 18, Create React App 5 und React Router 6 aus und beginnt mit einem standardmäßigen Create React App-Projekt, das schrittweise umgewandelt wird. Das Rendering muss auf einem Server ausgeführt werden, weshalb ein Node-Prozess erforderlich ist. Einfacher Node funktioniert ebenfalls, doch nodemon ist während der Entwicklung praktisch, da es die Anwendung jedes Mal neu startet, wenn sich eine Datei ändert. Eine globale Installation sieht so aus:
% npm install -g nodemon
CSR gegen SSR in wenigen Sätzen
Beim Client-Side Rendering (CSR) erhält der Browser eine nahezu leere HTML-Seite, die durch JavaScript mit Inhalt gefüllt wird. Die erste Ladezeit kann lang sein, doch spätere Navigationen sind schnell, und der Server muss nur geringe Last tragen, da die JavaScript-Bibliotheken die Arbeit übernehmen. Die Schwäche liegt in der Suchmaschinenoptimierung, da ein Crawler keinen statischen Inhalt zum Lesen findet.
Beim Server-Side Rendering erhält der Browser hingegen eine bereits vom Server vollständig gerenderte HTML-Seite. Dadurch entstehen Vorteile für die SEO, und die erste Anzeige der Seite erfolgt schnell. Der Nachteil ist, dass spätere Änderungen in der Regel einen vollständigen Seitenneuladen erfordern, was die Last auf dem Server erhöhen kann.
Einige verwandte Begriffe helfen hier weiter:
- Eine Single-Page-Application (SPA) nutzt CSR. Anstelle von separaten HTML-Dateien pro Route wird jede Route dynamisch im Browser generiert.
SSR ist eigentlich älter als CSR. Es ist in einer modernen Form, die auf Universal JavaScript basiert, zurückgekehrt – und das ist es, was heutzutage gemeinhin unter SSR verstanden wird.
Eine Basis-CSR-Anwendung
Erstellen Sie ein neues Projekt:
% yarn create react-app react-ssr
% cd react-ssr
Führen Sie es mit npm start aus. Im Elements-Tab des Browsers erscheinen das drehende Logo sowie der Text als durch JavaScript erzeugter HTML-Code.
Das ist typisches CSR: Skripte erzeugen den HTML-Inhalt. Der Network-Tab zeigt, was der Server tatsächlich gesendet hat.
Der Body enthält nur ein JavaScript-Bundle und keinen eigentlichen Inhalt, sodass ein Crawler fast nichts Sinnvolles zum Indizieren hat.
Ausliefern der Produktionsversion
SSR benötigt eine Produktionsversion, weshalb die erste Aufgabe darin besteht, einen kleinen Express-Server zu erstellen, der die kompilierte React-Ausgabe bereitstellt. Die zuvor erläuterte, für die Produktion geeignete React-Einrichtung legt hierfür die Grundlage. Die Serverdatei gibt es in zwei Formen: eine als CommonJS, server/index.js, und die andere als ES-Modul, server/index.mjs.
Führen Sie npm run build aus, um die Build-Datei zu erzeugen, und starten Sie anschließend den Server mit nodemon server oder nodemon server/index.mjs. Die Network-Karte zeigt nun ein JavaScript-Bundle (Zeile 12) sowie leere Markup-Elemente (Zeile 17). Es findet noch keine Server-Seitengenerierung statt, daher handelt es sich weiterhin um CSR.
Die drei Schritte für SSR in Express
Damit SSR innerhalb des Express-Servers funktioniert, sind drei Schritte erforderlich.
Schritt 1: Hydratierung auf der Client-Seite. ReactDOM.hydrateRoot() ähnelt ReactDOM.createRoot() und hat die Signatur hydrateRoot(container, element[, options]). Er dient dazu, einen Container zu verwenden, dessen HTML bereits von ReactDOMServer erzeugt wurde. Da der Node bereits mit vom Server gerendertem Markup versehen ist, behält React dieses bei und fügt lediglich Event-Handler hinzu, wodurch die Anfangsladegeschwindigkeit erhöht wird. Der Aufruf befindet sich in Zeile 7 von src/index.js.
Schritt 2: Rendering auf der Server-Seite. React stellt das ReactDOMServer-Objekt bereit, um Komponenten in statisches Markup umzuwandeln, sodass der Browser eine bereits mit Daten gefüllte Seite erhält. Da React-Code universelles JavaScript ist, laufen die gleichen Komponenten auf beiden Seiten.
Schritt 3: Behandlung seitenbezogener Anforderungen. Jeder Route kann eigene Besonderheiten erfordern, wie in den folgenden Abschnitten gezeigt wird.
Node dazu bringen, JSX zu verstehen
Mehrere Pakete können SSR in Betrieb nehmen. @babel/register gehört zu den einfachsten. Installieren Sie es zusammen mit babel-plugin-transform-assets:
% yarn add -D @babel/register
% yarn add -D babel-plugin-transform-assets
Sie beide landen in devDependencies:
"devDependencies": {
"@babel/register": "^7.18.9",
"babel-plugin-transform-assets": "^1.0.2"
}
@babel/register kompiliert Dateien in Echtzeit und kann so konfiguriert werden, um JSX sowie Assets zu transformieren. Es gibt jedoch eine Einschränkung: Er kann keine nativen Node.js ES-Module in Echtzeit kompilieren, da es noch keine stabile API zur Steuerung des Ladevorgangs von ES-Modulen gibt. Deshalb verwenden die hier gezeigten SSR-Experimente Dateien im CommonJS-Format.
React 18 hat seine Server-APIs überarbeitet und sie unter react-dom/server zusammengefasst. renderToString funktioniert weiterhin, doch seine Unterstützung für Suspense ist eingeschränkt. Die neueren APIs, darunter renderToPipeableStream, unterstützen Suspense auf dem Server vollständig sowie das Streaming-basierte SSR.
Durchschnitt durch den SSR-Server
Der aktualisierte server/index.js wird im Folgenden Zeile für Zeile erläutert.
- Zeilen 1 bis 15 konfigurieren Babel über den
require-Hook, sodass Dateien beim Laden kompiliert werden.
@babel/preset-env ermöglicht es dem Code, die neueste JavaScript-Version zu verwenden, ohne für jede Zielumgebung manuell Syntax-Transformationen auswählen zu müssen. @babel/preset-react importiert automatisch die Funktionen, in die JSX kompiliert wird.babel-plugin-transform-assets, das festlegt, wie statische Mediendateien transformiert werden. Ohne dieses Plugin führt ein svg-Tag zu einem SyntaxError: Unexpected token '<'.React, Zeile 17 erfordert ReactDOMServer und Zeile 18 den Standardexport von src/App.js.Die Reihenfolge ist hier entscheidend. app.use() in Zeile 47 muss nach app.get() in den Zeilen 25 bis 45 ausgeführt werden. Würde app.use() zuerst aufrufen, würde es die statischen Dateien, einschließlich index.html, für die Root-Route liefern, und app.get() würde niemals zur Ausführung kommen.
Zeile 26 protokolliert jede Anfragen-URL. Bei einer Build-Ausführung von Create React App sieht die Liste so aus:
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
Die Zeilen 27 bis 29 stellen sicher, dass nur die Wurzelroute durch app.get() dargestellt wird, während statische Ressourcen an das nächste Middleware-Element in Zeile 47 weitergeleitet werden.
Zeile 30 ruft ReactDOMServer.renderToString(element) auf, um HTML auf dem Server zu erzeugen. Es wäre natürlich, direkt ReactDOMServer.renderToString() zu schreiben, doch dann müsste JSX in einer anderen Datei als derjenigen liegen, in der @babel-register konfiguriert ist – daher das separate Element.
Zeile 31 gibt den vom Server generierten Markup-Ausdruck aus:
<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>
Zeile 33 lädt die Produktionsversion von index.html und Zeilen 34 bis 44 lesen deren Inhalt. Falls es keine Fehler gibt, wird die erzeugte Markup-Struktur (Zeile 42) in das Wurzeltag eingefügt, und die fertige index.html-Datei wird zur Antwort auf die erste Anfrage.
Überprüfung der Funktionalität
Führen Sie nodemon server aus und schauen Sie sich erneut das Tab „Network“ an. Das heruntergeladene Dokument enthält nun vom Server generierte Markup-Strukturen. Zeilen 17 bis 27 des Body enthalten die gesamte Seite, was den SEO-Tools etwas Greifbares zum Auswerten bietet.
Für einen präziseren Test öffnen Sie chrome://settings/content/javascript und deaktivieren Sie JavaScript.
Die Seite wird auch ohne JavaScript weiter gerendert.
Das ist SSR.
Hinzufügen von Routen
Eine Ein-Seiten-Anwendung ist einfach umzusetzen, daher stellt sich die nächste Frage nach mehreren Routen. Beginnen Sie damit, react-router-dom zu installieren:
% yarn add react-router-dom
Es wird den dependencies hinzugefügt:
"dependencies": {
"react-router-dom": "^6.11.2"
}
Dann bearbeiten Sie src/App.js. Zeile 1 importiert React, das SSR benötigt. Zeile 2 importiert anstelle von BrowserRouter MemoryRouter. Der Browser-Router stützt sich auf die HTML5-pushState-History-API, die von Node.js nicht bereitgestellt wird. MemoryRouter speichert die URL-Geschichte im Speicher, ohne die Adressleiste zu beeinflussen, was sich für Tests und Umgebungen außerhalb des Browsers eignet. StaticRouter aus react-router-dom/server ist eine weitere Option, eignet sich jedoch nicht für komplexe Routen. Zeilen 34 bis 40 setzen MemoryRouter ein.
Bauen Sie erneut mit npm run build auf, starten Sie nodemon server und besuchen Sie http://localhost:8080.
SSR funktioniert – mit einem kleinen Problem: Die URL in der Adressleiste aktualisiert sich nicht.
Wo handgefertigtes SSR schwierig wird
Das Routing funktioniert unter dieser Einschränkung, doch es gibt weiterhin Probleme zu bewältigen, wie zum Beispiel das Abrufen von Daten und Redux. Die Arbeit auf Serverseite ist weniger unkompliziert als auf der Client-Seite, und jede Seite kann eine spezielle Behandlung erfordern.
Es ist nicht notwendig, alles manuell zu lösen, denn mehrere Frameworks und Bibliotheken bieten SSR mit. Remix ist ein Full-Stack-Webframework, das von den Grundlagen des Webs ausgeht und bis zur Benutzeroberfläche reicht, wobei SSR sowie weitere Funktionen bereits enthalten sind und kein Boilerplate erforderlich ist. Sein Client-Code ruft in Zeile 4 hydrateRoot auf, und sein Server-Code ruft in Zeile 4 renderToPipeableStream auf.
React 18 führte renderToPipeableStream(element, options) ein. Wenn man ihm ein React-Element gibt, erzeugt es die anfängliche HTML-Struktur und gibt einen Stream zurück. Der Stream verfügt über pipe(res) zum Weiterleiten der Ausgabe sowie über abort() zum Abbrechen der Anfrage. Suspense wird vollständig unterstützt, und HTML mit verzögert geliefertem Inhalt streamt ohne spätes „Pop-in“, da inline-<script>-Tags die Lücken anschließend füllen.
Im Network-Tab enthält der Körperteil der Remix-Indexseite bereits die gesamte Seite, sodass es sich um SSR handelt.
Auch Gatsby, Next.js, NuxtJS, Quasar und SvelteKit bieten standardmäßig SSR sowie damit verbundene Funktionen ohne zusätzlichen Boilerplate – allerdings definiert jeder Anbieter SSR etwas anders. Gathbys Indexseite zeigt im Network-Tab denselben vollständigen Körperteil.
Gatsby ist ein schneller, robuster Static-Site-Generator, der React verwendet, um statische Inhalte anzuzeigen, und es ermöglicht Ihnen, statisches HTML während der Server-Renderung zu ändern.
SSR oder statische Renderung
Wenn eine Seite nur statische Daten enthält, ist die statische Renderung schneller. Wenn die Antwort dynamisch ist, ist SSR die bessere Wahl. Manchmal eignet sich ein Hybrid am besten. Bei SSR erfolgt die Renderung auf Anfrage, jedes Mal, wenn eine Datei abgerufen wird, während die statische Renderung einmal bei der Erstellung stattfindet. Beide Methoden sind SEO-freundlich, da in beiden Fällen der HTML-Körper den vollständigen Inhalt enthält.
Zusammenfassung
Die Einrichtung von SSR für Create React App reduziert sich auf drei Komponenten:
- Zeigen Sie die vom Server gerenderte Markup-Struktur mit
ReactDOM.hydrateRoot()an. - Rendern Sie Komponenten in statische Markup-Strukturen mithilfe des
ReactDOMServer-Objekts, dessen APIs sowohl für SSR als auch für die statische Renderung genutzt werden können.
In einem echten Projekt ist die sinnvolle Wahl ein Framework oder eine Bibliothek, die dies bereits übernimmt.
Want to Connect?
If you are interested, check out my directory of web development articles.