使用 Tauri 和 Yew 在 Rust 中创建桌面应用程序

2025-05-27

使用 Tauri 和 Yew 在 Rust 中创建桌面应用程序

我最近用 Tauri 和 Yew 在 Rust 中创建了一个完整的桌面应用。有几个人对我的实现方式很感兴趣,所以这里就写了一个教程来演示如何上手。

如果您喜欢 Rust 并想构建桌面应用,Tauri 是个不错的选择。Rust 的 Web 前端开发还处于早期阶段,但正如您所见,Yew 已经相当好用了。

那么让我们开始吧。

首先,让我们为新项目创建一个目录。我们将创建一个 monorepo,其中包含用于前端和后端的单独目录。



mkdir tauri-yew-demo
cd tauri-yew-demo
git init


Enter fullscreen mode Exit fullscreen mode

如果您愿意,您可以为其创建一个新的 github 存储库并按照此处的说明添加 git origin 。

如果您只想下载并继续学习本教程,本教程中的所有代码都可以在我的 github 上找到:
https://github.com/stevepryde/tauri-yew-demo

前端

接下来我们将创建前端目录。我们先创建它,因为稍后tauri安装程序会询问该目录的位置。



cargo new --bin frontend
cd frontend
mkdir public


Enter fullscreen mode Exit fullscreen mode

(该public目录是我们的 CSS 存放的地方)

由于这将是Yew项目的一部分,让我们按照此处的设置说明进行操作:https://yew.rs/docs/getting-started/introduction



rustup target add wasm32-unknown-unknown
cargo install trunk
cargo install wasm-bindgen-cli


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

我们还将在之前创建的目录main.css中创建:public/

前端/公共/main.css



body {
    margin: 20px;
    background: #2d2d2d;
    color: white;
}

.heading {
    text-align: center;
}


Enter fullscreen mode Exit fullscreen mode

现在,您可以构建前端应用并在浏览器中查看它了!让我们测试一下:



trunk build
trunk serve


Enter fullscreen mode Exit fullscreen mode

现在打开浏览器并转到http::/localhost:8080。您应该会看到一个带有灰色背景的空白页面。

您可能想知道,既然我们应该创建桌面应用,为什么却要在浏览器中查看这个页面。我们很快就会讲到。Tauri 本质上是将您的 Web 应用捆绑到桌面应用中,使用操作系统提供的浏览器渲染引擎来显示您的前端应用。因此,创建前端部分的过程与创建常规 Web 前端非常相似,不同之处在于,我们不是向外部 Web 服务器发送 HTTP 请求,而是通过一些 JavaScript 粘合代码(我保证,几乎不需要 JavaScript!)向 Tauri 后端进行函数调用。

添加紫杉

我们还没有添加yew到项目中,所以现在就这样做吧。

要将 rust 包安装到您的项目中,我强烈推荐该cargo-edit工具,您可以通过 进行安装cargo install cargo-edit

安装后cargo-edit您可以执行以下操作:



cargo add yew


Enter fullscreen mode Exit fullscreen mode

如果您希望手动添加依赖项,只需将以下几行添加到dependencies部分Cargo.toml



yew = "0.19.3"


Enter fullscreen mode Exit fullscreen mode

现在打开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>
    }
}


Enter fullscreen mode Exit fullscreen mode

现在,您应该可以重新运行trunk serve并在浏览器中看到更新后的页面。这是一个真正的 Rust 前端,已编译为 WASM,并在浏览器中运行。没错,就是这么简单!

顺便说一句,如果您希望制作一个常规的YewWeb 前端,您可以遵循完全相同的过程,并将该前端作为独立项目继续进行。

添加 Tauri

此步骤需要返回基tauri-yew-demo目录。Ctrl+C如果您仍在运行,请点击trunk serve

如果您仍在frontend目录中,请转到上一级目录:



cd ..


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

补充:许多读者在安装 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


Enter fullscreen mode Exit fullscreen mode

这将创建一个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
  },


Enter fullscreen mode Exit fullscreen mode

这意味着我们在开发时无需手动切换trunk serve到另一个选项卡。我们可以在前端和后端进行开发,并且当发生更改时,两者都会自动热加载。

好的,我们现在准备启动您在开发应用程序时将使用的 tauri 开发服务器:



cargo tauri dev


Enter fullscreen mode Exit fullscreen mode

这就是您的桌面应用程序,正在运行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))
  }
}


Enter fullscreen mode Exit fullscreen mode

您还需要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");
}


Enter fullscreen mode Exit fullscreen mode

这就是后端的全部内容。

有关命令的更多详细信息tauri,包括异步命令、错误处理和访问托管状态,强烈建议您阅读此处文档中的所有部分taurihttps://tauri.studio/en/docs/guides/command

在前端通过 Rust 访问 tauri 命令

为了在 Rust/WASM 中访问前端的 tauri 命令,我们需要在 Javascript 中添加一些粘合代码。

有一个taurinpm 包,但由于我们希望将 Javascript 的使用保持在最低限度,并且更喜欢使用cargotrunk管理我们的前端,因此我们将使用不同的方法。

Tauri 还通过windowJavascript 中的对象导出其功能。

在我们的front-end项目中,创建一个名为目录glue.js内的新文件public/

前端/公共/glue.js



const invoke = window.__TAURI__.invoke

export async function invokeHello(name) {
    return await invoke("hello", {name: name});
}


Enter fullscreen mode Exit fullscreen mode

这就是我们要添加的所有 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>;
}


Enter fullscreen mode Exit fullscreen mode

这里我们告诉 wasm_bindgen 我们的 javascript 代码在 Rust 中/public/glue.js。我们给它一个 javascript 函数名,catch参数告诉 wasm_bindgen 我们想要catch()为 javascript promise 添加一个处理程序,并将其转换为ResultRust 中的。

这段新代码暂时还不能运行,因为我们还没有添加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


Enter fullscreen mode Exit fullscreen mode

Cargo.toml这将在前端添加以下依赖项:



wasm-bindgen = "0.2.78"
wasm-bindgen-futures = "0.4.28"
web-sys = "0.3.55"
js-sys = "0.3.55"


Enter fullscreen mode Exit fullscreen mode

我们还需要在 main.rs 的顶部添加一些导入。

前端/src/main.rs



use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::window;


Enter fullscreen mode Exit fullscreen mode

现在我们已经添加了 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();
            }
        }
    });
}


Enter fullscreen mode Exit fullscreen mode

这应该可以编译并运行。

导航到上一个目录(到该tauri-yew-demo目录),然后运行:



cargo tauri dev


Enter fullscreen mode Exit fullscreen mode

您应该会看到应用程序窗口显示Hello, World!

应用程序截图

好了,代码有点多。我们来分析一下。

首先我们通过钩子向组件添加一些状态use_state_eq



let welcome = use_state_eq(|| "".to_string());
let name = use_state_eq(|| "World".to_string());


Enter fullscreen mode Exit fullscreen mode

组件状态有一个初始值,以及一个更新该值的方法。通常,更新状态值会导致组件使用新值重新渲染。

这个use_state_eq钩子为我们提供了一个状态句柄,只有当值发生变化时,它才会重新渲染组件。它使用了 Rust 的PartialEqtrait 来实现这一点。

您可以取消引用UseStateHandle以获取底层状态值。



    {
        let welcome = welcome.clone();
        use_effect_with_deps(
            move |name| {
                update_welcome_message(welcome, name.clone());
                || ()
            },
            (*name).clone(),
        );
    }


Enter fullscreen mode Exit fullscreen mode

我们需要克隆welcome句柄,以便将其移动到闭包中。

use_effect_with_deps()钩子是钩子的变体use_effect,其中仅当某些依赖项发生变化时才运行钩子。

它接受两个参数。第一个参数是依赖项值改变时我们想要运行的闭包,第二个参数是依赖项值本身。

在此示例中,依赖项被指定为(*name).clone(),这意味着我们正在取消引用句柄以获取底层String,然后克隆,String因为我们无法移动它。

然后将该值作为参数传递给闭包。

钩子use_effect*()通常用于执行一些耗时较长的操作或异步操作,组件会根据这些操作的结果进行更新。这就是为什么我们传入welcome句柄的原因,因为这允许闭包设置该句柄的值,这将导致组件重新渲染,但前提是该值已更改。对 tauri 命令的调用通常会在use_effectuse_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) => {
                ...
            }
        }
    });
}


Enter fullscreen mode Exit fullscreen mode

spawn_local()函数来自wasm_bindgen_futures,它允许我们在当前线程中运行异步函数。在这种情况下,我们需要它来调用我们的hello()函数,因为它是异步的。

接下来我们调用hello()函数,它返回Result<JsValue, JsValue>JsValue由于我们实现的原始 tauri 命令返回了Result<String, String>,所以 是一个字符串,因此我们在这里可以直接使用 unwrap() 。

最后,如果结果为Ok(..)真,则设置welcome状态变量的新值。否则,显示警报。

警告并非处理此类错误的好方法,但作为一个简单的示例,它展示了Result类型如何传递到前端。在正式的应用程序中,您可能希望显示一个 Toast 弹出窗口,并可选地将日志记录到浏览器控制台(在调试版本中)。
请参阅wasm-loggercrate,了解如何使用 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
PREV
不使用 JavaScript 的打字效果
NEXT
Docker 入门问答:结论