1. Svelte 04.04.2024

1.1. Einleitung

Um Svelte als Web Framework zu testen, wurde eine ToDo-Listen-Management App geschaffen. Die Svelte Anwendung verwendet eine REST API, um ToDo-Listen abzurufen, anzulegen und zu verändern. Im Ersten Schritt soll sich nur auf die Svelte Anwendung konzentriert werden, die REST API soll vorerst als gegebne betrachtet werden.

1.2. Entwicklungs-Setup

1.2.1. Vorbedingungen

1.2.1.1. Backend

1.2.1.1.1. Docker

Installation per apt Package Manager (Advanced Packaging-Tool)

  1. alte Pakete deinstallieren
    for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done

  2. Docker apt Repository registrieren

    # Add Docker's official GPG key:
    sudo apt-get update
    sudo apt-get install ca-certificates curl
    sudo install -m 0755 -d /etc/apt/keyrings
    sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
    sudo chmod a+r /etc/apt/keyrings/docker.asc
    
    # Add the repository to Apt sources:
    echo \
    "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
    $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
    sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    sudo apt-get update
    
  3. Docker installieren
    sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

  4. (optional) Installation prüfen durch Ausführen des Hello-World images
    sudo docker run hello-world

1.2.1.2. Frontend

1.2.1.2.1. yarn
  1. Node.js installieren (per apt Package Manager)
    (getestet unter Ubuntu, siehe hier für andere Linux Distributionen)

    curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - &&\
    sudo apt-get install -y nodejs
    
  2. Yarn Feature Flag aktivieren:

    corepack enable
    
  3. Prüfen, ob die Installation erfolgreich war:

    1. yarn --version ausführen => Version sollte 1.x.x sein

    2. zu <repository root>/todo-app/packages/todo-app/ navigieren

    3. yarn --version ausführen => Version sollte 4.x.x sein

1.2.2. Projekt initialisieren und Abhängigkeiten installieren

  1. zu <repository root>/todo-app/ navigieren

  2. yarn
    

    ausführen

1.2.3. Starten der Anwendung

Der Stand der reinen Svelte Anwendung mit .NET Backend befindet sich auf dem Git branch svelte-dotnet!

  1. zu <repository root>/todo-app/ navigieren

  2. yarn be:docker:create-image
    

    ausführen

  3. yarn be:docker:run
    

    ausführen

  4. yarn fe:build
    

    ausführen

  5. yarn fe:run
    

    ausführen

  6. Anwendung öffnen http://localhost:4173/

Das .NET Backend kann mit yarn be:docker:stop wieder gestoppt werden.

1.2.4. Erläuterungen

  • yarn (alias für yarn install) installiert alle npm Pakete, die als Abhängigkeiten der Svelte Anwendung gelistet sind. Außerdem wird das Projekt initialisiert, sodass yarn scripte ausgeführt werden können.

  • yarn be:docker:create-image baut die .NET API in einem Docker Container und erstellt ein lokales Docker Image dva/todo-api

  • yarn be:docker:run verwendet Docker compose, um

    • einen todo-api Container zu starten

    • einen postgres Container zu starten

  • yarn fe:build baut die Svelte Anwendung

  • yarn fe:run startet die Svelte Anwendung

1.3. Grundlegende Eigenschaften von Svelte

Svelte ist ein freies modernes JavaScript-Framework, welches Entwicklern ermöglicht, deklarative Single-Page-Webanwendungen zu erstellen. Im Gegensatz zu Frameworks wie React oder Vue.js werden bei Svelte-Applikationen keine frameworkspezifischen Module zurückgegeben. Der Code wird zur Übersetzungszeit vom Svelte-Compiler in JavaScript-Code übersetzt. Damit ist er von keinen externen Programmbibliotheken abhängig.

1.3.1. Erstellen eines Svelte Projekts

Ein neues Svelte Projekt kann mit folgendem Command erzeugt werden:

yarn create vite <my-svelte-app> --template svelte-ts

wobei my-svelte-app der Name für die zu erzeugende Anwendung ist.

1.3.2. Aufbau einer Svelte-Komponente

Im folgenden Codebeispiel sieht man den grundlegenden Aufbau einer Svelte-Komponente. Diese besteht meist aus 3 Teilen:

  • JavaScript (<script></script>)

  • Markup

  • Styling (<style></style>)

Im JavaScript-Teil können unter anderem Component Properties definiert werden (export let foo). Diese Properties können beim Verwenden der folgenden FooBar-Komponente wie folgt festgelegt werden: <FooBar foo="bar"/>

In diesem Fall wäre innerhalb der Komponente foo auf den Wert „bar“ gesetzt. Außerdem wird der Großteil der Logik und des Data Fetchings im Script-Teil erledigt. Hier können Variablen definiert werden, die im Markup-Teil verwendet werden können. Auf die Möglichkeiten des Templatings innerhalb des Markup-Teils wird später noch genauer eingegangen. Im Script-Teil kann neben JavaScript auch TypeScript verwendet werden, indem <script lang="ts"></script> verwendet wird.

Um die Komponente zu stylen, kann innerhalb des Style-Tags auf CSS zurückgegriffen werden. Styles, die hier definiert werden, sind standardmäßig gescoped auf die aktuelle Komponente. Dadurch wird verhindert, dass ungewollt Styles für die gesamte Anwendung anstatt für eine einzelne Komponente gesetzt werden.

    <script>
        export let foo;
        // logic goes here
    </script>
    <!-- markup (zero or more items) goes here -->
    <style>
        /* styles go here */
    </style>

1.3.3. SPA

Quellen

Single Page Applications zeichnen sich dadurch aus, dass die Navigation der Seiten durch JavaScript APIs erfolgt und dadurch beim Navigieren die Seite nicht durch einen Full Refresh neugeladen werden muss. Stattdessen wird der Inhalt der Seite dynamisch ersetzt. Beim ersten Laden der Seite muss bei einer SPA die komplette Payload der Seite inklusive aller Unterseiten an den Client geschickt werden, daher ist diese im Regelfall größer als bei regulären Webanwendungen, bei denen der Server für jede Route eine eigene HTML-Datei ausliefert. Der Vorteil der SPA ist hier aber, dass dadurch beim Navigieren durch die Seiten meist geringere Datenmengen angefragt werden müssen, da der Seitenaufbau bereits beim initialen Laden vorhanden ist und dadurch die Serverlast durch das reine Navigieren reduziert ist. Für den Benutzer fühlen sich SPAs meist schneller & dynamischer an.

1.3.4. Template-Syntax

Svelte ist komponentenbasiert. Daraus folgt, dass man beim Erstellen der Anwendung mehrere modulare Bausteine erstellt, die durch verschiedenste Mechanismen miteinander kommunizieren können. In diesen Svelte-Komponenten, die jeweils in einer .svelte-Datei angelegt werden, steht die Template Syntax von Svelte zur Verfügung. Diese erweiterte HTML-Syntax macht es dem Entwickler leicht, auch logische Entscheidungen im HTML Markup abzubilden.

  • Each
    Mithilfe von each können durch Arrays oder Objekte iteriert werden und Markup für die einzelnen Elemente erzeugt werden.

    {#each todoLists as todoList}
      <li>
        <button class:active={todoList.id === selectedListId} on:click={() => handleListChange(todoList.id)}>{todoList.name}</button>
      </li>
    {/each}
    
  • If-Else
    Mithilfe von if/else/else if kann der Kontrollfluss kontrolliert werden und abhängig von booleschen Werten jeweils unterschiedliches Markup gerendert werden.

    {#if editing}
    <div class="card-body">
      <input type="text" bind:value={editedName} class="input" placeholder="Title" />
      <textarea bind:value={editedDescription} class="textarea" placeholder="Description"></textarea>
      <div class="card-actions">
        <button class="btn btn-primary btn-sm" on:click={saveChanges}>Save</button>
        <button class="btn btn-error btn-sm" on:click={() => editing = false}>Cancel</button>
      </div>
    </div>
    {:else}
    <div class="card-body">
      <h2 class="card-title" class:line-through={done}>{name}</h2>
      {#if description}
        <p>{description}</p>
      {/if}
      <div class="card-actions">
        <button class="btn btn-primary btn-sm" on:click={startEditing}>Edit</button>
        <button class="btn btn-success btn-sm" on:click={toggleDone}>
          {done ? 'Undone' : 'Done'}
        </button>
        <button class="btn btn-error btn-sm" on:click={deleteItem}>Delete</button>
      </div>
    </div>
    {/if}
    
  • Await
    await macht es möglich, die 3 verschiedenen States (pending, fulfilled, rejected) eines Promises innerhalb des Markups abzubilden. Im folgenden Codebeispiel wird für den Status pending ein Loading-Spinner angezeigt. Sobald das Promise in den Zustand fulfilled übergegangen ist, haben wir Daten, mit denen wir arbeiten können (in diesem Fall todoLists) und können das UI bauen, welches die Navigation und die Todo-Items enthält. Falls beim Laden der Daten ein Fehler auftreten sollte, geht das Promise in den Zustand rejected über und wir zeigen im catch-Block des folgenden Markups die jeweilige Fehlermeldung in rot an.

    {#await todoListsPromise}
      <span class="loading loading-spinner loading-md"></span>
    {:then todoLists}
      <main class='w-8/12 mx-auto mt-5 dark:text-white'>
        <Navigation {todoLists} {selectedListId} on:listChange={handleListChange} on:triggerRefresh={handleRefresh} />
        <div class='flex flex-row justify-center mt-5'>
          {#if todoLists.length > 0}
            <TodoList todos={todosInSelectedList} {selectedListId} on:updateData={handleDataUpdate} on:triggerRefresh={handleRefresh} />
          {/if}
        </div>
      </main>
    {:catch error}
      <p class='text-error'>{error}</p>
    {/await}
    

1.3.5. State Management

Quellen

Um in Svelte reaktive Variablen zu erstellen, reicht es aus, eine einfache Variable mit let var = "value" zu definieren. Sobald diese geändert wird (im nachfolgenden Beispiel sobald die Funktion startEditing() aufgerufen wird), wird ein Rerender getriggert und Svelte updatet das UI. Da im folgenden Beispiel ein if-Branch erstellt wurde, der den Wert von editing prüft, wird nach der Änderung der Variable auch der jeweils andere Block angezeigt.

let editing = false;
function startEditing() {
  editing = true;
}
{#if editing}
{:else}
{/if}

Außerdem können in Svelte Reactive Statements erstellt werden. Diese Ausdrücke reagieren auf Änderungen von Variablen innerhalb des Ausdrucks. Im folgenden Beispiel verändert sich der Wert der reaktiven Variable formIsValid sobald sich der Wert der referenzierten Variable name verändert.

$: formIsValid = name.length > 0;

1.3.6. Event Handling

In Svelte können Komponenten Events erzeugen, die beim jeweiligen Parent ankommen. Diese Events können neben einem Namen des Events auch einen Payload mitliefern. Im folgenden Beispiel wird ein Todo-Item erzeugt, wobei der Server mit der neuen Liste an Todos antwortet. Diese neue Liste (gespeichert in response) wird mithilfe des erstellten Event Dispatchers dispatch im Event Payload an den Parent der aktuellen Komponente geschickt. Der Parent bindet die Komponente ein (NewTodoForm) und kann festlegen, was beim Empfangen des Events passiert. In unserem Fall geben wir mit der Syntax on:updateData das Event eine Ebene weiter nach oben. Auf dieser Ebene wird das Event dann gefangen und ein Handler wird ausgeführt (hier handleDataUpdate).

const dispatch = createEventDispatcher<{ updateData: ToDoListDto }>();

const response = await todoApi.putToDoItem(selectedListId, uuid, {
  name,
  description,
  done: false,
});

dispatch('updateData', response)
<NewTodoForm {selectedListId} on:updateData />
<TodoList todos={todosInSelectedList} {selectedListId} on:updateData={handleDataUpdate} on:triggerRefresh={handleRefresh} />

Event-Handler handleDataUpdate:
Der Event-Handler hat Zugriff auf das empfangene Event und hat in der Property event Zugriff auf die gesendeten Event-Daten.

function handleDataUpdate(event: CustomEvent<ToDoListDto>) {
    const updatedList = event.detail;
    todosInSelectedList = updatedList.items;
}

1.4. Verwendung in der ToDo-App

Aufbauend auf unserer bereits bestehenden Svelte ToDo-App haben wir Capacitor in die Anwendung eingebunden. Dadurch, dass Capacitor auf der bestehenden Anwendung aufsetzt, konnten wir bestehende Funktionen wiederverwenden.

Um Capacitor auszutesten, haben wir für die einzelnen Todo Cards eine Action Sheet Component eingebaut:

 const showActionSheet = async () => {
    const actionSheetOptions = [
        {
            title: 'Share',
            onTap: async () => {
                const descriptionText = description ? `(${description})` : '';
                void Share.share({
                    text: `${name} ${descriptionText} - Shared from ToDo App`
                });
            }
        },
        {
            title: 'Edit',
            onTap: () => startEditing()
        },
        {
            title: 'Remove',
            style: ActionSheetButtonStyle.Destructive,
            onTap: () => deleteItem()
        },
        {
            title: 'Cancel',
            style: ActionSheetButtonStyle.Destructive,
            onTap: () => {
            }
        }
    ]

    const result = await ActionSheet.showActions({
        title: 'Quick Actions',
        message: 'Select an action to perform',
        options: actionSheetOptions
    });

    actionSheetOptions[result.index].onTap();
};

Im eigens implementierten onTap-Callback wird jeweils die entsprechende Aktion ausgeführt, die auf bereits bestehende Funktionen zurückgreift.

Wie man sieht, ist es sehr einfach, native Features in einer bestehenden Anwendung zu integrieren.

Die Funktionalitäten werden dafür jeweils aus eigenen Packages importiert:

import {ActionSheet, ActionSheetButtonStyle} from '@capacitor/action-sheet';
import {Share} from '@capacitor/share';

1.4.1. Vorteile von Svelte

Quellen

  • Svelte kompiliert die Anwendung zur Erstellungszeit zu optimalem JavaScript Code anstatt den Code zur Laufzeit zu interpretieren. Dadurch wird das erstmalige Laden der Seite beschleunigt, da keine Zusatzkosten durch die Abstraktion des Frameworks entstehen. Außerdem fällt dadurch das Virtuelle DOM weg, wodurch ebenfalls die Ladezeiten verringert werden und die Anwendung flüssiger läuft.

  • Die potenziell geringere Bundle-Größe kann ebenfalls für geringere Ladezeiten sorgen.

  • Svelte benötigt weniger Boilerplate Code als ähnliche Frameworks.

  • In Svelte wird kein zusätzlicher Code benötigt um eine Variable reaktiv zu machen. Jede Variable ist standardmäßig reaktiv.

  • Svelte ist durch die simple Syntax leicht zu lernen und verstehen.

Beispiel für reaktive Variable in Svelte:

<script>
  let count = 0;

  function increment() {
    count += 1;
  }
</script>

<button on:click={increment}>
  Click to increment: {count}
</button>

Beispiel für reaktive Variable in React:

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  function increment() {
    setCount(count + 1);
  }

  return (
    <button onClick={increment}>
      Click to increment: {count}
    </button>
  );
}

1.5. Fazit

Svelte ist ein relativ neues JavaScript Framework, welches sich durch eine einfache Syntax, kaum Boilerplate Code und eine hohe Performance von anderen JavaScript Frameworks absetzt. Svelte wird bereits in großen Unternehmen produktiv eingesetzt und wird stets weiterentwickelt. Zwar ist das Ökosystem rund um Svelte zur Zeit nicht so ausgereift, wie bei anderen Frameworks, allerdings ist die Popularität des Frameworks sehr hoch, wodurch anzunehmen ist, dass sich das Ökosystem rasant ausweiten wird.

1.6. Quellen