Rust in Everything
基于 tauri 2.12.5

tauri 源码解析

全卷只对着一组版本写,也就是一个新项目写 tauri = "=2.12.5" 时 Cargo 解析出来的那一组:

项版本tagcommit日期
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.42.12.5tauri-v2.12.5993038c492026-10-21
wry(webview 封装)0.57.1wry-v0.57.12446fea2026-10-23
tao(窗口与事件循环)0.37.2tao-v0.37.22d9b2ba12026-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 卷对的是同一个版本。

各部分

  1. 00 导读:锁定的版本,三条主线每一步落在哪个函数、哪一页讲;custom-protocol 怎么切开发和发布两种模式,cfg(dev) 从哪来,debug_assertions 下多出来的东西;工作区按层分组。
  2. 01 全景:tauri 和 Electron、dioxus-desktop、直接用 wry 的分界;一次启动、一次 invoke 各从头到尾走一遍,带线程和时间戳。
  3. 02 编译期:tauri-build 的构建脚本做了什么,generate_context! 生成的 Context,#[command] 和 generate_handler! 展开成什么。
  4. 03 运行时:Runtime 这组 trait 为什么存在,tauri-runtime-wry 怎么实现它,wry 在 macOS 上怎么用 WKWebView,tao 的事件循环。
  5. 04 应用与管理器:Builder 与 App,AppManager,托管状态,全局的 tokio 运行时,插件。
  6. 05 窗口与协议:窗口和 webview 拆成两个对象,初始化脚本,tauri:// 协议,asset:// 协议。
  7. 06 IPC:本卷重点。JS 侧的 invoke,ipc:// 协议的处理,分发,参数与 InvokeResolver,Channel。
  8. 07 安全:权限、能力、作用域三个概念,编译期解析,运行时检查,CSP、原型冻结与隔离模式。
  9. 08 设计取舍:系统 webview、自定义协议 IPC、编译期 ACL、R: Runtime 泛型这些选择各自的代价,和 dioxus-desktop 对照。
  10. 09 常见坑:用报错原文或现象做标题,每个都先跑出最小复现。
  11. 10 在本站中:本站不用 tauri;把本站装进 tauri 会撞上什么,两种装法各试一遍。
  12. 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)。

💬 关联讨论

发起讨论
加载中...