初次体验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:
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 OpenSSLinstallation. If there isn't one installed then you can try the rust-opensslREADME for more information about how to download precompiled binaries ofOpenSSL:https://github.com/sfackler/rust-openssl#windowsnote: 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
函数:
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(有参数有返回值):
#[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.withGlobalTauri
为true
。
于是,invoke
函数就在这里:window.__TAURI__.core.invoke
。Rust前端和TS/JS前端都能使用。
调用实例:
#[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。
import { emit } from '@tauri-apps/api/event';
await emit('frontend-loaded', { loggedIn: true, token: 'authToken' });
emit()
实际上只是对invoke()
的简单封装:
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
例如:
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()
的简单封装,例如:
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相关体验,我会发布在本系列后面的文章中,敬请期待。