Wie ich eine plattformübergreifende Desktop-Anwendung mit Svelte, Redis und Rust erstellt habe

Avatar of Luke Edwards
Luke Edwards am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Bei Cloudflare haben wir ein großartiges Produkt namens Workers KV, das eine global replizierte Key-Value-Speicherschicht ist. Es kann Millionen von Schlüsseln verarbeiten, die jeweils von einem Worker-Skript aus mit außergewöhnlich niedriger Latenz zugänglich sind, unabhängig davon, wo auf der Welt eine Anfrage empfangen wird. Workers KV ist erstaunlich – und ebenso seine Preisgestaltung, die eine großzügige kostenlose Stufe beinhaltet.

Als langjähriger Nutzer der Cloudflare-Produktpalette habe ich jedoch eine Sache vermisst: lokale Introspektion. Bei Tausenden, manchmal Hunderttausenden von Schlüsseln in meinen Anwendungen hätte ich mir oft gewünscht, es gäbe eine Möglichkeit, alle meine Daten abzufragen, sie zu sortieren oder einfach nur einen Blick darauf zu werfen, was sich tatsächlich darin befindet.

Nun, vor kurzem hatte ich das Glück, zu Cloudflare zu wechseln! Noch mehr, ich bin kurz vor der „Quick Wins Week“ des Quartals eingestiegen – alias unsere einwöchige Hackathon-Veranstaltung. Und da ich noch nicht lange genug dabei war, um einen Rückstand anzuhäufen (noch nicht), glaubt mir, ich habe die Gelegenheit genutzt, meinen eigenen Wunsch zu erfüllen.

Nach dieser Einleitung erzähle ich Ihnen, wie ich Workers KV GUI erstellt habe, eine plattformübergreifende Desktop-Anwendung unter Verwendung von Svelte, Redis und Rust.

Die Front-End-Anwendung

Als Webentwickler war dies der *vertraute* Teil. Ich bin versucht, dies den „einfachen Teil“ zu nennen, aber angesichts der Tatsache, dass Sie *alle* HTML-, CSS- und JavaScript-Frameworks, Bibliotheken oder Muster verwenden können, kann die Wahl-Paralyse leicht einsetzen… was ebenfalls vertraut sein mag. Wenn Sie einen bevorzugten Front-End-Stack haben, großartig, nutzen Sie ihn! Für diese Anwendung habe ich mich für Svelte entschieden, da es für mich die Dinge definitiv einfach macht und hält.

Auch als Webentwickler erwarten wir, unsere gesamte Ausrüstung mitzunehmen. Das können Sie sicherlich tun! Auch diese Phase des Projekts unterscheidet sich *nicht* von Ihrem typischen Webentwicklungszyklus. Sie können davon ausgehen, dass `yarn dev` (oder eine Variante) Ihr Hauptbefehl ist und Sie sich wie zu Hause fühlen. Um dem „einfachen“ Thema treu zu bleiben, habe ich mich für SvelteKit entschieden, das Sveltes offizielles Framework und Toolkit für die Erstellung von Anwendungen ist. Es beinhaltet ein optimiertes Build-System, eine großartige Entwicklererfahrung (inklusive HMR!), einen dateisystembasierten Router und alles, was Svelte *selbst* zu bieten hat.

Als Framework, insbesondere eines, das sich um seine eigene Werkzeugkette kümmert, ermöglichte mir SvelteKit, *rein* über meine *Anwendung* und ihre Anforderungen nachzudenken. Tatsächlich musste ich, was die Konfiguration betrifft, nur SvelteKit mitteilen, dass ich eine Single-Page-Anwendung (SPA) erstellen möchte, die *nur* im Client läuft. Mit anderen Worten, ich musste explizit SvelteKits Annahme *ablehnen*, dass ich einen Server wollte, was eigentlich eine faire Annahme ist, da die meisten Anwendungen von Server-Side-Rendering profitieren können. Das war so einfach wie das Hinzufügen des `@sveltejs/adapter-static`-Pakets, das genau für diesen Zweck eine Konfigurations-Preset ist. Nach der Installation war dies meine gesamte Konfigurationsdatei

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

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: preprocess(),

  kit: {
    adapter: adapter({
      fallback: 'index.html'
    }),
    files: {
      template: 'src/index.html'
    }
  },
};

export default config;

Die Änderungen an `index.html` sind persönliche Präferenzen. SvelteKit verwendet `app.html` als Standard-Basisvorlage, aber alte Gewohnheiten sterben schwer.

Es sind erst wenige Minuten vergangen und meine Werkzeugkette weiß bereits, dass sie eine SPA erstellt, dass ein Router vorhanden ist und ein Entwicklungsserver bereitsteht. Außerdem ist die Unterstützung für TypeScript, PostCSS und/oder Sass vorhanden, falls ich sie wünsche (und das tue ich), dank `svelte-preprocess`. Bereit zum Start!

Die Anwendung benötigte zwei Ansichten

  1. eine Ansicht zur Eingabe von Verbindungsdetails (die Standard-/Willkommens-/Startseite)
  2. eine Ansicht zur tatsächlichen Anzeige Ihrer Daten

In der Welt von SvelteKit bedeutet dies zwei „Routen“, und SvelteKit gibt vor, dass diese als `src/routes/index.svelte` für die Startseite und `src/routes/viewer.svelte` für die Datenansichtsseite existieren sollen. In einer echten Webanwendung würde diese zweite Route der URL `/viewer` zugeordnet werden. Während dies immer noch der Fall ist, weiß ich, dass meine Desktop-Anwendung keine Navigationsleiste haben wird, was bedeutet, dass die URL nicht sichtbar sein wird… was bedeutet, dass es egal ist, wie ich diese Route nenne, solange sie für mich Sinn ergibt.

Der Inhalt dieser Dateien ist größtenteils irrelevant, zumindest für diesen Artikel. Für Neugierige ist das gesamte Projekt Open Source und wenn Sie nach einem Svelte- oder SvelteKit-Beispiel suchen, lade ich Sie herzlich ein, es sich anzusehen. Auf die Gefahr hin, wie eine kaputte Schallplatte zu klingen, ist der Punkt hier, dass ich eine normale Web-App erstelle.

Zu diesem Zeitpunkt entwerfe ich meine Ansichten und verwende gefälschte, hartkodierte Daten, bis ich etwas habe, das zu funktionieren scheint. Ich habe hier etwa zwei Tage verbracht, bis alles gut aussah und die gesamte Interaktivität (Schaltflächenklicks, Formularübermittlungen usw.) ausgearbeitet war. Ich würde dies als „funktionierende“ App oder Mockup bezeichnen.

Desktop-Anwendungs-Tooling

Zu diesem Zeitpunkt existiert eine voll funktionsfähige SPA. Sie wird in einem Webbrowser betrieben und wurde dort entwickelt. Vielleicht kontraintuitiv, aber das macht sie zum perfekten Kandidaten für eine Desktop-Anwendung! Aber wie?

Sie haben vielleicht von Electron gehört. Es ist *das* bekannteste Werkzeug zum Erstellen plattformübergreifender Desktop-Anwendungen mit Webtechnologien. Eine Reihe von massiv populären und erfolgreichen Anwendungen wurden damit erstellt: Visual Studio Code, WhatsApp, Atom und Slack, um nur einige zu nennen. Es funktioniert, indem Ihre Web-Assets mit einer eigenen Chromium-Installation und seiner eigenen Node.js-Laufzeitumgebung gebündelt werden. Mit anderen Worten, wenn Sie eine Electron-basierte Anwendung installieren, erhalten Sie einen zusätzlichen Chrome-Browser und eine ganze Programmiersprache (Node.js). Diese sind in den Anwendungsdateien eingebettet und es gibt kein Entkommen, da sie Abhängigkeiten der Anwendung sind und garantieren, dass sie überall konsistent läuft. Wie Sie sich vorstellen können, hat dieser Ansatz einen Haken – die Anwendungen sind ziemlich riesig (d. h. mehr als 100 MB) und verbrauchen viele Systemressourcen für den Betrieb. Um die Anwendung nutzen zu können, läuft ein komplett neues/separates Chrome im Hintergrund – nicht ganz dasselbe wie das Öffnen eines neuen Tabs.

Glücklicherweise gibt es einige Alternativen – ich habe Svelte NodeGui und Tauri evaluiert. Beide Optionen boten erhebliche Einsparungen bei der Anwendungsgröße und -nutzung, indem sie auf *native Renderer* setzten, die das Betriebssystem bietet, anstatt eine Kopie von Chrome einzubinden, um die gleiche Arbeit zu leisten. NodeGui tut dies, indem es auf Qt setzt, einem weiteren Desktop-/GUI-Anwendungsframework, das zu nativen Views kompiliert wird. Um dies zu erreichen, erfordert NodeGui jedoch einige Anpassungen Ihres Anwendungscodes, damit Ihre Komponenten in *Qt*-Komponenten übersetzt werden. Obwohl ich sicher bin, dass dies funktioniert hätte, war ich an dieser Lösung nicht interessiert, da ich *genau* das verwenden wollte, was ich bereits kannte, ohne *irgendwelche* Anpassungen an meinen Svelte-Dateien vornehmen zu müssen. Im Gegensatz dazu erzielt Tauri seine Einsparungen, indem es den nativen Webviewer des Betriebssystems verwendet – zum Beispiel Cocoa/WebKit unter macOS, gtk-webkit2 unter Linux und Webkit über Edge unter Windows. Webviewer sind im Wesentlichen Browser, die Tauri verwendet, weil sie bereits auf Ihrem System vorhanden sind, und das bedeutet, dass unsere Anwendungen reine Webentwicklungsprodukte bleiben können.

Mit diesen Einsparungen ist die minimale Tauri-Anwendung weniger als 4 MB groß, während durchschnittliche Anwendungen weniger als 20 MB wiegen. In meinen Tests wog die minimale NodeGui-Anwendung etwa 16 MB. Eine minimale Electron-App ist leicht 120 MB.

Ich muss wohl nicht sagen, dass ich mich für Tauri entschieden habe. Indem ich dem Tauri Integration Guide folgte, fügte ich das Paket `@tauri-apps/cli` zu meinen `devDependencies` hinzu und initialisierte das Projekt

yarn add --dev @tauri-apps/cli
yarn tauri init

Dies erstellt ein `src-tauri`-Verzeichnis neben dem `src`-Verzeichnis (wo die Svelte-Anwendung lebt). Hier befinden sich alle Tauri-spezifischen Dateien, was für die Organisation gut ist.

Ich hatte noch nie zuvor eine Tauri-Anwendung erstellt, aber nach dem Betrachten der Konfigurationsdokumentation konnte ich die meisten Standardeinstellungen beibehalten – abgesehen von Dingen wie den Werten für `package.productName` und `windows.title`, natürlich. Wirklich, die einzigen Änderungen, die ich machen *musste*, betrafen die `build`-Konfiguration, die mit SvelteKit für Entwicklungs- und Ausgabinformationen abgestimmt werden musste

// src-tauri/tauri.conf.json
{
  "package": {
    "version": "0.0.0",
    "productName": "Workers KV"
  },
  "build": {
    "distDir": "../build",
    "devPath": "https://:3000",
    "beforeDevCommand": "yarn svelte-kit dev",
    "beforeBuildCommand": "yarn svelte-kit build"
  },
  // ...
}

Die `distDir` bezieht sich darauf, wo sich die erstellten, produktionsreifen Assets befinden. Dieser Wert wird vom Speicherort der Datei `tauri.conf.json` aufgelöst, daher der Präfix `../`.

Der `devPath` ist die URL, die während der Entwicklung proxyed werden soll. Standardmäßig startet SvelteKit einen Dev-Server auf Port 3000 (konfigurierbar, natürlich). Ich habe während der ersten Phase die Adresse `localhost:3000` in meinem Browser besucht, also ist das keine Veränderung.

Schließlich hat Tauri seine *eigenen* `dev`- und `build`-Befehle. Um den Aufwand des Jonglierens mit mehreren Befehlen oder Build-Skripten zu vermeiden, bietet Tauri die `beforeDevCommand`- und `beforeBuildCommand`-Hooks, die es Ihnen ermöglichen, jeden Befehl auszuführen, *bevor* der `tauri`-Befehl ausgeführt wird. Das ist ein subtiler, aber starker Komfort!

Auf die SvelteKit CLI wird über den Binärnamen `svelte-kit` zugegriffen. Wenn Sie beispielsweise `yarn svelte-kit build` schreiben, wird `yarn` angewiesen, seine lokale `svelte-kit`-Binärdatei abzurufen, die als `devDependency` installiert wurde, und dann SvelteKit anzuweisen, seinen `build`-Befehl auszuführen.

Mit diesem Setup enthielt meine `package.json` auf der Root-Ebene die folgenden Skripte

{
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "tauri dev",
    "build": "tauri build",
    "prebuild": "premove build",
    "preview": "svelte-kit preview",
    "tauri": "tauri"
  },
  // ...
  "devDependencies": {
    "@sveltejs/adapter-static": "1.0.0-next.9",
    "@sveltejs/kit": "1.0.0-next.109",
    "@tauri-apps/api": "1.0.0-beta.1",
    "@tauri-apps/cli": "1.0.0-beta.2",
    "premove": "3.0.1",
    "svelte": "3.38.2",
    "svelte-preprocess": "4.7.3",
    "tslib": "2.2.0",
    "typescript": "4.2.4"
  }
}

Nach der Integration blieb mein Produktionsbefehl `yarn build`, der `tauri build` aufruft, um die Desktop-Anwendung tatsächlich zu bündeln, aber erst nachdem `yarn svelte-kit build` erfolgreich abgeschlossen wurde (über die Option `beforeBuildCommand`). Und mein Entwicklungskommando blieb `yarn dev`, das die Befehle `tauri dev` und `yarn svelte-kit dev` parallel ausführt. Der Entwicklungsworkflow befindet sich vollständig innerhalb der Tauri-Anwendung, die jetzt `localhost:3000` proxyed und mir so die Vorteile eines HMR-Entwicklungsservers weiterhin bietet.

Wichtig: Tauri befindet sich zum Zeitpunkt der Erstellung dieses Textes noch im Beta-Stadium. Dennoch fühlt es sich sehr stabil und gut geplant an. Ich habe keine Verbindung zu dem Projekt, aber es scheint, dass Tauri 1.0 eher früher als später eine stabile Veröffentlichung erreichen könnte. Ich fand den Tauri Discord sehr aktiv und hilfreich, einschließlich Antworten von den Tauri-Betreuern! Sie haben sogar einige meiner Anfängerfragen zu Rust während des gesamten Prozesses beantwortet. :)

Verbindung zu Redis

Zu diesem Zeitpunkt ist es Mittwoch Nachmittag der Quick Wins Woche, und – um ehrlich zu sein – ich fange an, nervös zu werden, ob ich es vor der Teampräsentation am Freitag schaffe. Warum? Weil ich bereits die halbe Woche hinter mir habe und obwohl ich eine gut aussehende SPA *innerhalb* einer funktionierenden Desktop-Anwendung habe, tut sie immer noch *nichts*. Ich habe die ganze Woche auf die gleichen *gefälschten Daten* gestarrt.

Sie denken vielleicht, dass ich *weil* ich Zugriff auf ein Webview habe, `fetch()` verwenden kann, um einige authentifizierte REST-API-Aufrufe für die gewünschten Workers KV-Daten zu tätigen und sie alle in `localStorage` oder einer IndexedDB-Tabelle zu speichern… **Sie haben zu 100 % Recht!** Allerdings ist das nicht ganz das, was ich mir für den Anwendungsfall meiner Desktop-Anwendung vorgestellt hatte.

Das Speichern aller Daten in einer Art In-Browser-Speicher ist absolut machbar, aber es speichert sie *lokal auf Ihrem Rechner*. Das bedeutet, wenn Teammitglieder versuchen, dasselbe zu tun, müssen *alle* alle Daten auf *ihren eigenen Rechnern* abrufen und speichern. Idealerweise sollte diese Workers KV-Anwendung die Option haben, sich mit einer externen Datenbank zu verbinden und mit ihr zu synchronisieren. So können in Teams alle denselben Daten-Cache nutzen, um Zeit zu sparen – und ein paar Dollar. Das wird wichtig, wenn man mit Millionen von Schlüsseln zu tun hat, was, wie bereits erwähnt, bei Workers KV nicht ungewöhnlich ist.

Nachdem ich eine Weile darüber nachgedacht hatte, beschloss ich, Redis als mein Backend zu verwenden, da es *ebenfalls* ein Key-Value-Speicher ist. Das war großartig, weil Redis Schlüssel bereits als Erstklasse-Bürger behandelt und die gewünschten Sortier- und Filterfunktionen bietet (d. h. ich kann die Arbeit delegieren, anstatt sie selbst zu implementieren!). Und dann ist Redis natürlich einfach zu installieren und lokal oder in einem Container auszuführen, und es gibt viele gehostete Redis-as-a-Service-Anbieter, falls jemand diesen Weg wählt.

Aber wie verbinde ich mich damit? Meine App ist im Grunde ein Browser-Tab, auf dem Svelte läuft, richtig? Ja – aber auch so viel mehr.

Sie sehen, ein Teil des Erfolgs von Electron ist, dass es zwar garantiert, dass eine Web-App auf jedem Betriebssystem gut aussieht, aber es bringt auch eine Node.js-Laufzeitumgebung mit. Für einen Webentwickler war das so, als würde man eine Backend-API direkt in seinen Client einbinden. Im Grunde löste sich das Problem „Aber es funktioniert auf meiner Maschine“, weil alle Benutzer (unwissentlich) genau dasselbe `localhost`-Setup ausführten. Über die Node.js-Schicht konnte man auf das Dateisystem zugreifen, Server auf mehreren Ports ausführen oder eine Reihe von `node_modules` einschließen, um – und ich spinne jetzt mal – eine Verbindung zu einer Redis-Instanz herzustellen. Mächtige Sachen.

Wir verlieren diese Superkraft nicht, nur weil wir Tauri verwenden! Es ist dasselbe, nur etwas anders.

Anstatt eine Node.js-Laufzeitumgebung einzubinden, werden Tauri-Anwendungen mit Rust, einer Low-Level-Systemsprache, erstellt. So interagiert Tauri *selbst* mit dem Betriebssystem und „leiht“ sich dessen nativen Webviewer. Der gesamte Tauri-Toolkit wird kompiliert (via Rust), was es der erstellten Anwendung ermöglicht, klein und effizient zu bleiben. Das bedeutet jedoch auch, dass *wir*, die Anwendungsentwickler, beliebige zusätzliche Crates – das Äquivalent zu „npm-Modulen“ – in die erstellte Anwendung einbinden können. Und natürlich gibt es einen passend benannten `redis`-Crate, der als Redis-Client-Treiber der Workers KV GUI ermöglicht, sich mit jeder Redis-Instanz zu verbinden.

In Rust ist die Datei `Cargo.toml` ähnlich wie unsere Datei `package.json`. Hier werden Abhängigkeiten und Metadaten definiert. In einer Tauri-Umgebung befindet sich diese unter `src-tauri/Cargo.toml`, da, wie gesagt, *alles*, was mit Tauri zu tun hat, in diesem Verzeichnis zu finden ist. Cargo hat auch das Konzept von „Feature Flags“, die auf Abhängigkeitsebene definiert werden. (Die nächstgelegene Analogie, die mir einfällt, ist die Verwendung von `npm`, um auf die internen Bestandteile eines Moduls zuzugreifen oder ein benanntes Untermodul zu importieren, obwohl es immer noch nicht ganz dasselbe ist, da in Rust Feature Flags beeinflussen, wie das Paket gebaut wird.)

# src-tauri/Cargo.toml
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.0-beta.1", features = ["api-all", "menu"] }
redis = { version = "0.20", features = ["tokio-native-tls-comp"] }

Oben wird der `redis`-Crate als Abhängigkeit definiert und das Feature `"tokio-native-tls-comp"` aktiviert, das laut Dokumentation für TLS-Unterstützung erforderlich ist.

Okay, ich hatte also alles, was ich brauchte. Bevor der Mittwoch zu Ende ging, musste ich meine Svelte-App mit meinem Redis zum Laufen bringen. Nach einigem Herumprobieren bemerkte ich, dass all die wichtigen Dinge im `src-tauri/main.rs`-File zu passieren schienen. Ich bemerkte das `#[command]`-Makro, das ich zuvor in einem Tauri-Beispiel gesehen hatte, also studierte kopierte ich das Beispiel-File abschnittsweise und sah, welche Fehler kamen und gingen, je nach dem Rust-Compiler.

Schließlich konnte die Tauri-Anwendung wieder ausgeführt werden, und ich lernte, dass das `#[command]`-Makro die zugrundeliegende Funktion so umschließt, dass sie „Kontextwerte“ empfangen kann, wenn Sie diese verwenden möchten, und vor-analysierte Argumentwerte empfangen kann. Außerdem führt Rust als Sprache *sehr viel* Typumwandlung durch. Zum Beispiel

use tauri::{command};

#[command]
fn greet(name: String, age: u8) {
  println!("Hello {}, {} year-old human!", name, age);
}

Dies erstellt einen `greet`-Befehl, der bei Ausführung zwei Argumente *erwartet*: `name` und `age`. Bei der Definition ist der Wert von `name` ein String-Wert und `age` ein `u8`-Datentyp – also eine Ganzzahl. Wenn jedoch eines der Argumente fehlt, wirft Tauri einen Fehler, da die Befehlsdefinition *nicht* besagt, dass etwas optional sein darf.

Um einen Tauri-Befehl tatsächlich mit der Anwendung zu verbinden, muss er als Teil der `tauri::Builder`-Komposition innerhalb der `main`-Funktion definiert werden.

use tauri::{command};

#[command]
fn greet(name: String, age: u8) {
  println!("Hello {}, {} year-old human!", name, age);
}

fn main() {
  // start composing a new Builder chain
  tauri::Builder::default()
    // assign our generated "handler" to the chain
    .invoke_handler(
      // piece together application logic
      tauri::generate_handler![
        greet, // attach the command
      ]
    )
    // start/initialize the application
    .run(
      // put it all together
      tauri::generate_context!()
    )
    // print <message> if error while running
    .expect("error while running tauri application");
}

Die Tauri-Anwendung kompiliert und ist sich *bewusst*, dass sie einen „greet“-Befehl besitzt. Sie steuert bereits ein Webview (das wir besprochen haben), aber *dadurch* fungiert sie als Brücke zwischen dem Frontend (dem Webview-Inhalt) und dem Backend, das aus den Tauri-APIs und beliebigem zusätzlichen Code besteht, den wir geschrieben haben, wie dem `greet`-Befehl. Tauri ermöglicht es uns, Nachrichten über diese Brücke zu senden, damit die beiden Welten miteinander kommunizieren können.

A component diagram of a basic Tauri application.
Der Entwickler ist für den Webview-Inhalt verantwortlich und kann optional benutzerdefinierte Rust-Module und/oder benutzerdefinierte Befehle definieren. Tauri steuert den Webviewer und die Ereignisbrücke, einschließlich aller Nachrichten-Serialisierungs- und Deserialisierungsoperationen.

Diese „Brücke“ kann vom Frontend durch Importieren von Funktionalität aus einem der (bereits enthaltenen) `@tauri-apps`-Pakete oder durch Nutzung des globalen `window.__TAURI__`-Objekts, das der gesamten Client-Anwendung zur Verfügung steht, angesprochen werden. Speziell interessiert uns der `invoke`-Befehl, der einen Befehlsnamen und eine Reihe von Argumenten benötigt. Wenn es Argumente gibt, müssen diese als Objekt definiert werden, bei dem die Schlüssel mit den Parameternamen übereinstimmen, die unsere Rust-Funktion erwartet.

Auf der Svelte-Ebene bedeutet dies, dass wir in unserem Code Folgendes tun können, um den auf der Rust-Ebene definierten `greet`-Befehl aufzurufen

<!-- Greeter.svelte -->
<script>
  function onclick() {
    __TAURI__.invoke('greet', {
      name: 'Alice',
      age: 32
    });
  }
</script>

<button on:click={onclick}>Click Me</button>

Wenn diese Schaltfläche geklickt wird, wird in unserem Terminalfenster (wo immer der Befehl `tauri dev` ausgeführt wird) Folgendes ausgegeben:

Hello Alice, 32 year-old human!

Auch dies geschieht aufgrund der `println!`-Funktion, die effektiv `console.log` für Rust ist, das der `greet`-Befehl verwendet hat. Sie erscheint im Konsolenfenster des Terminals – nicht in der Browserkonsole –, da dieser Code immer noch auf der Rust/Systemseite läuft.

Es ist auch möglich, etwas *zurück* an den Client von einem Tauri-Befehl zu senden. Ändern wir also `greet` schnell

use tauri::{command};

#[command]
fn greet(name: String, age: u8) {
  // implicit return, because no semicolon!
  format!("Hello {}, {} year-old human!", name, age)
}

// OR

#[command]
fn greet(name: String, age: u8) {
  // explicit `return` statement, must have semicolon
  return format!("Hello {}, {} year-old human!", name, age);
}

Da ich erkannte, dass ich `invoke` mehrmals aufrufen würde, und ein bisschen faul war, extrahierte ich einen einfachen Client-seitigen Helfer, um die Dinge zu konsolidieren

// @types/global.d.ts
/// <reference types="@sveltejs/kit" />

type Dict<T> = Record<string, T>;

declare const __TAURI__: {
  invoke: typeof import('@tauri-apps/api/tauri').invoke;
}

// src/lib/tauri.ts
export function dispatch(command: string, args: Dict<string|number>) {
  return __TAURI__.invoke(command, args);
}

Die vorherige `Greeter.svelte` wurde dann refaktorisiert zu

<!-- Greeter.svelte -->
<script lang="ts">
  import { dispatch } from '$lib/tauri';

  async function onclick() {
    let output = await dispatch('greet', {
      name: 'Alice',
      age: 32
    });
    console.log('~>', output);
    //=> "~> Hello Alice, 32 year-old human!"
  }
</script>

<button on:click={onclick}>Click Me</button>

Großartig! Es ist also Donnerstag und ich habe immer noch keinen Redis-Code geschrieben, aber zumindest weiß ich jetzt, wie man die beiden Hälften des Gehirns meiner Anwendung miteinander verbindet. Es war Zeit, sich den clientseitigen Code noch einmal vorzunehmen und alle `TODO`s innerhalb der Event-Handler zu ersetzen und sie mit der Realität zu verbinden.

Ich werde Sie hier von den Details verschonen, da sie ab hier sehr anwendungsspezifisch sind – und *hauptsächlich* die Geschichte des Rust-Compilers ist, der mich niedergeschlagen hat. Außerdem ist das Herumwühlen in den Details genau der Grund, warum das Projekt Open Source ist!

Auf hoher Ebene, sobald eine Redis-Verbindung mit den angegebenen Details hergestellt ist, ist ein `SYNC`-Button in der `/viewer`-Route zugänglich. Wenn dieser Button geklickt wird (und *nur dann* – aus Kostengründen) wird eine JavaScript-Funktion aufgerufen, die für das Verbinden mit der Cloudflare REST API und das Senden eines `"redis_set"`-Befehls für jeden Schlüssel verantwortlich ist. Dieser `redis_set` command ist auf der Rust-Ebene definiert – ebenso wie *alle* Redis-basierten Befehle – und ist dafür verantwortlich, das Schlüssel-Wert-Paar tatsächlich in Redis zu schreiben.

Das Auslesen von Daten *aus* Redis ist ein sehr ähnlicher Prozess, nur umgekehrt. Wenn zum Beispiel die `/viewer`-Route gestartet wurde, sollten alle Schlüssel aufgelistet und bereit sein. In Svelte-Begriffen bedeutet das, dass ich beim Einhängen der `/viewer`-Komponente einen Tauri-Befehl *dispatch*en muss. Das passiert hier, fast wortwörtlich. Zusätzlich enthüllt das Klicken auf einen Schlüsselnamen in der Seitenleiste zusätzliche „Details“ zum Schlüssel, einschließlich seines Ablaufs (falls vorhanden), seiner Metadaten (falls vorhanden) und seines tatsächlichen Werts (falls bekannt). Um Kosten und Netzwerklast zu optimieren, haben wir entschieden, dass der Wert eines Schlüssels nur auf Befehl abgerufen werden sollte. Dies führt zu einem `REFRESH`-Button, der beim Klicken erneut mit der REST-API interagiert und dann einen Befehl dispatcht, damit der Redis-Client diesen Schlüssel einzeln aktualisieren kann.

Ich will die Dinge nicht überstürzt beenden, aber sobald Sie eine erfolgreiche Interaktion zwischen Ihrem JavaScript- und Rust-Code gesehen haben, haben Sie sie alle gesehen! Der Rest meines Donnerstags und Freitagmorgens bestand nur darin, neue Request-Reply-Paare zu definieren, was sich sehr nach dem Senden von `PING`- und `PONG`-Nachrichten an mich selbst anfühlte.

Fazit

Für mich – und ich stelle mir vor, für viele andere JavaScript-Entwickler – bestand die Herausforderung in der vergangenen Woche darin, Rust zu lernen. Ich bin sicher, Sie haben das schon einmal gehört und werden es zweifellos wieder hören. Ownership-Regeln, Borrow-Checking und die Bedeutung von Ein-Zeichen-Syntaxmarkern (die übrigens nicht leicht zu durchsuchen sind) sind nur einige der Hindernisse, auf die ich gestoßen bin. Nochmals vielen Dank an den Tauri Discord für seine Hilfe und Freundlichkeit!

Das soll auch heißen, dass die Verwendung von Tauri *keine Herausforderung* war – es war eine massive Erleichterung. Ich plane definitiv, Tauri in Zukunft wieder zu verwenden, insbesondere wenn ich weiß, dass ich *nur den Webviewer* verwenden kann, wenn ich möchte. Das Eintauchen in und/oder Hinzufügen von Rust-Teilen war „Bonusmaterial“ und nur erforderlich, wenn *meine App* es verlangt.

Für diejenigen, die es sich fragen, da ich keinen anderen Ort fand, es zu erwähnen: unter macOS wiegt die Workers KV GUI-Anwendung weniger als 13 MB. Ich bin **so begeistert** davon!

Und natürlich hat SvelteKit diese Zeitplanung ermöglicht. Es hat mir nicht nur einen halben Tag mühsamer Konfiguration meiner Werkzeugkiste erspart, sondern der sofortige HMR-Entwicklungsserver hat mir wahrscheinlich mehrere Stunden des manuellen Aktualisierens des Browsers gespart – und dann des Tauri-Viewers.

Wenn Sie es bis hierher geschafft haben – das ist beeindruckend! Vielen Dank für Ihre Zeit und Aufmerksamkeit. Zur Erinnerung: Das Projekt ist auf GitHub verfügbar und die neuesten, vorkompilierten Binärdateien sind immer über die Releases-Seite verfügbar.