tauri 源码解析
全卷只对着一组版本写,也就是一个新项目写 tauri = "=2.12.5" 时 Cargo 解析出来的那一组:
| 项 | 版本 | tag | commit | 日期 |
|---|---|---|---|---|
tauri 工作区:tauri 2.12.5、tauri-runtime-wry 2.12.3、tauri-runtime 2.12.2、tauri-utils 2.10.2、tauri-build / tauri-macros / tauri-codegen 2.7.4 | 2.12.5 | tauri-v2.12.5 | 993038c49 | 2026-10-21 |
| wry(webview 封装) | 0.57.1 | wry-v0.57.1 | 2446fea | 2026-10-23 |
| tao(窗口与事件循环) | 0.37.2 | tao-v0.37.2 | 2d9b2ba1 | 2026-10-23 |
tauri 工作区里的 crate 各有各的版本号,同一个 tag 一起发布;wry 和 tao 是两个单独的仓库,按实验项目 Cargo.lock 里实际解析到的版本锁定。
tauri 给自己的描述是「a framework for building tiny, blazingly fast binaries for all major operating systems」:界面是任意能编成 HTML、JS、CSS 的前端,跑在系统自带的 webview 里;后端是一个 Rust 二进制,前端通过一套 API 调它。本卷只讲桌面端,实测平台是 macOS(WKWebView);Windows(WebView2)和 Linux(WebKitGTK)只读源码,凡是只读了源码的地方都会写明。mobile、tauri-cli 和 tauri-bundler 的源码、菜单和托盘、plugins-workspace 里的官方插件不展开,只在源码地图里一句话带过。
全卷沿三条线展开:
- 启动主线(一个窗口加载出页面):编译期
generate_context!把tauri.conf.json、压缩过的前端资源、CSP 哈希和解析好的 ACL 做成Context;Builder::build建AppManager、建 tao 的事件循环、注册核心插件;App::run进入事件循环,第一个事件Ready到来时才按配置建窗口:prepare_pending_webview补上tauri、ipc两个 URI scheme 和一组初始化脚本,wry 把它们变成WKURLSchemeHandler和WKUserScript,WKWebView 请求tauri://localhost,tauri 协议从内嵌资源里取出index.html。 - 调用主线(一次 invoke):
__TAURI_INTERNALS__.invoke登记一对回调 id,fetch发POST ipc://localhost/<命令名>,头里带回调 id 和 invoke key;wry 把请求交给ipc协议,parse_invoke_request拼出InvokeRequest,Webview::on_message比对 invoke key、查 ACL、交给generate_handler!生成的分发函数;#[command]的包装函数取参数、调用户函数,InvokeResolver把结果编码成 HTTP 响应,JS 按Tauri-Response头选回调,Promise 兑现。 - 事件主线(一次 emit):JS 的
listen本身是一次 invoke(plugin:event|listen),回调 id 记在 webview 里;Rust 的emit把 payload 序列化一次,先给有监听器的 webview 各eval一段 JS,再分发给 Rust 侧的监听器。
三条线每一步对应的源码和页面,见怎么读这一卷。
flowchart TB intro[00 导读] --> overview[01 全景] overview --> build[02 编译期] overview --> rt[03 运行时] build --> app["04 应用与管理器"] rt --> app app --> wv["05 窗口与协议"] wv --> ipc[06 IPC] ipc --> sec[07 安全] sec --> design[08 设计取舍] design --> pit[09 常见坑] pit --> site[10 在本站中] site --> hands[11 动手]
实验环境
关于行为的说法(JS 侧发出了什么请求、命令跑在哪个线程、协议响应带什么头、ACL 拒绝时说什么、rustc 报什么错、宏展开成什么)都配一个小程序和它的真实输出,平台是 M2 Max、macOS 27、rustc 1.99.0。
需要窗口的实验用同一个模板:一个最小的 tauri 应用,窗口配成 "visible": false。WKWebView 照样加载页面、执行 JS、走 IPC,桌面上不出现窗口;页面里的 JS 把想观察的东西收集起来,调一个 report 命令交回 Rust 打印,然后 app.exit(0)。实验一律带 --features tauri/custom-protocol 编译,也就是发布模式;不带这个 feature 时是开发模式,要去找 devUrl(见怎么读这一卷的「feature 与 cfg」)。不需要窗口的实验(ACL 解析、命令宏、事件分发、状态)用 tauri::test 的 MockRuntime 跑。实验依赖锁定 tauri = "=2.12.5",同一份锁文件里的 tokio 是 1.54.1、serde 是 1.0.229,和 tokio 卷、serde 卷对的是同一个版本。
各部分
- 00 导读:锁定的版本,三条主线每一步落在哪个函数、哪一页讲;
custom-protocol怎么切开发和发布两种模式,cfg(dev)从哪来,debug_assertions下多出来的东西;工作区按层分组。 - 01 全景:tauri 和 Electron、dioxus-desktop、直接用 wry 的分界;一次启动、一次 invoke 各从头到尾走一遍,带线程和时间戳。
- 02 编译期:
tauri-build的构建脚本做了什么,generate_context!生成的Context,#[command]和generate_handler!展开成什么。 - 03 运行时:
Runtime这组 trait 为什么存在,tauri-runtime-wry怎么实现它,wry 在 macOS 上怎么用 WKWebView,tao 的事件循环。 - 04 应用与管理器:
Builder与App,AppManager,托管状态,全局的 tokio 运行时,插件。 - 05 窗口与协议:窗口和 webview 拆成两个对象,初始化脚本,
tauri://协议,asset://协议。 - 06 IPC:本卷重点。JS 侧的
invoke,ipc://协议的处理,分发,参数与InvokeResolver,Channel。 - 07 安全:权限、能力、作用域三个概念,编译期解析,运行时检查,CSP、原型冻结与隔离模式。
- 08 设计取舍:系统 webview、自定义协议 IPC、编译期 ACL、
R: Runtime泛型这些选择各自的代价,和 dioxus-desktop 对照。 - 09 常见坑:用报错原文或现象做标题,每个都先跑出最小复现。
- 10 在本站中:本站不用 tauri;把本站装进 tauri 会撞上什么,两种装法各试一遍。
- 11 动手:不依赖 tauri,直接用 wry 和 tao 写一个最小的 invoke,再和 tauri 的实现逐项对照。
适合谁读
- 用 tauri 写过应用,想知道
invoke为什么在开了 CSP 之后还能用(其实是悄悄换了一条路)、命令跑在哪个线程、capabilities到底在哪一步起作用的读者:从导读开始,按阅读路线里「只想会用」那条读。 - 想读懂一个「Rust 后端 + 系统 webview」框架怎么搭起来的读者:按顺序读。06 是全卷的重点,调用主线全程在它里面。
- 需要的前置知识:Rust 的 trait 和泛型、async/await,
http::Request/http::Response(见 hyper 卷),serde(命令参数和返回值都走它,见 serde 卷),以及一点前端常识(fetch、Promise、CSP)。