Beliebte Suchanfragen

Cloud Native

DevOps

IT-Security

Agile Methoden

Java

//

Tutorial: Serverseitiges Rendering mit React – Teil 1: Einführung

6.12.2017 | 6 Minuten Lesezeit

Wie ich in dem Artikel SEO + SPA = serverseitiges Rendering? aufgezeigt habe, ist das serverseitige Rendering (SSR) nach wie vor die sicherste Möglichkeit, um SEO-kritische Inhalte zu präsentieren.

In diesem Tutorial zeige ich Schritt für Schritt, wie man eine clientseitige React-App auf eine serverseitig gerenderte App umstellt.

Das Tutorial habe ich in drei Teile aufgeteilt:

  1. Einführung
  2. React Router und React Helmet
  3. Redux Store

In diesem Teil schauen wir uns an, was SSR überhaupt ist und wie es prinzipiell funktioniert. Dabei steigen wir auch schon in die ersten Code-Beispiele ein und erstellen eine simple Version der serverseitig gerenderten App.
Im zweiten Teil kümmern wir uns um React Router und um React Helmet, damit wir für jede der angefragten Seiten immer das richtige statische HTML vom Server bekommen.
Im dritten Teil geht es dann um den State unserer Applikation. Wir tauchen tiefer in Redux und die Möglichkeiten ein, wie wir sowohl auf dem Server als auch auf dem Client den globalen State nutzen, um unsere Komponenten mit dynamischen Inhalten zu rendern.

1. Einführung

SSR bringt einiges an zusätzlichen Aufwand mit sich und hat auch ein paar Nachteile, die man bedenken sollte. Sowohl React als auch Angular bringen nativ die Möglichkeit mit, den Inhalt auf dem Server zu rendern. Das ist schonmal eine gute Sache, denn man muss nicht unbedingt kostenpflichtige externe Dienste wie prerender.io nutzen. Jedoch gibt es u.a. folgende Probleme:

  • SSR passiert immer dann, wenn die Seite angefragt wird. Es müssen also zunächst alle Inhalte auf dem Server zusammengesetzt werden bevor sie zum Browser gesendet werden. Gerade wenn es sich um Inhalte handelt, die man erst per API Request von externen Services holt, kann das eine gewisse Zeit in Anspruch nehmen, was die Wartezeit für den Nutzer erhöht. Hier macht es also Sinn, sich Gedanken über einen Caching Mechanismus zu machen. (Varnish, CDN)
  • Auf dem Server gibt es kein window-Objekt in JavaScript. Sollte dieses irgendwo im Code im Einsatz sein und nicht vor der Benutzung abgefragt werden, kommt es beim Rendern zu Fehlern.
  • Der Aufwand für das SSR hängt davon ab, wie viele Features wir aus dem React Ökosystem nutzen. Meistens kommen Module wie React Router, Redux, React Helmet usw. zum Einsatz. Für all die genannten müssen wir uns überlegen, wie der Server diese handhaben kann, denn ohne unser Zutun funktionieren diese Module nicht wie auf dem Client.

Für diejenigen, die noch nicht so viel Erfahrung mit SSR haben, zeigt der Infokasten „Wie funktioniert SSR in React?“ kurz zusammengefasst, was die grundlegende Funktionsweise des SSR ist.

Info: Wie funktioniert SSR in React?

Die Frage, die häufig gestellt wird, wenn es um das serverseitiges Rendering geht, ist, ob dann die ganze Dynamik der Single Page App (SPA) verloren geht, wenn man eine statische HTML Seite zum Browser schickt. Ich kann euch beruhigen, die Funktionalität wird in keinster Weise beeinflusst. Der Ablauf beim SSR ist zweistufig:

  1. Die statische HTML-Seite wird auf dem Server generiert und zum Client geschickt
  2. Im HTML ist ein script-Tag mit der bundle.js als src enthalten, die man auch im normalen React-Projekt ohne SSR laden würde. Die bundle.js enthält den minifizierten Code von React und allen anderen Dependencies. Sobald der Browser diese nachlädt, wird React initialisiert und aktualisiert den statischen Code, registriert alle Event-Handler etc.

Nach dem Laden der bundle.js funktioniert die Webseite genauso, wie ohne das SSR. Das heißt, es werden beim Navigieren solange keine erneuten Requests zum Server gemacht, bist der Nutzer die Seite komplett neu lädt. Die folgende Abbildung zeigt den Mechanismus nochmal grafisch:

1.1 Projektstruktur

Als Beispiel nutze ich eine einfache ToDo-App. Hierbei reduziere ich jedoch den Umfang der App allein auf das Anzeigen einer Homepage und einer Liste von ToDos.
Ich gehe von einem typischen Projekt-Setup für eine clientseitige React-App aus. Die Ordnerstruktur des Projektes könnte z.B. so aussehen:


my-project/
|------build/
|------src/
|      |-----actions/
|            |----- index.js
|      |-----components/
|            |----- Header.js
|            |----- Home.js
|            |----- ToDoList.js
|      |-----reducers/
|            |----- index.js
|            |----- todosReducer.js
|      |-----App.js
|      |-----index.js
|      |-----Routes.js
|------package.json
|------server.js
|------webpack.config.js

Weiterhin gehe ich davon aus, dass die React-App über einen express-Node.js-Server ausgeliefert wird. Die aktuelle server.js könnte in etwa so aussehen:

1import express from 'express';
2const app = express();
3 
4app.use(express.static('build/assets'));
5 
6app.get('*', (req, res) => {
7 res.sendFile(path.join(__dirname, "build/index.html"));
8});
9 
10app.listen(3000, () => {
11 console.log('Server gestartet auf http://localhost:3000');
12});

1.2 Webpack für Client und Server konfigurieren

Zunächst müssen wir uns klar machen, dass die serverseitig gerenderte React App in zwei verschiedenen Umgebungen gerendert werden wird (Server, Client). Wir benötigen deshalb auch unterschiedliche Konfigurationen.

In der Regel besitzen wir bei einer normalen React-App eine Webpack-Konfiguration, die dafür sorgt, dass der Code mit Babel zu ES5 transpiliert und in eine einzige bundle.js-Datei zusammengefasst wird. Das Ergebnis landet dann typischerweise in einem „build“-Ordner. Dies passen wir als erstes an.

Grundidee ist es, eine bundle.js für die statische Version unserer Webseite zu erzeugen, die dann vom Node-Server ausgeführt wird und eine zusätzliche bundle.js für die „dynamische“ React-App, die wir dann innerhalb der statischen HTML-Seite referenzieren und welche anschließend im Browser nachgeladen und ausgeführt wird.

Also passen wir zunächst unsere Webpack-Konfiguration an. Die bestehende webpack.config.js unterteilen wir in webpack.server.js und webpack.client.js:

1const path = require('path');
2const webpackNodeExternals = require('webpack-node-externals');
3 
4module.exports = {
5 target: 'node',
6 entry: './server.js',
7 output: {
8   filename: 'bundle.js',
9   path: path.resolve(__dirname, 'build'),
10 },
11 module: {
12  rules: [
13    ... // babel-loader, style-loader etc.
14  ]
15 },
16 externals: [webpackNodeExternals()]
17};
1const path = require('path');
2 
3module.exports = {
4 entry: './src/index.js',
5 output: {
6  filename: 'bundle.js',
7  path: path.resolve(__dirname, 'public')
8 },
9 module: {
10  rules: [
11    ... // babel-loader, style-loader etc.
12  ]
13 }
14};

Bei diesen Konfigurationen sind drei Properties besonders wichtig:
Das Erste ist das target innerhalb der webpack.server.js. Standardmäßig geht webpack von einer Browser-Umgebung aus (target: 'web'). Durch target: 'node' sagen wir webpack, dass es für eine Node.js-Umgebung transpilieren soll. Damit nutzt es z.B. die „require“-Syntax um Module zu importieren und verändert Module wie fs oder path nicht.

Das zweite wichtige Property ist der output path. Hier legen wir unsere transpilierten bundle.js Dateien in den public-Ordner (für den Client) und in den build-Ordner (für den Server).

Als drittes haben wir innerhalb der webpack.server.js durch externals: [webpackNodeExternals()] angegeben, dass keine npm-Module in die bundle.js eingebaut werden sollen, weil diese ganz einfach per require('npm-module-name') in der Node-Umgebung geladen werden können. Damit müssen sie nicht im bundle.js vorhanden sein, was die Datei kleiner macht.

Als nächstes passen wir die package.json an und rufen dort die beiden webpack-Konfigurationen auf:

1...
2"scripts": {
3 "dev": "npm-run-all --parallel dev:*",
4 "dev:start-server": "nodemon --watch build --exec \"node build/bundle.js\"",
5 "dev:build-server": "webpack --config webpack.server.js --watch",
6 "dev:build-client": "webpack --config webpack.client.js --watch"
7}

Ich nutze hier das Modul npm-run-all um alle „dev:*“-Skripte zu starten. Damit wird sowohl der Server als auch der Client gebaut und der Server gestartet. Mit nodemon überwache ich den „build“-Ordner und starte den Server automatisch neu, wenn sich dort eine Änderung ergibt. Mit npm run dev steht uns somit eine komfortable Entwicklungsumgebung zur Verfügung.

Zum Schluss passen wir nun noch die server.js an und integrieren hier bereits das serverseitige Rendering von React.

1import express from 'express';
2import ReactDOM from 'react-dom/server';const app = express();
3 
4const renderer = () => { const content = ReactDOM.renderToString(/* TODO */);  return `   <html>     <head></head>     <body>       <div id="app">${content}</div>       <script src="bundle.js"></script>     </body>   </html> `;}; 
5app.use(express.static('public'));app.get('*', (req, res) => {
6 res.send(renderer());});
7 
8app.listen(3000, () => {
9 console.log('Server gestartet auf http://localhost:3000');
10});

Neu ist hier die Methode renderer(). Darin nutzen wir für das SSR die Methode renderToString() von react-dom/server. Damit wird unsere App gerendert und als String ausgegeben. Unser HTML-Template lagern wir nicht wie zuvor in eine extra HTML-Datei aus, sondern definieren es hier mit Hilfe eines Template-Strings. Wir werden in den nächsten Abschnitten sehen, dass das sehr praktisch ist.

Die statischen Ressourcen liegen ab sofort im public-Ordner, weshalb wir app.use(express.static('public')); definiert haben.

Ein letztes Augenmerk will ich auf das

Beitrag teilen

Gefällt mir

3

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.