初次体验Tauri和Sycamore(1)

原创作者:庄晓立(LIIGO)

原创时间:2024年11月10日

原创链接:https://blog.csdn.net/liigo/article/details/143666827

版权所有,转载请注明出处。

前言

Tauri 2.0发布于2024年10月2日,Sycamore 0.9发布于2024年11月1日。二者在近期双双发布重大版本升级,是我(LIIGO)本次想体验他们的主要动机。Tauri自2022年发布v1.0之后就早已火出天际,而Sycamore自2022发布v0.8之后沉寂了两年之久,如今各自凤凰涅槃,他们的组合体会擦出怎样的火花?

关于Tauri

Tarui用于创建小巧、快速、安全、跨平台的桌面GUI应用和移动应用软件。

Create small, fast, secure, cross-platform applications.

Tauri APP主要由三个子系统组成,Webview + Shell + Rust。其中Webview是前端WEB载体,用于展现UI与用户交互;Shell是Webview的载体,提供窗口/菜单/通知等OS级支撑;Rust是后端核心,为前端提供功能支撑和运行时支撑。前端WebUI + 后端Rust业务逻辑,整合了WEB在布局和UI的优势以及Rust后端功能性能优势,是颇具竞争力的跨平台GUI应用开发模式,且已被Electron和Tauri实证有效。在Rust编程领域,主流NativeUI框架稀缺或生态不成熟,主动拥抱生态极其丰富的JS/TS前端框架是明智之举。

为什么需要Rust在后端提供功能支撑呢?因为前端App运行在Webview中,那是沙箱环境,其功能受限(例如不能启动OS进程/执行CLI),因而需要寻求拓展外界支持。对于Electron而言,由Nodejs提供后端支撑;对于Tauri而言,由Rust提供后端支撑。这也是Tauri和Electron的本质区别。区别的本质是不同程序员群体做出的不同倾向性选择:Rust程序员倾向于选择Rust,JS/TS程序员倾向于选择Nodejs。Tauri的核心运行时库也被编译进后端子系统,跟Tauri App的后端业务逻辑代码处于同一Crate。

Tauri 2.0开始引入插件(Plugins)生态,并且预置了一批立等可取的插件,可视为对后端的拓展,缓解了对后端的强依赖,对非Rust开发者更是福音。它的插件往往是跨桌面系统(Windows/Linux/macOS)和移动系统(Android/iOS)提供统一的接口。除了官方插件外,还有第三方插件可供选择。

抛开后端再看Webview,Tauri和Electron还有一个重大区别:Electron App内嵌携带Webview即Chromium;而Tauri App不携带Webview,它直接使用App用户OS里的Webview(Windows内的WebView2,macOS内的WKWebView,Linux内的webkit2gtk)。Electron的优势是Webview已知、确认与App兼容,劣势是App尺寸过大(上百MB)。Tauri的优势是App尺寸很小(十余MB),劣势是Webview未知、与App兼容性未知。此处Tauri的劣势是可控的、可接受的,理由如下:1) 各主流平台的Webview已经逐渐趋同(用现代的或相同的浏览器内核);2) Tauri为App生成的安装程序(Installer)会协助用户获取兼容Webview;3) WEB前端App原本就不应该使用厂商专用特性。4) 做网站也是要面对不同的Webview,该踩得坑全球网友已帮你踩过了。

作为Tauri App的开发者,你没法选Webview(由目标用户的操作系统而定),也没法选后端语言(确定使用Rust语言),但你可以自由的选择前端框架(取决于你个人或团队的习惯和偏爱)。类似于Electron,Tauri也支持多种主流前端框架,如React、Vue、Svelte、Solid、Angular、Preact等等。Tauri不仅支持前述JS/TS前端,还支持WASM前端,如Yew、Leptos、Sycamore、Dioxus等(Rust语言),以及.Net的Blazor前端(C#语言)。当然你也可以不用任何框架(Vanilla,纯JavaScript/CSS/HTML)。

创建App

Tauri使用两个命令行工具 (create-tauri-app, tauri-cli) 创建和编译打包App。首先要安装这两个CLI:

shell 复制代码
cargo install create-tauri-app --locked
cargo install tauri-cli --version "^2.0.0" --locked

create-tauri-app, tauri-cli 从Rust源码编译(cargo install)都相当耗时(均依赖数百个crates)。我还是建议自行下载编译后版本放到cargo bin目录。我给他们提了建议,今后会推荐使用 cargo binstall 下载编译好的二进制CLI,而不是使用cargo install从源码开始编译。

这两个CLI也有都对应的npm包:create-tauri-app, @tauri-apps/cli,二者都是间接调用Rust编译好的可执行文件。供TS/JS前端使用。

执行如下命令开始创建Tauri app:cargo create-tauri-app。CLI会逐步引导你输入或选择如下信息:

  • 项目名称(Project name),默认是"tauri-app"
  • Identifier 默认是"com.tauri-app.app"
  • 前端语言,可选 Rust, TS/JS, .Net
  • UI模板,视前端语言而定
    • Rust UI模板:可选 Vanilla, Yew, Leptos, Sycamore, Dioxus
    • TS/JS UI模板:可选 Vanilla, Vue, Svelte, React, Solid, Angular, Preact
    • .Net UI模板:可选 Blazor

选TS/JS的UI模板前还需要选择包管理器:pnpm, yarn, npm, deno, bun

因为这次我(Liigo)想体验Tauri+Sycamore,因而前端语言选Rust,UI模板选Sycamore。

目录结构

Tauri+Sycamore App目录结构:

├─ public/
├─ src/
│  ├─ app.rs
│  └─ main.crs
├─ src-tauri/
│  ├─ ...
│  ├─ Cargo.toml
│  └─ tauri.conf.json
├─ .gitignore
├─ .taurignore
├─ Cargo.toml
├─ index.html
├─ README.md
├─ styles.css
└─ Trunk.toml

Tauri源码目录对前端和后端代码进行了隔离。后端代码使用src-tauri/子目录;前端代码使用除此之外的其他文件和子目录。

编译打包

开发版

cargo tauri dev

编译完成后自动启动App,弹出GUI主窗口。允许开发者在App运行过程中修改前端源代码,Tauri(或者说Trunk)会自动编译,并刷新App窗口内容,但是App并不会中途退出或重启(原理:Trunk通过WebSocket向开发版App推送重新加载UI的指令;Dioxus虽然没用Trunk但也实现了类似机制)。

它会检查Trunk是否存在,不存在的话会自动下载源码并编译。但是在Windows下编译Trunk很可能会碰到如下问题(间接依赖openssl开发者库):

  It looks like you're compiling for MSVC but we couldn't detect an OpenSSL
  installation. If there isn't one installed then you can try the rust-openssl
  README for more information about how to download precompiled binaries of
  OpenSSL:

  https://github.com/sfackler/rust-openssl#windows

  note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
warning: build failed, waiting for other jobs to finish...
error: failed to compile `trunk v0.21.2`, intermediate artifacts can be found at `E:\tmp\RUST_DIR\CARGO_TARGET_DIR`.
To reuse those artifacts with a future compilation, set the environment variable `CARGO_TARGET_DIR` to that path.

我找到的解决办法是,去github的trunk官方仓库下载编译好的trunk.exe,丢进cargo bin目录即可(或任意PATH目录均可)。

如果cargo tauri dev过程中看到如下提示时只需耐心等待:

Warn Waiting for your frontend dev server to start on http://localhost:1420/...

这是因为Trunk先启动了(WEB服务监听1420端口),等待APP主动连接。但是编译App需要时间,等它编译完并启动后才能连上。

通过App窗口右键菜单"检查"可以打开devtools。在App运行过程中,还可以在浏览器中打开 http://localhost:1420/ ,网页外观和功能跟App窗口是一样的(可视为App的另一个实例)。

发行版

cargo tauri build

编译App并打包为安装包。

如果编译过程中提示正在下载Wix但失败(Github国内连接不稳定):

Downloading https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314-binaries.zip

你可以通过其他方法手动下载此连接,解压到如下目录:C:\Users\liigo\AppData\Local\tauri\WixTools314\(里面有一堆exe等文件)。

此方法是我(LIIGO)从 Tauri仓库源码 里扒出来的。实证管用。

同理,如果NSIS也下载不了,可以用类似的办法手动下载解压到目录C:\Users\liigo\AppData\Local\tauri\NSIS。但是我没用这个方法。因为我觉得,既然已经有Wix用来生成MSI安装包,就没必要再下载NSIS用来生成另一种安装包。我研究了一下,将配置文件tauri.conf.json里面的bundle.targets改为"msi"(原来是"all")即可禁用NSIS等。

文件大小

Tauri+Sycamore app编译后是一个可独立运行的图形用户界面(GUI)exe,其内部整合了wasm/css/图片等文件,没有其他外部依赖。exe文件大小是10.3MB,对应的安装包msi文件大小是3.6MB(安装后也只有那个exe和一个用于卸载的快捷方式文件(指向系统文件msiexec.exe /x))。Sycamore生成的wasm文件大小为750KB(已包含在exe中)。App启动时有大约一两秒的窗口白屏。

作为对比,再看一下Tauri+Dioxus app的数据:exe大小10.6MB,msi大小3.9MB,wasm文件大小为1.3MB(debug版33MB或25MB),也有一两秒的启动白屏。大同小异吧。我(LIIGO)暂且认为这是Tauri App (Hello world)的平均水平。

这样的文件大小应该很香吧。最起码比Electron app香多了。

1MB的wasm文件,用在普通网站上,网络传输加载延迟是一个较大的负担,但是对Tauri app这种桌面应用而言,就是本地加载啊,性能没得说。况且Tauri还用了"Localhost free"技术,直接注入Webview,连本地WEB传输步骤也省了。

资源占用

Tauri+Sycamore app启动后,内部加载3到6个Webview2进程,连同exe合计占用内存60到90MB。

无操作时CPU占用率为0%;在app窗口上移动鼠标时,CPU占用率逐步上升到10%甚至更多。这个问题是不是需要改善呀。

Tauri+Dioxus app的表现与之类似。

命令(Commands)

前端可以调用后端定义的Command。反之则不行,因为只有后端有Command,前端没有。

TS/JS前端调用Command

Tauri给前端提供了调用后端Command的通用接口,invoke函数:

ts 复制代码
import { invoke } from '@tauri-apps/api/core';
const result = invoke('greet', 'liigo'); // 调用后端greet命令并接收返回值

@tauri-apps/api是Tauri发布到npm的多个package之一,可供Tauri所有TS/JS前端框架使用。

示例greet是在后端Rust代码中自定义的Command(有参数有返回值):

rust 复制代码
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

注意:新增Command后需要同步更新lib.rs文件内的tauri::generate_handler![greet]调用。

此处涉及的前后端数据传输,本质是进程间通讯(IPC),invoke()底层利用的是各Webview的专有实现,调用链:invoke, ipc, postMessage, window.ipc.postMessage, window.chrome.webview.postMessage, WRY crate。传输的内容是JSON文本。前后端要传输的值,传输前需序列化到JSON,传输后再从JSON反序列化。后端依赖serde_json crate。

Rust前端调用Command

但是,我现在用的是Rust前端啊,@tauri-apps/api用不上啊。

没关系,Tauri还通过另一种方式为前端提供接口:window.__TAURI__,大致等效于@tauri-apps/api。此接口存在的前提是事先修改tauri.conf.json文件配置app.withGlobalTauritrue

于是,invoke函数就在这里:window.__TAURI__.core.invoke。Rust前端和TS/JS前端都能使用。

调用实例:

rust 复制代码
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])]
    async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}

#[derive(Serialize, Deserialize)]
struct GreetArgs<'a> {
    name: &'a str,
}

let name = "liigo";
let args = serde_wasm_bindgen::to_value(&GreetArgs { name }).unwrap();
let new_msg = invoke("greet", args).await.as_string().unwrap();

可以看出,从Rust前端调用Rust后端,反而比TS/JS更麻烦。麻烦的点是:

  • 需要借助#[wasm_bindgen]声明invoke函数
  • 需要定义被调用Command的参数struct并支持序列化/反序列化JSON
  • Command的参数值需要先转换为JsValue类型才能传入invoke函数
  • invoke返回值是JsValue类型,还要转换类型才能使用

我想这可能是大伙儿不愿意在Tauri中使用Rust前端的原因之一。不知道后续能不能改善。现阶段还是使用TS/JS前端更有生产力,生态也更成熟。

题外话:Rust前端调用Rust后端?都是Rust,代码直接写在前端是不是就不用调用后端了?你想多了。Rust前端是被编译成WASM、在Webview沙箱环境中执行的,其功能也是受限的,有时候不可避免依赖Rust后端。

事件(Events)

前端可以给后端发送事件,后端也可以给前端发送事件。

前端给后端发送Event

TS/JS前端使用@tauri-apps/api package 内的emit()函数给后端发送Event。

ts 复制代码
import { emit } from '@tauri-apps/api/event';
await emit('frontend-loaded', { loggedIn: true, token: 'authToken' });

emit()实际上只是对invoke()的简单封装:

ts 复制代码
async function emit(event, payload) {
    await invoke('plugin:event|emit', { event, payload });
}

(上述invoke调用中的'plugin:event|emit'似乎暗示event也是一个插件?我研究后发现的确如此。)

Rust前端给后端发送Event,比照前文调用window.__TAURI__.event.emit()window.__TAURI__.core.invoke()

后端给前端发送Event

使用 tauri crate:

  • tauri::AppHandle::emit
  • tauri::App::emit
  • tauri::webview::WebviewWindow::emit
  • tauri::webview::Webview::emit
  • tauri::window::Window::emit

例如:

rust 复制代码
use tauri::Emitter;

#[tauri::command]
fn synchronize(window: tauri::Window) {
  window.emit("synchronized", ());
}

Rust前端如何获取tauri::Window对象呢?我估计是比照前文调用window.__TAURI__.window.getCurrentWindow()

通道(Channels)

通道用于在前后端之间快速双向传输大块数据、流数据,传输是有序的,先发先到。

通道在前端的JavaScript类型是@tauri-apps/api/core/Channel,位于@tauri-apps/api package内;通道在后端的Rust类型是tauri::ipc::Channel,位于tauri crate内。以上二者是Channel的一体两面,同一个Channel对象,在前端表现为JS Channel,在后端表现为Rust Channel。其底层如何实现的,我还不太清楚。

应用示例:https://tauri.app/develop/calling-frontend/#channels

插件(Plugins)

前端和后端都可以调用插件。

Tauri插件本身就是Rust的crate(例如tauri-plugin-dialog),在后端Rust代码中调用插件,跟调用其他crate一样,直接cargo add就OK了。

每个插件都有对应的npm包(例如@tauri-apps/plugin-dialog)提供TS/JS接口(通常是自动化生成),供TS/JS前端调用。如果是Rust前端呢,大概要麻烦一些,参考前端调用Command。

前面用于调用Command的invoke()函数也具备调用插件的功能,调用语法是:invoke('plugin:插件名|函数名', { 参数 })

插件的TS/JS接口实际上也只是对invoke()的简单封装,例如:

ts 复制代码
async function open(options = {}) {
    if (typeof options === 'object') {
        Object.freeze(options);
    }
    return await invoke('plugin:dialog|open', { options });
}

总结

近两年来Tauri受到大量关注,其Github仓库收获STAR已高达84.7K,逐渐逼近Electron的114K。它不仅吸引了许多Rust用户,还有很多前端非Rust用户尝试用它开发跨平台桌面应用程序。在后Tauri 2.0时代可以预见会有越来越多的开发者用它开发跨平台移动应用APP(安卓Android + 苹果iOS)。

我虽然也一直观望着Tauri的开发进展,但沉下心来仔细体验它,这还是头一次。借此机会,大致介绍了它的功能特点,初步总结了它的基础用法,希望对感兴趣的朋友们有所帮助。

感觉TS/JS前端与Tauri后端的结合更妥帖,使用更方便,生态更好。Rust前端与Tauri后端之间交互较为麻烦,尚需继续打磨。

关于标题中提到的Sycamore相关体验,我会发布在本系列后面的文章中,敬请期待。

相关推荐
SomeB1oody5 小时前
【Rust自学】4.1. 所有权:栈内存 vs. 堆内存
开发语言·后端·rust
SomeB1oody19 小时前
【Rust自学】4.2. 所有权规则、内存与分配
开发语言·后端·rust
SomeB1oody19 小时前
【Rust自学】4.5. 切片(Slice)
开发语言·后端·rust
编码浪子1 天前
构建一个rust生产应用读书笔记6-拒绝无效订阅者02
开发语言·后端·rust
baiyu331 天前
1小时放弃Rust(1): Hello-World
rust
baiyu331 天前
1小时放弃Rust(2): 两数之和
rust
Source.Liu1 天前
数据特性库 前言
rust·cad·num-traits
编码浪子1 天前
构建一个rust生产应用读书笔记7-确认邮件1
数据库·rust·php
SomeB1oody1 天前
【Rust自学】3.6. 控制流:循环
开发语言·后端·rust
Andrew_Ryan1 天前
深入了解 Rust 核心开发团队:这些人如何塑造了世界上最安全的编程语言
rust