架构与进程模型
本教程共 48 篇 · 第 3 篇 · 更新于 2026-08-09 · 约 6 分钟阅读
本节目标:读完你能画出 Tauri 的进程关系图,并说清核心进程、WebView、IPC 和 WRY/TAO 各自扮演什么角色。
Tauri 的架构和现代浏览器很像:不是一个大进程包揽全部,而是拆成多个相互隔离的进程。理解这一点,是看懂后面所有机制(安全、通信、窗口)的前提。
为什么是多进程,而不是一个进程
早期的图形程序常用单个进程同时算逻辑、画图、听输入。问题很明显:一个耗时计算就会让界面卡死;更糟的是,任何一个部件崩了,整个程序就跟着没了。
后来大家学乖了:把不同组件放进不同进程。好处有两条:
- 韧性:某个组件崩溃,不会拖垮整个应用;坏掉的进程可以单独重启。
- 安全:每个进程只给「刚好够用」的权限。这叫最小权限原则——你请园丁来修树篱,给花园钥匙就够,没必要把房门钥匙也交出去。
Tauri 把这套思路用到了桌面应用里。一个应用至少有一个核心进程,下面挂着一到多个 WebView 进程。
核心进程:唯一拥有系统全权的角色
每个 Tauri 应用都有一个核心进程(core process),它是一个 Rust 程序,是整个应用的入口,也是唯一默认拥有完整操作系统权限的部件。
它的主要责任包括:
- 创建和管理窗口、系统托盘菜单、桌面通知。
- 通过一套跨平台抽象,把这些系统操作变得简单统一。
- 路由所有进程间通信(IPC),让你能在一个中心位置拦截、过滤、加工消息。
- 管理全局状态,比如设置项、数据库连接,方便在多个窗口间同步,也能把敏感数据挡在前端之外。
之所以选 Rust 写核心进程,是因为 Rust 的「所有权」机制在保障内存安全的同时还能保持高性能——不需要垃圾回收器,也就少了一层不确定性。
Note核心进程崩了,整个应用还是会退出。所以重活、危险活尽量放在 WebView 或异步任务里,别让核心进程卡在同步长计算上。
WebView 进程:负责画界面
核心进程自己不画界面。它启动的是 WebView 进程,由操作系统提供的 WebView 库来跑你的 HTML / CSS / JS。换句话说,你做网页的那套技术栈——框架、打包工具、调试习惯——几乎都能直接搬过来。
和别的方案不同,Tauri 的 WebView 库不打包进最终可执行文件,而是运行时动态链接系统自带的那个。这正是它体积小的原因,但也带来一个后果:不同平台渲染表现可能有细微差别,需要像做普通网页那样做跨平台测试。
各平台的 WebView 对应关系是固定的:
- Windows → WebView2(Edge 内核)
- macOS → WKWebView(Safari 内核)
- Linux → WebKitGTK
IPC 消息传递:前后端怎么对话
进程之间是隔离的,不能直接调用对方的函数。它们靠 IPC(Inter-Process Communication,进程间通信) 来对话。Tauri 用的是一种叫「异步消息传递」的风格:进程之间交换请求和响应这类简单数据,而不是直接共享内存。
这种方式的妙处在于接收方可以自由拒绝。如果核心进程发现某条请求像恶意的,直接丢掉不执行就行,前端拿不到结果也不会出事。
Tauri 的 IPC 有两个原语:
事件(event):一次性、单向的消息,最适合通知「状态变了」「生命周期到了」。事件的特殊之处是前端和核心都能发——前端可以 tell 后端「我准备好了」,后端也能主动 push 给前端「文件读完了」。
命令(command):类似远程函数调用。前端用 invoke 调一个 Rust 函数、传参数、拿返回值,用起来很像浏览器的 fetch。底层走的是类 JSON-RPC 的协议,所以参数和返回值都必须能序列化成 JSON。
Tip简单记:要「问后端要个结果」用命令(invoke);要「通知对方发生了什么」或「后端主动推数据」用事件(emit / listen)。
WRY 与 TAO:Tauri 脚下的两块地基
Tauri 自己并不直接和系统窗口、浏览器引擎纠缠,它站在两个由 Tauri 团队维护的上游库之上:
- TAO:负责创建和管理窗口,支持 Windows、macOS、Linux,以及 iOS、Android。它其实是
winit的一个分支,Tauri 团队按自己的需要扩展了菜单栏、系统托盘等能力。 - WRY:负责渲染 WebView,是个跨平台 WebView 封装层。它决定「用哪个系统的 WebView、怎么跟它交互」。
你可以把关系想成:你的代码 → Tauri(封装与 API)→ TAO/WRY(和系统对话)→ 操作系统。当你需要超出 Tauri 封装的更深集成时,也能直接使用 TAO 和 WRY。
一个最小的命令示例
下面这段 Rust 定义了一个可被前端调用的命令,并把它登记到应用里:
// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: &str) -> String {
format!("你好,{name}!这条消息来自 Rust 后端。")
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("运行 Tauri 应用时出错");
}
前端这样调用它(注意 2.x 里 invoke 来自 @tauri-apps/api/core):
import { invoke } from "@tauri-apps/api/core";
async function sayHello() {
const msg = await invoke<string>("greet", { name: "小明" });
console.log(msg); // 你好,小明!这条消息来自 Rust 后端。
}
#[tauri::command] 是个属性宏,它标记「这个函数能从前端的 JS 调」;generate_handler! 则把允许被调用的命令登记进去。两个都要写,前端才调得到。
Warning
invoke的参数名用 camelCase(如userName),但 Rust 函数里的参数名是 snake_case(如user_name)。Tauri 会自动转换,但你在前端传参时写的键必须和 Rust 参数对应上,否则会报错。
小结
Tauri 的架构可以浓缩成一句话:一个拥有系统全权的 Rust 核心进程,管着若干个只负责画界面的 WebView 进程,两者通过异步 IPC(事件 + 命令)对话,底层依赖 TAO 管窗口、WRY 管渲染。
这套「隔离 + 最小权限 + 消息传递」的设计,正是 Tauri 体积和安全感的来源。下一章我们看看 Tauri 从 1.x 走到 2.x 发生了哪些关键变化。