Desktopanwendungen erstellen: Tauri (Rust)

Nachdem wir die Grundlagen des Editor im letzten Artikel gebaut haben, geht es nun zu den komplexeren Teilen unserer Anwendung: das Laden und Speichern unserer Markdown Dateien. Hier werden wir einen großen Unterschied zu der klassischen Webentwicklung wiederfinden. Auch werden wir die „Eigenheiten“ von Tauri kennenlernen. Davor müssen wir uns aber erste einmal vertraut machen, wie Tauri funktioniert.

Tauri: Rust ❤️ JavaScript

Im Framework Tauri kommen zwei Programmiersprachen zur Anwendung: Rust und JavaScript. Dieser Fakt sollte zu diesem Zeitpunkt keine Überraschung sein. Aber es bildet immer noch eine kleine Ausnahme, da die meisten Frameworks (zumindest bei JavaScript) auf eine Sprache vertrauen.
Die Vorteile sind aber deutlich. Mithilfe von Rust und eigenen Paketen kann das Backend samt Webview so effizient und klein gestaltet werden, wie möglich. Dabei stellt Tauri nicht nur einen Rust Webview, sondern ermöglicht auch, das ganze Backend mit Rust zu erweitern und zu modifizieren. Zugleich kann mit HTML, CSS und JavaScript alle Möglichkeiten der modernen Webentwicklung genutzt werden. Jedoch heißt das auch, dass es nicht mehr so unmittelbar leicht ist, mit beiden Seiten zu kommunizieren.

Menubar

Um jetzt ein typisches Datei Menu einzubauen, wechseln wir auf die Rust Seite. So schauen wir in die src-tauri/src/main.rs Datei. Diese sieht beim ersten öffnen so aus:

#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)\]

fn main() {
    tauri::Builder::default
     .menu(tauri::Menu::os_default(&context.package_info().name))
      .run(tauri::generate_context!())
      .expect("error while running tauri application");
}

Hier wird unsere Anwendung konfiguriert. Der obere Teil umfasst ein Macro, was für diesen Artikel erst einmal nicht von Relevanz ist. tauri::Builder::default() lädt die Standardkonfiguration, die mit unserer Konfiguration aus der tauri.config.json Datei überschrieben wird, was der Befehl .run(tauri::generate_context!()) bezweckt. Auch generiert sie den Inhalt.
Deswegen ist der nachfolgenden Teil, also .expect("error while running tauri application"); nur noch für das Error Handling zuständig. Unser Menü muss also vor dem run Befehl hinzugefügt werden. Also fügen wir eine Menükategorie mit dem Namen „Datei“ und dem Unterkategoriefeld „Schließen“ dazu, der wir die ID „close“ geben. Auch bekommt sie den Tastenkombination strg+Q hinzugefügt.

.menu(Menu::new().add_submenu(Submenu::new("Datei",
    Menu::new()
        .add_item(CustomMenuItem::new("close", "Schließen").accelerator("cmdOrControl+Q"))

Schauen wir in unsere Anwendung, so sehen wir, dass wir ein Menu haben, was aber bis jetzt keine Funktion hat. Das ändern wir gleich. Wir fügen den Punkt .on_menu_event hinzu. Wir konfigurieren ihn so, dass wir in einem match clause die Möglichkeiten für Menu Events durchgehen können. Desweiteren fügen wir den „close“ Button hinzu. Über event.window().close().unwrap(); können wir unsere Anwendungsfenster schließen:

.on_menu_event(|event| match event.menu_item_id() {
    "close" => {
        event.window().close().unwrap();
    }
    _ => {}

Schauen wir in Anwendung wieder rein, können wir sie jetzt schließen.

Jetzt kommen noch die restlichen Menupunkte für Öffnen und Speichern:

.add_item(CustomMenuItem::new("open", "Datei öffnen").accelerator("cmdOrControl+O"))
.add_item(CustomMenuItem::new("save", "Datei Speichern").accelerator("cmdOrControl+S")),

// und bei on_menu_event
"save" => {}
"open" => {}

Doch wir sind jetzt mit einem Problem konfrontiert. Unser Markdown Dokument wird im Frontendteil bearbeitet, aber unsere Menüpunkte befinden sich im Backend. So ist es nicht so einfach möglich, den Text zu laden oder zu speichern im Rust Teil unserer Anwendung, wenn wir auf die Menüpunkte klicken. Das bedeutet, dass wir unser Frontend über Ereignisse aus dem Backend (in dem Fall den Klick auf den Button) informieren müssen. Dass geschieht über Events.

Events

Wir können mit dem Befehllet _ = event.window().emit("menu-event", "save-event").unwrap(); ein Event senden. Dabei gibt es immer einen Sender/Empfänger. Der Sender sagt, wie das Event heißt und gibt ihm eine Payload und der Empfänger kann immer auf ein bestimmtes Event hören und die Informationen aus dem Payload lesen. Dabei kann das Frontend/Backend Events senden/empfangen. In unserem Beispiel handelt es sich um ein festerspezifisches Event, was vom Backend zum Frontend geschickt wird. Dabei hat das Event den Namen „menu-event“ und den Payload „save-event“. Dies fügen wir bei den match case für „save“ hinzu. Für „open“ ändern wir den Payload auf „open-event“. Später wollen wir anhand des Payloads entscheiden, welches Menu geöffnet werden soll. Auf der Frontend-Seite erstellen wir nun zwei useState()-Variablen. Die erste Variable heißt menuPayload und nimmt den Menü-Ereignistyp-String als Daten auf. Die zweite Variable heißt menuOpen und dient als Boolean, um zu prüfen, ob das Menü geöffnet sein soll.

const [menuPayload, setMenuPayload] = useState("");
const [menuOpen, setMenuOpen] = useState(false);

Außerdem erstellen wir einen useEffect-Hook: useEffect(() => {}, []) Damit lauschen wir auf das Ereignis „menu-event“ und geben den Payload an console.log zurück.

useEffect(() => {
    listen("menu-event", (e) => {
        console.log(e);
    });
}, []);

Anhand des Payloads, wird nun das richtige Menu ausgewählt:

useEffect(() => {
    if (menuOpen) {
      switch (menuPayload) {
        case "open-event":
          OpenFile();
          break;
        case "save-event":
          SaveFile();
          break;

        default:
          break;
      }
      setMenuOpen(false)
    }
}, [menuOpen]);

Öffnen von Dateien

Dies passiert sehr einfach. Tauri gibt uns die Möglichkeit, aus dem Frontend ein Systemdialog zu öffnen. Dabei handelt es sich um asynchrone Operationen, weshalb wir zwei useEffect Hooks verwenden, da es sonst sehr schnell zu Problemen kommen kann.
Doch bevor wir das tun, müssen wir die Methoden in tauri.config.json erlauben:

"allowlist": {
    "fs": {
        "writeFile": true,
        "readFile": true
    },
    "path": {
        "all": true
    },
    "dialog": {
        "open": true,
    "save": true 
    }
},

Um jetzt eine Datei zu öffenen nutzen wir ein try catch Statement, falls was während des Prozess schief geht. let filepath = await open(); öffnet ein natives Systemmenü und gibt uns einen filepath zurück. let content = await readTextFile(filepath); liest den Inhalt der Datei, die wir ausgewählt haben. Zum Schluss speichern wir den Inhalt in Settext(content);.

Speichern von Dateien

Sehr ähnlich sieht es dann auch beim Speichern aus. Mit let filepath = await save(); bekommen wir den Ort der Datei, die wir mit await writeFile({contents: text, path: filepath,}); schreiben
Beide Funktionen sehen nun folgendermaßen aus:

const OpenFile = async () => {
    try {
        let filepath = await open();
        let content = await readTextFile(filepath);
        Settext(content);
    } catch (e) {
        console.log(e);
    }
};
const SaveFile = async (text) => {
    try {
        let filepath = await save();
        await writeFile({contents: text, path: filepath,});
    } catch (e) {
    console.log(e);
    }
};

Nun ist unsere Anwendung fertig.

Fazit

Mit diesen wenigen Zeilen an Code konnten wir schon eine kleine Anwendung erstellen. Mit gerade 4,5 Mb (Debian Paket, als Release Build) besitzt es eine sehr akzeptable Größe für solche Anwendungen. Auch haben wir in unserem Frontend die Möglichkeit, sehr einfach ein modern aussehendes UI umzusetzen. Dies bildet wahrscheinlich auch einen großen Vorteil von diesem Weg, Desktop Anwendungen zu bauen. Man kann auf alle Technologien und Entwicklungen des Webs setzen. Auch ist man nicht an eine Plattform gebunden. Jedoch muss man sich auch bewusst sein, dass Tauri noch ganz frisch ist. Dieser Artikel wurde mit dem 14. Release Kandidaten der ersten 1.0 Version geschrieben. Es ist schon faszinierend, was alles jetzt schon möglich ist und um so interessanter, was alles noch kommt (Mobile Bundler, Cross Compiler …). Auch ist die erste Resonanz aus der Entwicklercommunity äußerst positiv, wenn es nach dem State of JS 2021 geht. Ich würde schon sagen, dass Tauri eine sehr interessante Zukunft vor sich hat.


Beitrag veröffentlicht

in

, ,

von

Schlagwörter:

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert