Moderne Svelte-Apps bauen mit SvelteKit

04.08.2021 Tom Trapp
Mobile svelte sveltekit javascript frontend framework handson tutorial howto

SvelteKit?

Nachdem wir Svelte als Frontend-Compiler bereits im Juni kennengelernt haben wollen wir nun SvelteKit genauer unter die Lupe nehmen. SvelteKit ist ein vollwertiges Frontend-Framework und eine Erweiterung für Svelte, welches den Svelte-Compiler nutzt um die Grundlage für das Bauen von modernen Apps zu bieten. Das Kit selbst ist sehr neu und seit März 2021 in einer public Beta verfügbar und aktuell auf der Road to 1.0.0. Der Vorgänger Sapper selbst wird nicht mehr wirklich weiterentwickelt und Nutzer sollten auf SvelteKit umsteigen.

Unterschied zu Svelte

SvelteKit baut auf Svelte auf. Svelte alleine ist lediglich ein Frontend-Compiler. Genauer gesagt liefert Svelte seinen eigenen JavaScript-/TypeScript-Compiler aus welcher zur Build-Time das Client-seitige JavaScript generiert. Dieses kompilierte JavaScript ist darauf spezialisiert den DOM so anzupassen, dass so wenig wie möglich an Daten zwischen Server und Client ausgetauscht werden müssen. Dies hat zahlreiche Benefits für die User Experience. Genaueres dazu finden Sie in vorhin genannten ersten Beitrag zu Svelte.

Das Kit hingegen fungiert somit als vollwertiges App-Framework. Dies erlaubt es uns, alle modernen Best-Practises wie Server-Side Rendering (kurz SSR), Routing usw. zu implementieren.

Setup

Ähnlich wie bei unserer Svelte To Do List biete auch SvelteKit uns eine Command zum Erstellen neuer Projekte an. Nun wollen wir SvelteKit kennenlernen und nutzen dafür eine Skeleton-App mit TypeScript, ESLint und Prettier.

npm init svelte@next b-nova-todo-list-sveltekit
cd b-nova-todo-list-sveltekit
npm install
npm run dev -- --open

Nachdem wir nun die SvelteKit App erfolgreich gestartet haben wollen wir diese via code . öffnen (vorrausgesetzt Sie haben VS Code bereits installiert) und genauer unter die Lupe nehmen.

Hot Code Replacement

Ein klares Highlight von SvelteKit ist das HotCode- oder HotModule-Replacement. Dies erlaubt es uns, Code, Markup und CSS zur Laufzeit zu ändern, ohne die Page erneut laden zu müssen. Die Updates werden unglaublich schnell gemacht und ohne, dass die Page den State, die Werte der Variablen usw. verliert. Sehr cool wie ich finde! 🚀

Routing

SvelteKit nutzt ein sogenanntes Filesystem-based Routing um Pages und Urls für den Benutzer zu definieren und zu implementieren. Dies bedeutet, dass die Dateistruktur die Struktur der eigentlichen Webpages spiegelt. Im Gegensatz zu Svelte ist die Ordnerstruktur unter src leicht anders, beim Kit gibt es der Folder routes. Darin liegen unterschiedliche Svelte Pages, der Dateinamen symbolisiert hier die aufrufbare Url.

Will man eine Sub-URL definieren, so kann man unter Routes einen Ordner erstellen und dort weitere Svelte Pages anlegen.

Die erste eigene Route

Nun wollen wir eine neue Page anlegen, wo später unsere ToDo-Liste platziert werden soll. Hierfür erstellen wir eine Datei mit dem Namen todos.svelte unter /src/routes.

<!-- src/routes/todos.svelte -->

<h2>To Dos</h2>

Nach dem Speichern ist unsere neue Page direkt unter http://localhost:3000/todos erreichbar.

Routes mit Parametern

Oft kommt es vor, dass Urls bestimmte Parameter beinhalten, SvelteKit bietet auch hier eine Möglichkeit solche Placeholder zu definieren. Wir wollen ein ToDo.Item mit einer ID direkt über eine URL wie z. B. /todos/abc aufrufen können.

Hierfür müssen wir unter routes/ zuerst einen todos/-Ordner anlegen, darin können wir dann unsere Page anlegen:

<!-- src/routes/todos/[id].svelte -->

Hello there!

Die eckigen Klammern symbolisieren hier einen Parameter der später in einer sogenannten load()-Funktion wieder entgegengenommen werden kann. Hier ist es auch möglich, mehrere Parameter wie z. B. [id]-[number].svelte in einer Route entgegenzunehmen.

So sieht unsere URL- und Routes-Struktur nun wie folgt aus: b-nova SvelteKit Routing

Navbar

Nun brauchen wir aber noch eine Navigation, diese wollen wir als dezentrale Komponente anlegen. Hierfür legen wir einen components/-Ordner an und legen eine Svelte Komponente an:

<!-- src/components/nav.svelte -->

<nav>
    <a href="/">Home</a>
    <a href="/todos">To Dos</a>
</nav>

Diese Komponente könnten wir nun in jeder Page einzeln einbinden, SvelteKit bietet uns hier aber eine elegantere Lösung.

Layout

Der Aufbau einer Webpage ist, unabhängig vom Framework oder der eingesetzten Technologie meist gleich, es gibt einen Header, einen Body und einen Footer. Unsere Pages (oder auch Routes genannt) sollen immer nur den Body beinhalten, die Komponente Header & Footer sollten immer automatisch eingebunden werden.

Hierfür legen wir ein __layout.svelte-File unter /src/routes/ an. Mit dem <slot />-Platzhalter können wir eine Stelle für den eigentlichen Inhalt der Pages definieren.

<!-- src/routes/__layout.svelte -->

<script>
    import Nav from "/src/components/nav.svelte";
</script>
<h1>b-nova To Do List</h1>

<Nav/>
<slot />

<footer>
    <a href="https://b-nova.com" target="_blank">b-nova.com</a>
</footer>

Zusätzlich bietet das Kit hier auch die Möglichkeit solche Layouts zu verschachteln.

Customizing

Nun wollen wir beispielsweise Header-Informationen wir den Title- oder bestimmte SEO-Daten anpassen, auch hierfür bietet das Kit uns eine elegante Lösung. Wir können mit der svelte:head Direktive bestimmte Metadaten im Header überschreiben / anpassen:

<!-- src/routes/index.svelte -->

<svelte:head>
    <title>b-nova To Do list</title>
</svelte:head>
<h2>Welcome to the b-nova SvelteKit To Do List</h2>
<!-- src/routes/todos.svelte -->

<svelte:head>
  <title>b-nova ToDos</title>
</svelte:head>
<h1>To Dos</h1>

Hier ist nun zu sehen, dass ohne echte Page-Refresh die Meta-Informationen der Seite angepasst werden können. So wird unsere SvelteKit Applikation SEO-friendly.

Loading data

In einer normale Frontend-Applikation ist das Laden von dynamischen Daten meist tief in den Komponenten eingebunden. SvelteKit wählt hier mit der load()-Function einen ähnlichen Ansatz wie Angular mit dem OnLoad-Guard. Diese Funktion kann in jeder Page Komponente implementiert werden und wird von dem Rendern der Komponente ausgeführt.

Mit einer exemplarischen Rest API wollen wir Daten über ToDos abfragen und anzeigen. Hierfür implementieren wir im module-context in unserer ToDos-Page die load()-Funktion:

<!-- src/routes/todos.svelte -->

<script context="module">
	export async function load({ page, fetch, session, context }) {
		console.log('Loading ToDos');
		const url = `https://jsonplaceholder.typicode.com/todos`;
		const res = await fetch(url);

		if (res.ok) {
			return {
				props: {
					todos: await res.json()
				}
			};
		}

		return {
			status: res.status,
			error: new Error(`Could not load ${url}`)
		};
	}
</script>

<script lang="ts">
	export let todos: any[];
</script>

<svelte:head>
	<title>b-nova ToDos</title>
</svelte:head>
<h1>To Dos</h1>

<ul>
	{#each todos as todo}
		<li>
			<a href="/todos/{todo.id}">
				{todo.id}: {todo.title}
			</a>
		</li>
	{/each}
</ul>

Hier ist schön zu sehen, dass wir das Resultat aus der load()-Funktion als Parameter in der eigentlichen Komponente entgegennehmen und dort dann nutzen können.

Das Gleiche wollen wir nun in unserer parametrisierten Route für jedes ToDo-Item einzeln machen.

<!-- src/routes/todos/[id].svelte -->
<script context="module">
	export async function load({ page, fetch, session, context }) {
		const id = page.params.id;
		console.log('Loading To Do ' + id);
		const url = `https://jsonplaceholder.typicode.com/todos/${id}`;
		const res = await fetch(url);

		if (res.ok) {
			return {
				props: {
					todo: await res.json()
				}
			};
		}

		return {
			status: res.status,
			error: new Error(`Could not load ${url}`)
		};
	}
</script>

<script lang="ts">
	export let todo;
</script>

title: {todo.title}

Der grosse Vorteil dieser Struktur ist, dass die load()-Funktion sowohl auf Client- als auch auf Server-Seite ausgeführt werden kann.

Rendering & Adapters

SvelteKit unterstützt Server-side Rendering. Auf den ersten Blick klingt es logisch, wir wollen aber hinter die Facade sehen und die Unterschiede zu client-side Rendering und anderen Arten beleuchten.

Schauen wir uns zuerst das bekannte Client-Side Rendering (kurz CSR) genauer an. Der Browser bekommt vom Server eine fast leere HTML-Page, muss das komplette JavaScript herunterladen, ausführen und kann dann die Daten asynchron nachladen und die Page anzeigen. Der Moment, indem den Kunden etwas angezeigt wird, ist derselbe, indem die Page vollumfänglich benutzbar ist und der Kunde interagieren kann. Diese Methode hat den grossen Vorteil, dass die Rechenleistung auf den Client übertragen wird und kein Server benötigt wird. Hat der Kunde eine langsame Internetleitung oder gar zu wenig Rechenleistung zur Verfügung kann dies schnell zu Problemen führen.

Das Server-Side Rendering (kurz SSR) hingegen liefert ein fertig gerendertes, anzeigbares HTML dem Browser aus. Sämtliche Abfragen von dynamischen Daten usw. wird auf dem Server gemacht. Der Browser kann dem Kunden direkt die Page anzeigen und anschliessend das JavaScript-Framework laden, um die Page interactive zu machen. Dieser Prozess nennt man Hydration.

Hier gibt es je nach Implementation Unterschiede, meist wird das weitere Rendering dann aber vom Frontend übernommen, da das Framework geladen ist. Hierfür wird auch oft der Name Universal Rendering verwendet. Dies hat den grossen Vorteil, dass die Zeit zur Anzeige der Page kürzer wird. Selbstverständlich ist es ein Nachteil, das man hier einen Server fürs Rendering benötigt. SvelteKit hat SSR per Default aktiviert.

SvelteKit unterstützt noch zwei andere Arten von Rendering: Pre-Rendering und Static Rendering. Beim Static Rendering wird die gesamte Applikation beim Build Prozess gerendert und als statisches Artefakt ausgeliefert. Beim Pre-Rendering dagegen werden nur einzelne Pages statisch gebaut und so ausgeliefert. Der Youtuber Franck Abgrall hat auf seinen Kanal animierte, kurze Videos, welche diese Prozesse anschaulich darstellen.

Prefetching

SvelteKit unterstützt ein sogenanntes PreFetching, was sich in der Praxis als sehr hilfreich herausstellt. Man kann Anker Tags mit sveltekit:prefetch annotieren, so wird die Load Funktion der ZielUrl beim Hover oder direkt beim Click auf Mobile aufgerufen. Dies funktioniert natürlich nur mit internen Links zu anderen Svelte Pages, sorgt aber für einen noch schnellere und flüssigere UX für den Kunden.

So würde unser To Do Detail Link beispielsweise wie folgt aussehen:

<!-- src/routes/todos.svelte -->

<a href="/todos/{todo.id}" sveltekit:prefetch>
    {todo.id}: {todo.title}
</a>

Adapters

In SvelteKit benutzt man einen sogenannten Adapter, um das Build & Deployment Verhalten der SvelteKit App zu steuern. Mit dem @sveltejs/adapter-static wird die komplette Page beim Build-Vorgang als statische HTML Dateien gebaut. Hierzu muss man das svelte.config.js File entsprechend anpassen:

<!-- svelte.config.js -->
import preprocess from 'svelte-preprocess';
import adapter from '@sveltejs/adapter-static';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	// Consult https://github.com/sveltejs/svelte-preprocess
	// for more information about preprocessors
	preprocess: preprocess(),

	kit: {
		// hydrate the <div id="svelte"> element in src/app.html
		target: '#svelte',
		adapter: adapter()
	}
};

export default config;

Alle Pages werden nacheinander aufgerufen und weiterführende Links werden ebenfalls inkl. Abfrage von Rest API usw. abgerufen und gebaut. Beim Pre-Rendering hingegen werden nur einzelne Pages der Applikation zur Build-Zeit gerendert, dies kann pro Pages konfiguriert werden.

Ausserdem gibt es noch weitere Adapter welche mit Cloud-Providern o. ä. kompatibel sind, z. B. der Netlify Adapter. Für normale Deployment bietet sich der Node Adapter an, welcher einen eigenständigen Node Server mit baut und ausliefert.

Selbstverständlich finden Sie auch dieses TechUp auf GitHub.

Fazit

Svelte + SvelteKit = Big Love ❤️ build awesome webapps for app sizes!

Nachdem das TechUp für Svelte an sich schon auf grossen Anklang und Interesse gestossen ist legt SvelteKit noch eine Schippe drauf. SvelteKit schliesst die Lücken, welche Svelte aufgrund seiner Grösse nicht schliessen kann oder will. Durch die Erweiterung mit dem Kit wird die Svelte App ready für jede Herausforderung.

Sie benötigen eine zukunftssichere Web-Applikation mit Svelte inkl. einem Backend beispielsweise in Go? Kontaktieren Sie uns!

Tom Trapp - problem solver, innovator, athlete. Tom prefers to work on modern software all day long and attaches great importance to objectively clean, lean code.