Svelte – Ein Einblick
Weniger Boilerplate, mehr Produktivät
Svelte sorgt für einige Aufregung im Web-Frontend-Universum. In Performancevergleichen landet es mühelos auf den ersten Plätzen, die Bundlesize liegt meilenweit unter Konkurrenten wie React. Vor allem aber ist es ausdrucksstark: Minimaler Code führt zu maximalen Ergebnissen. Performance-Tuning durch den User mit Hilfe von Dingen wie shouldComponentUpdate? Nicht nötig. Boilerplate-Code, um eine Komponente zu erstellen? Nicht vorhanden. Gleichzeitig wird einiges bereitgestellt. Ein Animationspaket und eine State-Management-Lösung sind bereits enthalten. Wie schafft Svelte das? Darauf wollen wir in diesem Artikel einen Blick werfen. Gemeinsam machen wir die ersten Schritte mit Svelte und bauen eine Todo-Liste.
Svelte ist momentan schwer im Trend. Alle Hype-Metriken zeigen steil nach oben: Innerhalb eines Jahres ging es von 10.000 auf über 35.000 GitHub-Stars [1]. Die npm-installs sind auf 90.000 pro Woche angewachsen [2]. Im State of Javascript 2019 Survey gaben 45 Prozent der Befragten an, sich für Svelte zu interessieren, wodurch das Framework den Prediction Award für den heißesten Newcomer einheimste [3]. Woher kommt diese Begeisterung für das Framework?
Svelte – wirlich so neu?
Um das zu verstehen, lohnt sich ein Blick auf die Anfänge von Svelte. Das Framework ist nämlich gar nicht so neu, wie viele denken. Ende 2016 erschien die erste Version, geschrieben von Richard Harris [4], der zu diesem Zeitpunkt für The Guardian arbeitete und mittlerweile sein Geld bei der New York Times verdient. Seine Aufgabe ist das Erstellen von interaktiven Grafiken [5] – und Svelte entstand in diesem Kontext. Harris wollte ein Framework, welches es auch für JavaScript-Einsteiger einfach macht, schnell produktiv zu werden. Außerdem sollte das Ergebnis möglichst klein und schnell sein und flüssige Animationen ermöglichen – Anforderungen, die für eine bereits schwergewichtige Nachrichtenwebseite, die auch auf Low-Budget-Smartphones gelesen wird, essenziell sind. Da keines der existierenden Frameworks seine Anforderungen erfüllte, entwickelte Harris Svelte. Version 1 erfüllte zwei der Punkte – klein und schnell – bereits sehr gut. Doch erst mit Version 3, die im April 2019 veröffentlicht wurde, gelang auch der Durchbruch bei der Einfachheit. Den Svelte-Nutzern wurde einiges an Migrationsaufwand zugemutet, denn Version 3 hat mit den Vorgängern recht wenig zu tun. Es hat sich jedoch gelohnt, und in der Folge gewann Svelte stark an Popularität.
Ein Blick unter die Haube
Wie schafft es Svelte nun, so klein und schnell zu sein und gleichzeitig ohne Boilerplate-Code auszukommen? Das Geheimnis: Der Hauptteil der Arbeit geschieht zur Compile-Zeit, und nicht zur Laufzeit. React beispielsweise lädt eine Runtime und benötigt darüber hinaus den Virtual DOM, um lauffähig zu sein. Svelte dagegen generiert aus dem Entwickler-Code imperativen Laufzeit-Code, der nur noch eine minimale Runtime und ein paar Helfermethoden für weniger Codeduplikation enthält. Weil der Compiler und die Runtime sehr modular aufgebaut sind, können zudem jegliche nicht benötigte Features aus dem Kompilat entfernt werden. So schafft es ein minimales Hello-World-Beispiel auf sagenhafte 2,5 Kilobyte – vor Browserkomprimierung.
Das Schreiben von Svelte-Komponenten geht schnell in Fleisch und Blut über.
Das ist auch der Grund, warum die Performance so gut ist. Svelte weiß durch den Compiler, was sich wann ändern kann. Der Compiler fügt also an den entsprechenden Stellen imperative Statements ein, die die Variable der Komponente als veraltet markieren, sobald sie verändert wird. Mit dem nächsten Browser Repaint werden alle als veraltet markierten Komponenten und Variablen abgearbeitet und aktualisiert. Aus diesem Grund kann sich Svelte auch Performancekrücken wie shouldComponentUpdate sparen, was den Code zugleich einfacher macht. Im Vergleich dazu führt React nach jedem Update einen Diff auf dem gesamten Virtual DOM aus und rendert das neu, was sich geändert hat – das ist deutlich kostenintensiver.
Dass Svelte die Hauptarbeit zur Compilezeit erledigt, ermöglicht darüber hinaus auch große Fortschritte in der Developer Experience. Das Leitziel: Mache es dem Entwickler so einfach wie möglich, Code zu schreiben und entferne jegliche unnötige Syntax. Das Ergebnis ist beeindruckend und gerade zu Anfang ist man immer wieder überrascht, was Svelte alles automagisch herausfindet. Anfangs mag manche Syntax – auch aus diesem Grund – gewöhnungsbedürftig sein, doch geht das Schreiben von Svelte-Komponenten schnell in Fleisch und Blut über.
Wie sieht es nun aus, eine Anwendung in Svelte zu schreiben? Das wollen wir uns im Folgenden anhand einer Todo-App anschauen.
Todo-App – Liste anlegen
Wir beginnen damit, eine statische Liste anzulegen.
Listing 1: Statische Todo-Liste
<script>
let todos = ['Svelte lernen', 'Begeistert sein'];
</script>
<ul>
{#each todos as todo}
<li>{todo}</li>
{/each}
</ul>
Wie man sieht, unterscheidet sich eine Svelte-Komponente auf den ersten Blick nur unwesentlich von einer normalen HTML-Datei. Im script-Tag schreiben wir Javascript-Code, darunter folgt HTML-Code, erweitert um Svelte-Syntax. Diese kennzeichnet sich durch Moustache-Tags ({ oder }). Im obigen Beispiel sehen wir die Verwendung einer For-Schleife. Diese beginnt mit {#each und schließt mit {/each}, ihr Inhalt wird wiederholt gerendert. In diesem Fall ist dies ein Listenelement, welches den Wert der Listenelementvariable todo als String rendert.
Was wir ebenfalls sehen – oder eben nicht sehen –, ist jegliche Form von Boilerplate-Code. Wo Angular mit Standardeinstellungen schon bei drei Dateien und an die zehn Zeilen Code ist, bevor es überhaupt losgehen kann, startet Svelte einfach direkt mit dem Code, der wirklich relevant ist.
Als nächstes setzen wir das Anlegen von Todos um. Dazu fügen wir ein input-Element hinzu, auf dessen Eingaben wir reagieren.
Listing 2: Simple Todo-App
<script>
const ENTER_KEY = 13;
let todos = [];
let newTodo = '';
function addTodo(evt) {
if (evt.which !== ENTER_KEY) {
return;
}
todos = [...todos, newTodo];
newTodo = '';
}
</script>
<label>
<p>Add Todo</p>
<input bind:value={newTodo} on:keydown={addTodo} />
</label>
<ul>
{#each todos as todo}
<li>{todo}</li>
{/each}
</ul>
Neu hinzugekommen ist zunächst die Variable newTodo. Sie ist durch den Ausdruck bind:value={newTodo} mit dem input-Element verknüpft. Dies ist ein Two-Way-Binding: Ändert sich die Variable durch den Code, wird der Wert des input-Elements neu gerendert, umgekehrt führen Nutzereingaben zu einem Update der Variable.
Die Funktion addTodo verwendet die neue Variable, um bei einem Druck auf die Enter-Taste die Todo-Liste um den neuen Eintrag zu erweitern und leert das Input danach. Hierbei sehen wir schön die reduzierte Syntax von Svelte: Es ist kein explizites Update in Form von setState oder ähnlichem nötig, der Compiler erkennt unseren Änderungswunsch und wird Liste und Input aktualisieren. Aufgerufen wird die Methode durch das on:keydown={addTodo} auf dem input-Element. Generell werden in Svelte mit on:<eventname> Event-Listener an Elemente gebunden.
Komponenten und ihre Interaktion
Bis jetzt können wir Todos erstellen, aber nicht abhaken. Wir müssen unser Listenelement um eine entsprechende Funktion erweitern. Dies ist eine gute Gelegenheit, das Listenelement in eine eigene Komponente auszulagern. Diese sieht zunächst sehr simpel aus:
Listing 3: Todo-Komponente
<script>
export let todo;
</script>
{todo}
Hier sehen wir, wie wir Inputs (Angular-Sprech) bzw. Props (React-Sprech) auf Komponenten definieren. Mit export let todo geben wir an, dass die Komponente eine Property mit dem Namen todo von außen akzeptiert. Die Verwendung in unserer Hauptkomponente sieht dann folgendermaßen aus:
Listing 4: Todo-App verwenden Unterkomponente
<script>
import Todo from './Todo.svelte';
// … restlicher Code bleibt gleich
</script>
<!-- … label und input bleiben gleich -->
<ul>
{#each todos as todo}
<li><Todo todo={todo} /></li>
{/each}
</ul>
Das Einfügen der Komponente ist denkbar einfach: Wir importieren sie als default import und nutzen den Namen als Tag. Das todo-Property geben wir über todo={todo} hinein. Alternativ können wir auch die Kurzschreibweise {todo} verwenden, da die Variable genauso wie das Input-Property heißt.
Events für Komponenten definieren
Als Nächstes erweitern wir die Todo-Komponente, sodass wir Todos abhaken können. Dazu erweitern wir zunächst unser Todo-Datenmodell von einem simplen String auf ein Objekt der Form {id: string; done: boolean; name: string;}. In unserer Todo-Komponente fügen wir eine Checkbox hinzu. Wird die Checkbox angeklickt, feuern wir ein Event ab. Dazu nutzen wir als erstes createEventDispatcher, um einen Dispatcher zu kreieren. Den Dispatcher rufen wir als erstes Argument mit einem String auf, der den Eventnamen angibt, das zweite Argument ist der Inhalt des Events.
Listing 5: Event auf Komponente definieren
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let todo;
function todoChange() {
dispatch('todoChange', {...todo, done: !todo.done});
}
</script>
<input type="checkbox" checked={todo.done} on:input={todoChange} />
<span>{todo.name}</span>
In der Vaterkomponente hören wir auf das Event wie auf andere Events auch mit on:<eventName>, in diesem Fall also on:todoChange. Der Eventparameter wird in event.detail mitgegeben.
Listing 6: Auf Komponentenevent reagieren
<script>
// …
function changeTodo(evt) {
const updatedTodo = evt.detail;
todos = todos.map(todo => todo.id === id ? updatedTodo : todo);
}
</script>
<!-- … -->
<ul>
{#each todos as todo}
<li>
<Todo {todo} on:todoChange={changeTodo} />
</li>
{/each}
</ul>
An dieser Stelle sei erwähnt, dass wir alternativ wie in React mit Callback-Props arbeiten können, also eine Funktion als Input-Property hineingeben können, die dann aufgerufen wird. Welche Variante genutzt wird, ist letztlich vor allem persönlicher Geschmack.
State-Management mit dem Store-Modul
Um unsere Daten zu persistieren, nutzen wir der Einfachheit halber den localStorage. Die Zustandsverwaltung und -persistierung soll aber nicht länger Teil der Komponente sein, sondern ausgelagert werden. Hierfür bietet sich das store-Modul von Svelte an, welches im Core bereits enthalten ist.
Teil des store-Moduls ist die Funktion writable, die wir verwenden werden. Ein writable bietet zur Manipulation die Methoden set und update und zum Lesen die Methode subscribe. Mit diesem Grundbaustein können wir uns sehr einfach unseren eigenen globalen Zustandsverwalter bauen:
Listing 7: Todo-Store
import { writable } from 'svelte/store';
let _todos = JSON.parse(localStorage.getItem('todos')) || [];
const todoStore = writable(_todos);
function updateTodoStore() {
todoStore.set(_todos);
localStorage.setItem('todos', JSON.stringify(_todos));
}
function addTodo(newTodo) {
_todos = [..._todos, {id: Math.random(), name: newTodo, done: false}];
updateTodoStore();
}
function changeTodo(updatedTodo) {
_todos = _todos.map(todo => todo.id === updatedTodo.id ? updatedTodo : todo);
updateTodoStore();
}
export const todos = {
addTodo,
changeDone,
subscribe: todoStore.subscribe
};
Die subscribe-Methode stellen wir öffentlich zur Verfügung, die Methoden zum Aktualisieren des writables verstecken wir hinter fachlichen Methoden und bieten so eine saubere API an.
Neben writable bietet das Store-Modul noch die Funktion readable, mit dem der Zustand nur selbst von innen gesetzt werden kann, und derived, was für die Komposition von Stores genutzt werden kann. Mit diesen einfachen Bausteinen lassen sich mächtige Zustandsverwalter bauen.
Das Ganze einzubinden, ist denkbar einfach. Wir importieren den todos-Store und delegieren die Todo-Listenverwaltung an ihn.
Listing 8: Nutzung des Todo-Store
<script>
import Todo from './Todo.svelte';
import { todos } from './todo-store';
const ENTER_KEY = 13;
let newTodo = '';
function addTodo(evt) {
if (evt.which !== ENTER_KEY) {
return;
}
todos.addTodo(newTodo);
newTodo = '';
}
function changeTodo(evt) {
todos.changeTodo(evt.detail);
}
</script>
<label>
<p>Add Todo</p>
<input bind:value={newTodo} on:keydown={addTodo} />
</label>
<ul>
{#each $todos as todo}
<li>
<Todo {todo} on:todoChange={changeTodo} />
</li>
{/each}
</ul>
Wer im obigen Code nun nach einem umständlichen subscribe-unsubscribe-Pattern sucht, um auf Änderungen der Liste zu reagieren, sucht vergeblich. Stattdessen schreiben wir einfach $todos – der Compiler erkennt, dass es eine entsprechende Variable mit dem Namen todos gibt, welche eine subscribe-Methode besitzt, und kann den Code für uns generieren. Hieran zeigt sich, wie sehr bei der Entwicklung von Svelte auf eine gute Developer-Experience geachtet wurde, und dass ein Compiler Dinge vereinfachen oder erst möglich machen kann, an die man vorher gar nicht denken konnte.
Die Verwendung der $-Syntax zum Subscriben auf Änderungen funktioniert übrigens nicht nur mit Sveltes store-Modul. Generell gilt: Alles, was dem subscribe-unsubscribe-Pattern folgt, kann mit der $-Syntax genutzt werden. Das heißt also, dass RxJS-Observables genauso einfach genutzt werden können, was in der RxJS-Community große Begeisterung ausgelöst hat.
Abgeleitete Werte – Anzahl erledigter Todos anzeigen
Als Nächstes möchten wir unter der Liste die Anzahl erledigter Todos anzeigen. Da diese Zahl aus der Liste ableitbar ist, soll hierfür kein neuer Zustand eingeführt werden. Hierfür nutzen wir Reactive Declarations.
Listing 9: Anzahl erledigter Todos ableiten
<script>
import { todos } from './todo-store';
// …
$: nrOfDoneTodos = $todos.filter(todo => todo.done).length;
// …
</script>
<!-- … -->
<p>Done: {nrOfDoneTodos}</p>
Reactive Declarations werden durch ein $: zu Beginn der Zeile gekennzeichnet. Wir sagen dem Compiler damit, dass er die Variable jedes Mal aktualisieren soll, wenn sich eine der abhängigen Variablen geändert hat. In diesem Fall wird also bei jedem Update am todo-Store der Zähler neu berechnet. Wer Vue kennt, kann Reactive Declarations am ehesten mit computed vergleichen – nur dass Svelte, wie jetzt schon mehrmals zu sehen, mit weniger Boilerplate-Code auskommt.
Schöner wohnen – Styles und Transitions
Zu guter Letzt wollen wir unsere Todo-App optisch verschönern. Zunächst stylen wir dafür die li- und ul-Elemente.
Listing 10: Komponentenstyles
<script>
// …
</script>
<!-- … -->
<style>
ul {
padding: 10px;
}
li {
list-style: none;
}
</style>
Wir stylen die Elemente anhand ihres Tags. Über ein Überlappen mit anderen Styles müssen wir uns aber keine Gedanken machen: Alle Styles gelten nur für die gegebene Komponente. Nur wenn die Selektoren mit :global(…) umschlossen sind, gelten sie über die Komponente hinaus.
Zum Abschluss nutzen wir die transition-Direktive auf dem li-Tag, um ein neues Todo mit einem Fade-in-Effekt erscheinen zu lassen.
Listing 11: Transitions
<script>
import { fade } from 'svelte/transition';
// …
</script>
<!-- … -->
<ul>
{#each $todos as todo}
<li transition:fade>
<Todo {todo} on:todoChange={changeTodo} />
</li>
{/each}
</ul>
<style>
// …
</style>
Die transition-Direktive ermöglicht es uns, sehr einfach Übergänge zu definieren. Vorgefertigte Übergänge wie fade werden von Svelte bereitgestellt, eigene zu schreiben ist nicht schwer. Für allgemeinere Fälle gibt es die animation-Direktive, die ebenfalls mit vorgefertigten Animationen wie flip aufwartet. Die Animationen werden, wenn möglich, komplett über CSS gesteuert und nur wenn nicht anders möglich über Javascript ausgeführt.
Hier zeigt sich der Arbeitseinfluss von Richard Harris: Als Redakteur für interaktive Grafiken sind Animationen essenziell, weshalb sie auch in Svelte ein Core-Feature sind.
Ist Svelte "Production-ready"?
Durch die Todo-App haben wir gesehen, wie erfrischend einfach es ist, mit Svelte Applikationen zu schreiben. Echte Webseiten und Web-Apps sind natürlich weitaus komplexer und umfangreicher – kann Svelte auch hier noch liefern?
Zunächst sei gesagt, dass die Todo-App natürlich nur einen kleinen Teil des Feature-Umfangs von Svelte abdeckt. Darüber hinaus gibt es unter anderem Lifecycle-Methoden, eine Context-API, um Komponenten über verschiedene Level hinweg direkt miteinander kommunizieren zu lassen, und Actions, die an DOM-Elemente gehängt werden können und ähnlich wie Angular-Direktiven hervorragende Möglichkeiten für die Wiederverwendung bieten. Wer sich einen Schnellüberblick verschaffen will, dem sei das Tutorial auf der offiziellen Webseite ans Herz gelegt [6]. Darüber hinaus arbeitet die Community aktiv an Sapper – Kurzschreibweise für Svelte App Maker –, welches Server Side Rendering, PWA-Unterstützung und einen Router mitliefert.
Der Featureumfang ist also ausreichend, doch wie sieht es mit typischen Managerfragen wie der erwarteten Lebenszeit des Frameworks und dem Support einer großen Firma aus? Da Svelte Version 3 noch recht neu ist und sich zunehmender Beliebtheit erfreut, dürfte es die nächsten Jahre sicher aktiv weiterentwickelt werden. Eine Firma mit entsprechenden Ressourcen steht allerdings nicht hinter Svelte. Dass das kein Nachteil sein muss, hat aber bereits VueJS bewiesen.
Wie sieht es mit der Nutzung von Sprachen wie TypeScript und Less/SCSS aus? Hier kann Svelte mittlerweile liefern. Es gibt einen offiziellen Präprozessor, der viele gängige Sprachen unterstützt. Editorunterstützung ist ebenfalls vorhanden. Die Community arbeitet aktiv an einem sogenannten Language-Server, welcher in allen IDEs, die das Language Server Protocol unterstützen, genutzt werden kann [7]. Die entsprechenden Extensions stehen unter anderem für VSCode ("Svelte for VSCode") und Vim ("coc-svelte") bereit. Für IntelliJ/WebStorm muss eine separate Lösung implementiert werden, doch auch diese wird aktiv entwickelt und seit neuestem sogar offiziell von Jetbrains unterstützt [8].
Einzig was das Ökosystem und die Community angeht, gibt es noch Nachholbedarf. Das ist aber auch allzu verständlich – eine Art Henne-Ei-Problem, mit dem alle aufstrebenden Frameworks zu kämpfen haben: Viele Entwickler sind aufgrund des Ökosystems noch zögerlich, weshalb sich dieses nur schwer vergrößern kann. Dennoch spricht der Trend klar für Svelte. Tutorials schießen momentan wie Pilze aus dem Boden, und auch die Libraries werden stetig mehr. Natürlich sind diese noch nicht in einem Reifegrad wie bei vergleichbaren Frameworks. So gibt es noch keinen Platzhirsch bei einer Formular- oder Routing-Library – Dinge, die Svelte (noch) nicht im Core mitliefert.
Svelte bietet alles, um skalierbare Anwendungen zu bauen.
Zusammengefasst gibt es kaum Einwände, das nächste kleine bis mittelgroße Projekt einmal mit Svelte zu wagen. Große Projekte mit einer Laufzeit von vielen Jahren werden aufgrund der Managementaspekte wahrscheinlich eher noch abwarten.
Ausprobieren!
Wie wir gesehen haben, ist Svelte nicht ohne Grund der neue Stern am Web-Frontend-Himmel. Es übernimmt bewährte Konzepte aus der React/VueJS/Angular-Welt und schafft es gleichzeitig, den Boilerplate-Code auf ein Minimum zu reduzieren. Code mit Svelte zu schreiben, ist erfrischend reduziert und simpel, ohne einzuschränken. Die Hauptarbeit leistet das Framework während des Kompilierens und erzeugt so performante Anwendungen, die gegenüber anderen Frameworks erstaunlich klein sind – die Beispiel-Todo-App kommt auf nicht einmal 9 Kilobyte. Außerdem ermöglicht es der Compiler, Syntax-Konstrukte einzuführen, die den Code entschlacken – Entwickler können sich so auf das Wesentliche konzentrieren.
Allerdings ist Svelte hinsichtlich Ökosystem und Popularität noch am Anfang, wenn auch mit riesigem Potenzial. Deshalb ist ein genauer Blick lohnenswert, auch für das nächste Projekt inhouse oder beim Kunden. Denn Svelte bietet alles, um skalierbare Anwendungen zu bauen. Daher: Ausprobieren!
M. Sezgin Ruhi
am 11.11.2020Es ist verlinkt auf "http://http//"
Informatik Aktuell
am 11.11.2020