初次体验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相关体验,我会发布在本系列后面的文章中,敬请期待。

相关推荐
幸运小圣3 小时前
Vue3 -- 项目配置之stylelint【企业级项目配置保姆级教程3】
开发语言·后端·rust
ZJ_.3 小时前
Electron 沙盒模式与预加载脚本:保障桌面应用安全的关键机制
开发语言·前端·javascript·vue.js·安全·electron·node.js
fanxbl9573 小时前
Electron 项目实现下载文件监听
javascript·electron·状态模式
老猿讲编程4 小时前
Rust编写的贪吃蛇小游戏源代码解读
开发语言·后端·rust
yezipi耶不耶11 小时前
Rust 所有权机制
开发语言·后端·rust
喜欢打篮球的普通人11 小时前
rust并发
rust
大鲤余14 小时前
Rust开发一个命令行工具(一,简单版持续更新)
开发语言·后端·rust
梦想画家14 小时前
快速学习Serde包实现rust对象序列化
开发语言·rust·序列化
数据智能老司机18 小时前
Rust原子和锁——Rust 并发基础
性能优化·rust·编程语言
怕冷的火焰(~杰)20 小时前
创建vue+electron项目流程
vue.js·electron