用 Tauri 写一个 Hello World:从环境配置到打包踩坑记录

为什么折腾 Tauri

以前想用前端技术写桌面应用,基本只有 Electron 可选。虽然生态成熟、轮子多,但缺点也很明显:自带 Chromium 和 Node.js,空项目打包出来就有一两百兆,跑起来开几个窗口内存就上去了。

Tauri 的思路不太一样,它不打包浏览器内核,而是直接调用操作系统自带的 WebView(Windows 下用 Edge WebView2,Mac 下用 WebKit),后端换成了 Rust。这样搞出来的可执行文件一般只有几兆到十几兆,内存占用也低得多。

而且 Rust 部分并不需要你上来就精通生命周期或者异步运行时,对于绝大部分功能,Rust 端只充当一个提供本地系统 API 的“后端”,页面 UI 依然是纯前端。


1. 本地环境准备

Tauri 依赖底层的 C/C++ 编译链和 Rust 环境,所以第一步得把编译工具装好。

  • Node.js: 随便装个 18+ 或 20+ 的 LTS 版本就行。
  • Rust:
    • 官网下载 rustup-init.exe(Windows)或者终端执行脚本安装。
    • 装完后终端跑 cargo --version 和 rustc --version,能正常输出版本号就行。
  • C++ 编译工具链(Windows 用户重点):
    • 需要安装 Visual Studio 2022 生成工具(Build Tools)。
    • 安装时勾选“使用 C++ 的桌面开发”,否则第一次 cargo build 时会直接报缺少链接器或 MSVC 环境的错误。
  • WebView2:
    • Win10 较新版本和 Win11 默认自带,如果是较老的系统需要去微软官网下个 Evergreen 运行时。

2. 初始化项目

Tauri 官方提供了脚手架:

1
2
pnpm create tauri-app
# 或者 npm create tauri-app@latest

交互式菜单里按自己的习惯选:

  • 项目名称:tauri-hello-world
  • 前端语言:JavaScript / TypeScript
  • 包管理器:pnpm(或 npm)
  • 模版:选 Vanilla(纯原生 HTML/JS,最适合用来理清架构,不被 Vue/React 脚手架的额外配置干扰)

进入项目目录并安装依赖:

1
2
cd tauri-hello-world
pnpm install

3. 项目结构说明

创建出来的项目目录结构非常清晰,核心就两层:

1
2
3
4
5
6
7
8
9
10
├── src/                    # 前端代码(HTML/CSS/JS)
│ ├── index.html
│ ├── main.js
│ └── style.css
├── src-tauri/ # Rust 桌面端工程
│ ├── Cargo.toml # Rust 依赖声明
│ ├── tauri.conf.json # 核心配置文件(窗口、权限、构建目标)
│ └── src/
│ └── main.rs # Rust 入口
└── package.json
  • **src/**:完全是常规的前端静态资源。
  • **src-tauri/**:桌面壳子。包括窗口怎么开、透明度、尺寸、以及给前端暴露什么本地接口。

4. 编写代码:实现前端与 Rust 的通信

一个桌面应用如果只是显示静态网页,那和直接双击 html 没区别。真正关键的是“前端调用本地后端能力”。

我们实现一个小功能:前端输入名字,点击按钮调用 Rust 函数,Rust 拼好字符串再返回给前端渲染。

4.1 在 Rust 端写一个命令函数

打开 src-tauri/src/main.rs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

// 用 #[tauri::command] 标记这个函数可以被前端调用
#[tauri::command]
fn greet(name: &str) -> String {
format!("你好,{}!这是从 Rust 后端返回的消息。", name)
}

fn main() {
tauri::Builder::default()
// 关键:在 invoke_handler 里把 greet 函数注册进去
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

这里逻辑很简单:

  1. #[tauri::command] 宏负责处理参数的反序列化和返回值的序列化。
  2. tauri::generate_handler![greet] 把它挂载到 IPC 调度器里。

4.2 编写前端页面

修改 src/index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri Hello World</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="container">
<h2>Tauri Hello World</h2>
<div class="row">
<input id="name-input" placeholder="输入名称..." />
<button id="greet-btn">调用 Rust</button>
</div>
<p id="result"></p>
</div>
<script type="module" src="main.js"></script>
</body>
</html>

加一点简单的样式 src/style.css:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #1e1e2e;
color: #cdd6f4;
}

.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}

.row {
display: flex;
gap: 8px;
}

input {
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #45475a;
background: #313244;
color: #fff;
outline: none;
}

button {
padding: 8px 16px;
border-radius: 6px;
border: none;
background: #89b4fa;
color: #11111b;
font-weight: 600;
cursor: pointer;
}

button:hover {
background: #b4befe;
}

#result {
margin-top: 10px;
color: #a6e3a1;
}

4.3 前端通过 IPC 调用 Rust

打开 src/main.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { invoke } from '@tauri-apps/api/tauri';

const input = document.getElementById('name-input');
const btn = document.getElementById('greet-btn');
const result = document.getElementById('result');

async function sendGreet() {
const name = input.value.trim() || 'World';
try {
// 调用在 main.rs 中注册的 greet 函数
// key 必须和 Rust 里的形参名一致(驼峰转下划线 Tauri 会自动处理)
const msg = await invoke('greet', { name });
result.textContent = msg;
} catch (err) {
console.error('调用出错:', err);
result.textContent = '调用失败,看下控制台报错';
}
}

btn.addEventListener('click', sendGreet);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') sendGreet();
});

5. 调试与运行

在终端执行:

1
pnpm tauri dev

第一次启动会慢一些,因为 cargo 需要拉取 crates 依赖并进行首次编译。编译完成后窗口会自动弹出来。

调试快捷键:

  • 窗口内直接按 F12 或右键“检查”,就能调出 Edge WebView2 开发者工具,断点、控制台、网络请求跟平时调试 Web 页面一模一样。

6. 打包成可执行文件

开发完毕后直接打包:

1
pnpm tauri build

打包完成后,安装包或者单个 exe 文件会输出在:
src-tauri/target/release/bundle/nsis/

看一眼文件大小,生成的 setup 安装包大概在 3MB~5MB 左右,相比 Electron 动辄 100MB+ 的安装包,轻量了不止一个数量级。


7. 几个常见踩坑点

  1. Windows 编译报错缺少 link.exe 或 C++ 运行库

    • 原因:本机只有 Rust,没有装 Visual Studio C++ 生成工具。
    • 解决:下载 Visual Studio Installer,勾上“使用 C++ 的桌面开发”装完重启终端。
  2. tauri.conf.json 中的 devPath 别写成 ../

    • 很多新手把 devPath 直接指到项目根目录 ../,这会导致 cargo build 时 Tauri 把整个项目(包括 src-tauri/target 下的锁文件)都当成前端资源扫描,从而在 Windows 下报 os error 33(文件被占用)死锁。
    • 正确做法:只指向前端代码目录,比如 ../src 或 ../dist。
  3. 前端调用 invoke 时参数名对不上

    • Rust 里的入参叫 name: &str,前端传参必须是 { name: 'xxx' }。如果传了未定义的 key,Rust 序列化会解析成空或者直接报错。
  4. 不想引入 @tauri-apps/api 依赖也可以直接调用

    • 只要在 tauri.conf.json 里开启 "withGlobalTauri": true,Tauri 就会在所有网页全局注入 window.__TAURI__。
    • 前端无需 npm 安装任何包,直接写 window.__TAURI__.tauri.invoke(...) 就能跑。

总结

Tauri 的上手成本没有网上传的那么高。你不需要真的去啃透 Rust 的复杂特性,日常开发大部分工作还是在写前端代码。当需要操作系统底层特权或者本地高性能计算时,再在 Rust 里写一两个函数暴露给前端即可。对于轻量级桌面工具、小玩具或者桌面挂件类项目,Tauri 是个很舒适的选择。