Zum Inhalt springen

Nuxt und serverseitiges Rendern

Eine SPA schickt dem Browser zuerst eine fast leere Seite und baut den Inhalt mit JavaScript auf. Beim serverseitigen Rendern, kurz SSR, rendert der Server dieselben Komponenten schon zu HTML. Der Browser zeigt den Inhalt sofort, und danach übernimmt dieselbe App im Browser die Seite. Dieser Schritt heisst Hydration. Vue bringt beides mit. createSSRApp erzeugt eine App für dieses Zusammenspiel, renderToString macht auf dem Server HTML daraus, und mount übernimmt dieses HTML im Browser. Nuxt ist ein Framework auf Vue, das SSR, Routing und Datenladen für ganze Projekte einrichtet.

SSR lohnt sich, wenn Inhalte schnell sichtbar sein müssen oder Suchmaschinen sie finden sollen, etwa bei einem öffentlichen Katalog oder Blog. Es kostet aber etwas. Die App braucht einen Server, der Node ausführt, statt nur Dateien auszuliefern, und Code, der im Browser läuft, muss auch auf dem Server laufen können. Für eine App hinter einer Anmeldung bringt SSR wenig. Wer nur einzelne Seiten vorab erzeugen will, kann sie beim Build als statische Seiten rendern lassen.

Das Beispiel unter optional/ssr/de rendert den Katalog mit Vue selbst, ohne Nuxt. app.js erzeugt die App mit createSSRApp. Die Funktion legt bei jedem Aufruf eine neue App an, denn auf dem Server teilen sich alle Anfragen denselben Prozess, und der Zustand einer Anfrage darf nie bei einer anderen landen.

app.js
// The same app for the server and for the browser. Every call creates a new app, so the state of
// one request never reaches another.
import { createSSRApp, ref } from 'vue'
export function createApp(things) {
return createSSRApp({
setup() {
const saved = ref(0)
return { things, saved }
},
template: `
<h1>Katalog</h1>
<p>Gemerkte Dinge {{ saved }}</p>
<ul>
<li v-for="thing in things" :key="thing.id">
{{ thing.name }}
<button type="button" @click="saved++">Merken</button>
</li>
</ul>
`,
})
}

renderToString macht aus der App einen Text mit HTML. render.js setzt ihn in eine ganze Seite, gibt dem Browser die Daten mit und lädt das Skript, das die Seite übernimmt. Die Kommentare <!--[--> im HTML markieren Fragmente, an denen sich die Hydration orientiert.

render.js
import { renderToString } from 'vue/server-renderer'
import { createApp } from './app.js'
// The whole page with the HTML of the app, the data for the browser and the script that takes over.
// \u003c keeps a name with </script> from ending the data block early.
export async function renderPage(things) {
const html = await renderToString(createApp(things))
const data = JSON.stringify(things).replaceAll('<', '\\u003c')
return `<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Katalog</title>
<script type="importmap">{ "imports": { "vue": "/vue.js" } }</script>
</head>
<body>
<div id="app">${html}</div>
<script type="application/json" id="things">${data}</script>
<script type="module" src="/client.js"></script>
</body>
</html>`
}

Im Browser ruft client.js dieselbe Funktion auf. Weil die App mit createSSRApp entsteht, zeichnet mount die Seite nicht neu, sondern hängt die Event-Listener an das vorhandene HTML. Erst danach reagiert der Knopf «Merken».

client.js
// In the browser the same app takes over the HTML of the server instead of drawing it again
import { createApp } from './app.js'
const things = JSON.parse(document.getElementById('things').textContent)
createApp(things).mount('#app')

Mit npm start liefert server.js die Seite unter http://localhost:3002 aus. Im Quelltext der Seite steht die Liste schon im HTML, ohne dass JavaScript gelaufen ist. Liefert der Server anderes HTML, als die App im Browser rendern würde, meldet Vue beim Hydrieren einen Mismatch. Das passiert etwa, wenn eine Komponente beim Rendern die aktuelle Uhrzeit oder eine Zufallszahl verwendet.

Nuxt richtet all das für ein ganzes Projekt ein. Die aktuelle Version ist Nuxt 4, ein neues Projekt legst du mit npm create nuxt@latest an. Seiten liegen als Komponenten im Ordner app/pages, und Nuxt macht aus den Dateinamen die Routen, statt dass du sie von Hand einträgst. Komponenten und Composables importiert Nuxt automatisch. useFetch lädt Daten auf dem Server und gibt sie der Seite mit, damit der Browser sie nicht noch einmal holt, und Dateien im Ordner server/api werden zu Routen einer API. Ob eine Seite auf dem Server, nur im Browser oder schon beim Build entsteht, legst du pro Route fest. Der Entwicklungsserver von Nuxt nimmt wie die Kurs-API den Port 3000, starte also nicht beide auf demselben Port.

Texte und Abbildungen stehen unter CC BY-SA 4.0, der Code unter der MIT-Lizenz und die Daten des Kurses unter CC0 1.0. Die Schrift der Abbildungen ist Kalam unter der SIL Open Font License 1.1. Die Quellen liegen auf GitHub.