初识 Tauri 篇三之前后端通信

3,975 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

前言

Tauri 采用两种不同的方式实现了前后端的双向通信,当然,这里的双向不是特别准确,详细的介绍可以看看官网的文档 Inter-Process Communication, 本文我们将只讲如何使用。

如果不喜欢这两种你也可以用一些特殊的方法实现通信,比如 rust 起个 webswocket 或者起个 http server 也都可以实现一定的通信能力,具体就看需要了。

前端 -> 后端:Command

看这个标题大概也能感受到一点不同,与其说这是一种通信,倒不如说更像是执行命令。

如果你使用的 @tauri-apps/cli 是最新版本的,即 1.0.5 及以上,那你肯定对它不会陌生,因为首屏的 greet 就是通过这种方式实现的。

至于怎么使用我们也可以直接看源码就能了解个大概:

后端首先定义好一个函数,然后加上 #[tauri::command] 宏定义其为 command 类型,然后前端通过调用 invoke + 函数名 + 参数 即可完成通信,结果将通过 promise 即异步的形式进行返回 。

详细官方文档可见 Calling Rust from the frontend

// component/Greet.vue
<script setup lang="ts">
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/tauri";

const greetMsg = ref("");
const name = ref("");

async function greet() {
  greetMsg.value = await invoke("greet", { name: name.value });
}
</script>

<template>
  <div class="card">
    <input id="greet-input" v-model="name" placeholder="Enter a name..." />
    <button type="button" @click="greet()">Greet</button>
  </div>

  <p>{{ greetMsg }}</p>
</template>

// main.rs
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

xmsM2n.png

后端 <-> 前端:Event

了解或熟悉 QT 的肯定都知道它的 signal->slot 通信方式,十分好用也十分强大,而 Tauri 在这里的实现功能上是类似的,由一方发送 event 事件,另一方 listen 监听并捕获事件,然后进行处理。

依然是 greet 的功能实现,但我们这次不返回值,而是通过 event 将结果发送至前端,具体代码如下。

// component/Greet.vue
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { invoke } from "@tauri-apps/api/tauri";
import { emit, listen } from '@tauri-apps/api/event'

const greetMsg = ref("");
const name = ref("");

async function greet() {
  await invoke("greet", { name: name.value });
}

async function listen_to_greet() {
  const unlisten = await listen('greet', (event: any) => {
    // event.payload 才是实际的结构体
    greetMsg.value = event.payload;
  });
}

onMounted(() => {
  listen_to_greet();
})
</script>

<template>
  <div class="card">
    <input id="greet-input" v-model="name" placeholder="Enter a name..." />
    <button type="button" @click="greet()">Greet</button>
  </div>

  <p>{{ greetMsg }}</p>
</template>
// main.rs
use tauri::Manager;

#[tauri::command]
fn greet(name: &str, app_handle: tauri::AppHandle) {
    let msg = format!("Hello, {}! You've been greeted from Rust!", name);
    app_handle.emit_all("greet", msg);
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

xmsM2n.png

细心的小伙伴肯定注意到了,前端也是有能力 emit 的,那是不是说明后端也能被 greet?让我们来试试。

// component/Greet.vue
// component/Greet.vue
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { invoke } from "@tauri-apps/api/tauri";
import { emit, listen } from '@tauri-apps/api/event'

const greetMsg = ref("");
const name = ref("");

function greet() {
  emit("greet", { name: name.value });
}

async function listen_to_greet() {
  const unlisten = await listen('hello', (event: any) => {
    // event.payload 才是实际的结构体
    greetMsg.value = event.payload;
  });
}

onMounted(() => {
  listen_to_greet();
})
</script>

<template>
  <div class="card">
    <input id="greet-input" v-model="name" placeholder="Enter a name..." />
    <button type="button" @click="greet()">Greet</button>
  </div>

  <p>{{ greetMsg }}</p>
</template>
// main.rs
use tauri::Manager;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Greet {
    name: String
}

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let callback_app_handle = app.app_handle().clone();
            let _event_id = app.listen_global("greet", move |event| {
                let greet_msg: Greet =  serde_json::from_str(&event.payload().unwrap()).unwrap();
                let msg = format!("Hello, {}! You've been greeted from Rust!", greet_msg.name);
                println!("{}", msg);
                let _ = &callback_app_handle.emit_all("hello", msg);
            });

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

xms1K0.png

果然也是可以的,虽然不如 command 方便,但能跑就是好的。

细心的小伙伴肯定又发现了,此时我们的后端程序其实发生了挺多变化的,一个是结构体上多了一行宏定义,另一个是我们实际上获得了由后端主动发送 event 的 “自由”,让我们一个一个来说。

首先是宏定义的含义,其实很简单就是允许结构体序列化和解序列化,或者再换个说法,赋予了结构体成为 JSON 和从 JSON 解出结构体的能力,其第三方库 serde 和 serde_json 已经默认帮我们装好了,相当方便,最后一个 debug 则是允许直接打印整个结构体,就是说我们可以通过这样子的代码直接打印整个结构体。

println!("{:?}", some_struct);

其次是后端主动发送 event 的 “自由” ,这其实是相当重要的一件事,但是在本章我们不会继续探讨,而是将其放在下一章状态管理相关内容中探讨,到时我们才能见到它的完成体。

博客版:

初识 Tauri 篇三之前后端通信

Medium 版:

New Acquaintance With Tauri Chapter 3: Communication Between Webpage and Rust