首页 / Tauri 2 入门教程 / Tauri 如何与前端协作

Tauri 2 入门教程

Tauri 如何与前端协作

本教程共 48 篇 · 第 14 篇 · 更新于 2026-08-09 · 约 8 分钟阅读

TauriWebViewIPC命令事件

本节目标:学完能说清 Tauri 应用里前端页面和 Rust 后端分别是怎么跑的,两者之间靠哪两条通道通信,以及前端那句 invoke 背后是什么。

很多新手第一次用 Tauri,会有个困惑:我的界面是网页写的,可它又能调用系统文件对话框、能读写本地磁盘——这网页明明跑在浏览器里是做不到这些的。答案就藏在「前端跑在哪」和「前后端怎么通信」这两件事里。本章把协作机制一次讲透,不写完整项目,只讲清楚原理和最小调用方式。

前端其实跑在 WebView 里

Tauri 的前端并不是跑在 Chrome、Edge 这种独立浏览器里,而是跑在操作系统自带的 WebView 控件中。Windows 上叫 WebView2(基于 Chromium),macOS 上叫 WKWebView(基于 Safari 内核),Linux 上叫 WebKitGTK。你可以把它理解成一个「没有地址栏、没有工具栏的迷你浏览器窗口」,它只负责把你的 HTML/CSS/JS 渲染出来。

因为 WebView 就是个网页渲染引擎,所以你熟悉的任何前端技术——原生 JS、Vue、React、Svelte——都能直接用。但网页本身是被「关在沙箱里」的,它不能直接碰操作系统。那它怎么打开文件、怎么读写数据?这就需要一个「中间人」把网页的请求转交给真正有权限的 Rust 后端。这个中间人,就是 Tauri 的进程模型和通信通道。

Note

正因为界面是网页,而能力来自 Rust,所以 Tauri 应用天然分成「前端(WebView 里的 JS)」和「后端(Rust 主进程)」两个部分。它们是两个独立的运行环境,不能直接共享变量,只能「说话」。

唯一通道:进程间通信(IPC)

前端(JavaScript)和后端(Rust)不在同一个进程,它们之间的所有交流,都通过一套叫 IPC(Inter-Process Communication,进程间通信)的机制完成。在 Tauri 2.x 中,IPC 给你两条通道,分工明确:

  • 命令(command):前端「主动呼叫」后端的一个具体函数,可以传参数、拿返回值,像调一个远程函数。它类型是安全的(参数和返回值都有明确类型)。
  • 事件(event):后端「主动广播」一条消息给前端(或反过来),适合通知、进度更新这类场景。它更像发订阅,不保证类型安全、不能拿返回值。

记住一句话:命令是「前端问、后端答」,事件是「谁都可以喊一嗓子、谁在听谁收」。下面分别看。

命令:类型安全的远程调用

命令是前后端协作的主力。后端那边,你只要写一个普通的 Rust 函数,再给它贴一个 #[tauri::command] 标签,它就变成了一个可以被前端调用的命令。

// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: String) -> String {
    format!("你好,{}!这条消息来自 Rust。", name)
}

光写函数还不够,你得告诉 Tauri「有这么个命令可以调用」,在 run() 函数里用 generate_handler! 注册它:

// src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

前端这边,用 invoke 函数去「点名」调用它。命令名要和 Rust 那边完全一致(这里是 greet),参数用对象传,返回的是一个 Promise:

// 前端任意 .ts 文件
import { invoke } from '@tauri-apps/api/core';

async function sayHi() {
    const msg = await invoke('greet', { name: '小明' });
    console.log(msg); // 输出:你好,小明!这条消息来自 Rust。
}

这里有个细节:Rust 函数的参数名是 name(snake_case),但从前端传参要用 name(Tauri 默认把参数映射成 camelCase 也认,简单类型同名即可)。如果 Rust 端写了 #[tauri::command(rename_all = "snake_case")],那前端就必须用 snake_case 的键名。命令还支持 async、支持返回 Result 来表示成功或失败、支持把文件等二进制用 tauri::ipc::Response 高效返回——这些是进阶用法,知道「命令能传参、能返回值、能报错」就够了。

Warning

在 Tauri 2.x 的安全模型下,命令默认需要被「能力(capability)」授权才能从前端调用。脚手架生成的项目已经在 src-tauri/capabilities/ 里配好了默认权限;如果你新增了一个自定义命令却忘了授权,前端调用时会收到权限错误。把命令加进能力文件即可,例如:

{
  "identifier": "default",
  "windows": ["main"],
  "permissions": ["core:default", "greet"]
}

事件:谁都可以广播的消息

命令适合「一问一答」,但有些场景是后端主动、持续地往前端的。比如后端在后台压缩文件,想实时把进度「10%、20%……」推给界面。这时命令就不合适了(它是前端发起的),该用事件。

后端用 app.emit 广播,前端用 listen 接收:

// 后端:广播一个进度事件
app.emit("progress", "已完成 50%").unwrap();
// 前端:监听这个事件
import { listen } from '@tauri-apps/api/event';

await listen('progress', (event) => {
    console.log(event.payload); // 输出:已完成 50%
});

事件的特点要记牢:它总是异步的、不带返回值、负载(payload)只能是 JSON 能表达的数据、且默认是「群发」给所有监听器。如果你只想发给某个特定窗口,可以用 emit_to 指定窗口标签。事件的代价是少了类型保护——传什么、收什么,得你和后端约定好,编译器帮不了你。所以「要返回值、要类型安全」用命令,「纯通知、流式推送」用事件。

JS API 包:前端的工具箱

你刚才看到的 invokelisten,都不是浏览器自带的东西,而是来自一个 npm 包:@tauri-apps/api。它是 Tauri 为前端准备的官方 JavaScript API 工具箱,把底层 IPC 的复杂细节封装成了好调用的函数。

安装它很简单,版本以官方最新为准:

npm install @tauri-apps/api@latest

这个包按能力分了模块,最常用的几个:

  • @tauri-apps/api/core:提供 invoke,用来调用命令(最核心)。
  • @tauri-apps/api/event:提供 listenemit,用来收发事件。
  • @tauri-apps/api/window@tauri-apps/api/webviewWindow:操作窗口。
  • @tauri-apps/api/path@tauri-apps/api/fs 等:访问文件路径、文件系统等原生能力(部分需要对应插件和权限)。
Tip

如果你不想引入 npm 包,也可以在 tauri.conf.json 里开启 app.withGlobalTauri,这样 Tauri 会把 API 挂到全局的 window.__TAURI__ 上,用 window.__TAURI__.core.invoke(...) 调用。但官方推荐用 npm 包的方式,类型提示更友好、打包更干净。

小结:一张协作图

把前面几节串起来,Tauri 的协作关系就是:你在 WebView 里写的网页,想干任何「超越网页」的活,都通过 @tauri-apps/api 包,要么用 invoke 走「命令」通道去调 Rust 函数,要么用 emit/listen 走「事件」通道收发消息;Rust 后端处理完系统层面的事,再把结果原路返回。理解了这条链路,后面无论接入原生前端还是 Vue、React,本质都只是「前端用什么语法去写那句 invoke」的区别。

接下来两章,我们就分别看两种最典型的前端写法:先是最轻量的原生 HTML/CSS/JS,再是更工程化的 Vue 3。