使用 Tauri 和 Yew 在 Rust 中创建桌面应用程序
我最近用 Tauri 和 Yew 在 Rust 中创建了一个完整的桌面应用。有几个人对我的实现方式很感兴趣,所以这里就写了一个教程来演示如何上手。
如果您喜欢 Rust 并想构建桌面应用,Tauri 是个不错的选择。Rust 的 Web 前端开发还处于早期阶段,但正如您所见,Yew 已经相当好用了。
那么让我们开始吧。
首先,让我们为新项目创建一个目录。我们将创建一个 monorepo,其中包含用于前端和后端的单独目录。
mkdir tauri-yew-demo
cd tauri-yew-demo
git init
如果您愿意,您可以为其创建一个新的 github 存储库并按照此处的说明添加 git origin 。
如果您只想下载并继续学习本教程,本教程中的所有代码都可以在我的 github 上找到:
https://github.com/stevepryde/tauri-yew-demo
前端
接下来我们将创建前端目录。我们先创建它,因为稍后tauri
安装程序会询问该目录的位置。
cargo new --bin frontend
cd frontend
mkdir public
(该public
目录是我们的 CSS 存放的地方)
由于这将是Yew
项目的一部分,让我们按照此处的设置说明进行操作:https://yew.rs/docs/getting-started/introduction
rustup target add wasm32-unknown-unknown
cargo install trunk
cargo install wasm-bindgen-cli
index.html
接下来在目录底部创建frontend
:
前端/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Tauri Yew Demo App</title>
<link data-trunk rel="css" href="/public/main.css"/>
</head>
<body></body>
</html>
我们还将在之前创建的目录main.css
中创建:public/
前端/公共/main.css
body {
margin: 20px;
background: #2d2d2d;
color: white;
}
.heading {
text-align: center;
}
现在,您可以构建前端应用并在浏览器中查看它了!让我们测试一下:
trunk build
trunk serve
现在打开浏览器并转到http::/localhost:8080
。您应该会看到一个带有灰色背景的空白页面。
您可能想知道,既然我们应该创建桌面应用,为什么却要在浏览器中查看这个页面。我们很快就会讲到。Tauri 本质上是将您的 Web 应用捆绑到桌面应用中,使用操作系统提供的浏览器渲染引擎来显示您的前端应用。因此,创建前端部分的过程与创建常规 Web 前端非常相似,不同之处在于,我们不是向外部 Web 服务器发送 HTTP 请求,而是通过一些 JavaScript 粘合代码(我保证,几乎不需要 JavaScript!)向 Tauri 后端进行函数调用。
添加紫杉
我们还没有添加yew
到项目中,所以现在就这样做吧。
要将 rust 包安装到您的项目中,我强烈推荐该cargo-edit
工具,您可以通过 进行安装cargo install cargo-edit
。
安装后cargo-edit
您可以执行以下操作:
cargo add yew
如果您希望手动添加依赖项,只需将以下几行添加到dependencies
部分Cargo.toml
:
yew = "0.19.3"
现在打开src/main.rs
并将其内容替换为以下内容:
前端/src/main.rs
use yew::prelude::*;
fn main() {
yew::start_app::<App>();
}
#[function_component(App)]
pub fn app() -> Html {
html! {
<div>
<h2 class={"heading"}>{"Hello, World!"}</h2>
</div>
}
}
现在,您应该可以重新运行trunk serve
并在浏览器中看到更新后的页面。这是一个真正的 Rust 前端,已编译为 WASM,并在浏览器中运行。没错,就是这么简单!
顺便说一句,如果您希望制作一个常规的
Yew
Web 前端,您可以遵循完全相同的过程,并将该前端作为独立项目继续进行。
添加 Tauri
此步骤需要返回基tauri-yew-demo
目录。Ctrl+C
如果您仍在运行,请点击trunk serve
。
如果您仍在frontend
目录中,请转到上一级目录:
cd ..
Tauri 有自己的 CLI,我们将使用它来管理应用程序。
有多种安装选项,但由于我们使用的是 Rust,因此我们将通过 来安装它cargo
。
Tauri CLI 安装说明可在此处找到:https://tauri.studio/docs/getting-started/beginning-tutorial#alternatively-install-tauri-cli-as-a-cargo-subcommand
cargo install tauri-cli --locked --version ^1.0.0-rc
补充:许多读者在安装 Tauri CLI 时遇到了问题,原因是之前版本存在各种 bug,或者与较新版本的 Rust 存在兼容性问题。我建议你查看官方文档并按照其中的说明操作,尤其是在 Tauri CLI 版本未来更新的情况下。
然后运行,cargo tauri init
它会询问你一些关于该应用程序的问题:
$ cargo tauri init
What is your app name?: tauri-yew-demo
What should the window title be?: Tauri Yew Demo
Where are your web assets (HTML/CSS/JS) located, relative to the "<current dir>/src-tauri/tauri.conf.json" file that will be created?: ../frontend/dist
What is the url of your dev server?: http://localhost:8080
这将创建一个src-tauri
目录,其中包含应用程序的所有后端代码。
您可以运行cargo tauri info
来检查所有安装详细信息。
在运行应用程序之前,我们需要了解tauri
如何实际启动前端服务器。
编辑目录tauri.conf.json
中的文件src-tauri
。此文件包含所有 Tauri 配置,用于指导 Tauri 如何构建您的应用。您需要将https://tauri.studio/en/docs/api/config/添加到书签中,因为它包含有关此文件中所有选项的大量实用信息。目前,我们只需更新构建设置即可。
build
用以下内容替换该部分:
src-tauri/tauri.conf.json
"build": {
"distDir": "../frontend/dist",
"devPath": "http://localhost:8080",
"beforeDevCommand": "cd frontend && trunk serve",
"beforeBuildCommand": "cd frontend && trunk build",
"withGlobalTauri": true
},
这意味着我们在开发时无需手动切换trunk serve
到另一个选项卡。我们可以在前端和后端进行开发,并且当发生更改时,两者都会自动热加载。
好的,我们现在准备启动您在开发应用程序时将使用的 tauri 开发服务器:
cargo tauri dev
这就是您的桌面应用程序,正在运行tauri
!
添加 tauri 命令
Tauri 本身是用 Rust 编写的,您的后端应用程序将是一个 Rust 应用程序,它通过 Rust 提供“命令”,其方式与 Rust Web 服务器提供路由的方式非常相似。您甚至可以管理数据库tauri
等状态!sqlite
src/main.rs
在项目中打开src-tauri
并在函数下方添加以下内容main()
:
src-tauri/src/main.rs
#[tauri::command]
fn hello(name: &str) -> Result<String, String> {
// This is a very simplistic example but it shows how to return a Result
// and use it in the front-end.
if name.contains(' ') {
Err("Name should not contain spaces".to_string())
} else {
Ok(format!("Hello, {}", name))
}
}
您还需要main()
稍微修改该函数以告知tauri
您新的命令:
src-tauri/src/main.rs
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![hello])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
这就是后端的全部内容。
有关命令的更多详细信息
tauri
,包括异步命令、错误处理和访问托管状态,强烈建议您阅读此处文档中的所有部分tauri
:https://tauri.studio/en/docs/guides/command
在前端通过 Rust 访问 tauri 命令
为了在 Rust/WASM 中访问前端的 tauri 命令,我们需要在 Javascript 中添加一些粘合代码。
有一个tauri
npm 包,但由于我们希望将 Javascript 的使用保持在最低限度,并且更喜欢使用cargo
和trunk
管理我们的前端,因此我们将使用不同的方法。
Tauri 还通过window
Javascript 中的对象导出其功能。
在我们的front-end
项目中,创建一个名为目录glue.js
内的新文件public/
。
前端/公共/glue.js
const invoke = window.__TAURI__.invoke
export async function invokeHello(name) {
return await invoke("hello", {name: name});
}
这就是我们要添加的所有 Javascript 代码。我说过,代码量会很小!
请注意,该 JavaScript 函数是异步的。这是因为 tauriinvoke()
命令返回的是 JavaScript 的 Promise。通过 WASM,JavaScript Promise 可以转换为 Rust Future。稍等片刻,感受一下它的魅力吧 :)
我们如何从 Rust 调用该 JavaScript 函数?我们添加了更多粘合代码,这次是在 Rust 中。在前端中添加以下内容main.rs
:
前端/src/main.rs
#[wasm_bindgen(module = "/public/glue.js")]
extern "C" {
#[wasm_bindgen(js_name = invokeHello, catch)]
pub async fn hello(name: String) -> Result<JsValue, JsValue>;
}
这里我们告诉 wasm_bindgen 我们的 javascript 代码在 Rust 中/public/glue.js
。我们给它一个 javascript 函数名,catch
参数告诉 wasm_bindgen 我们想要catch()
为 javascript promise 添加一个处理程序,并将其转换为Result
Rust 中的。
这段新代码暂时还不能运行,因为我们还没有添加wasm-bindgen
前端依赖项。现在就来添加它。我们还会添加wasm-bindgen-futures
稍后调用它的方法,web-sys
它提供对浏览器window
对象的访问,并js-sys
提供JsValue
上面引用的类型。
cargo add wasm-bindgen
cargo add wasm-bindgen-futures
cargo add web-sys
cargo add js-sys
Cargo.toml
这将在前端添加以下依赖项:
wasm-bindgen = "0.2.78"
wasm-bindgen-futures = "0.4.28"
web-sys = "0.3.55"
js-sys = "0.3.55"
我们还需要在 main.rs 的顶部添加一些导入。
前端/src/main.rs
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::window;
现在我们已经添加了 JavaScript 的胶水代码和调用该方法的 Rust 代码。我们需要做的是将它们连接到前端代码并实际使用它。
使用 Yew 代码调用 Tauri 命令
Yew 在 0.19 版本中新增了函数组件,如果你熟悉 JavaScript 中的 React 框架,它的操作方式与 React hooks 非常相似。如果你不熟悉 React hooks,我会尝试简要解释本教程中使用的概念,但对 hooks 的更完整解释超出了范围。Yew 文档本身对此进行了更详细的介绍。
将前端中的功能组件替换main.rs
为以下内容:
前端/src/main.rs
#[function_component(App)]
pub fn app() -> Html {
let welcome = use_state_eq(|| "".to_string());
let name = use_state_eq(|| "World".to_string());
// Execute tauri command via effects.
// The effect will run every time `name` changes.
{
let welcome = welcome.clone();
use_effect_with_deps(
move |name| {
update_welcome_message(welcome, name.clone());
|| ()
},
(*name).clone(),
);
}
let message = (*welcome).clone();
html! {
<div>
<h2 class={"heading"}>{message}</h2>
</div>
}
}
fn update_welcome_message(welcome: UseStateHandle<String>, name: String) {
spawn_local(async move {
// This will call our glue code all the way through to the tauri
// back-end command and return the `Result<String, String>` as
// `Result<JsValue, JsValue>`.
match hello(name).await {
Ok(message) => {
welcome.set(message.as_string().unwrap());
}
Err(e) => {
let window = window().unwrap();
window
.alert_with_message(&format!("Error: {:?}", e))
.unwrap();
}
}
});
}
这应该可以编译并运行。
导航到上一个目录(到该tauri-yew-demo
目录),然后运行:
cargo tauri dev
您应该会看到应用程序窗口显示Hello, World!
。
好了,代码有点多。我们来分析一下。
首先我们通过钩子向组件添加一些状态use_state_eq
。
let welcome = use_state_eq(|| "".to_string());
let name = use_state_eq(|| "World".to_string());
组件状态有一个初始值,以及一个更新该值的方法。通常,更新状态值会导致组件使用新值重新渲染。
这个use_state_eq
钩子为我们提供了一个状态句柄,只有当值发生变化时,它才会重新渲染组件。它使用了 Rust 的PartialEq
trait 来实现这一点。
您可以取消引用UseStateHandle
以获取底层状态值。
{
let welcome = welcome.clone();
use_effect_with_deps(
move |name| {
update_welcome_message(welcome, name.clone());
|| ()
},
(*name).clone(),
);
}
我们需要克隆welcome
句柄,以便将其移动到闭包中。
该use_effect_with_deps()
钩子是钩子的变体use_effect
,其中仅当某些依赖项发生变化时才运行钩子。
它接受两个参数。第一个参数是依赖项值改变时我们想要运行的闭包,第二个参数是依赖项值本身。
在此示例中,依赖项被指定为(*name).clone()
,这意味着我们正在取消引用句柄以获取底层String
,然后克隆,String
因为我们无法移动它。
然后将该值作为参数传递给闭包。
钩子use_effect*()
通常用于执行一些耗时较长的操作或异步操作,组件会根据这些操作的结果进行更新。这就是为什么我们传入welcome
句柄的原因,因为这允许闭包设置该句柄的值,这将导致组件重新渲染,但前提是该值已更改。对 tauri 命令的调用通常会在use_effect
或use_effect_with_deps
钩子内部进行。
闭包的返回值始终是另一个闭包,该闭包将在组件从虚拟 DOM 中卸载时运行。您可以使用此闭包执行与此效果钩子相关的任何清理操作。在本例中,没有任何需要清理的操作,因此我们返回一个空闭包。
让我们看一下该update_welcome_message()
函数。
fn update_welcome_message(welcome: UseStateHandle<String>, name: String) {
spawn_local(async move {
match hello(name).await {
Ok(message) => {
welcome.set(message.as_string().unwrap())
}
Err(e) => {
...
}
}
});
}
该spawn_local()
函数来自wasm_bindgen_futures
,它允许我们在当前线程中运行异步函数。在这种情况下,我们需要它来调用我们的hello()
函数,因为它是异步的。
接下来我们调用hello()
函数,它返回Result<JsValue, JsValue>
。JsValue
由于我们实现的原始 tauri 命令返回了Result<String, String>
,所以 是一个字符串,因此我们在这里可以直接使用 unwrap() 。
最后,如果结果为Ok(..)
真,则设置welcome
状态变量的新值。否则,显示警报。
警告并非处理此类错误的好方法,但作为一个简单的示例,它展示了
Result
类型如何传递到前端。在正式的应用程序中,您可能希望显示一个 Toast 弹出窗口,并可选地将日志记录到浏览器控制台(在调试版本中)。
请参阅wasm-logger
crate,了解如何使用 crate 宏将日志发送到浏览器控制台的简便方法log
。
总结
希望本教程能让您了解如何使用 Rust 几乎完全创建桌面应用程序,并将其用于Tauri
后端和Yew
前端。
您可以在我的 github 上找到本教程的所有代码:
https://github.com/stevepryde/tauri-yew-demo
请随意使用和修改它作为您自己项目的基础。
我强烈建议阅读 Tauri 和 Yew 的文档。
金牛座:https://tauri.studio/en/docs/get-started/intro
紫杉:https ://yew.rs/docs/getting-started/introduction
如果本教程中我遗漏了任何内容或还有任何不清楚的地方,请在评论中告诉我,我会尽力更新它。
感谢阅读!
文章来源:https://dev.to/stevepryde/create-a-desktop-app-in-rust-using-tauri-and-yew-2bhe