Read OSS

Tauri 的架构:探索一个包含 15 个 Crate 的 Rust Monorepo

中级

前置知识

  • 基础 Rust 知识(trait、泛型、模块)
  • 熟悉 Cargo workspace 与 crate 依赖管理
  • 对桌面端 webview 框架的基本认识

Tauri 的架构:探索一个包含 15 个 Crate 的 Rust Monorepo

Tauri 是 Rust 开源生态中颇具野心的项目之一——一个多语言应用框架,让你能够以 Web 前端搭配 Rust 后端的方式构建桌面和移动应用。然而,第一次克隆仓库时,迎接你的是由 15 个 crate、多个 TypeScript 包以及横跨编译期与运行期的构建流水线组成的复杂迷宫。本文将帮你建立清晰的心智模型,从而自如地在其中导航。

Monorepo 总览

仓库的顶层目录结构清晰明了:

目录 用途
crates/ 所有 Rust crate——核心框架、运行时、CLI、打包器、宏以及辅助工具
packages/ TypeScript/JavaScript 包——@tauri-apps/api JS API 与基于 NAPI 的 CLI 封装
examples/ 示例应用,包含一个完整的 API 展示项目
bench/ 性能基准测试套件

工作区根目录的 Cargo.toml 将一切串联在一起,声明了 15 个成员 crate 以及测试和示例项目:

graph TB
    subgraph "Workspace Root"
        CT[Cargo.toml]
    end
    subgraph "crates/"
        tauri[tauri]
        runtime[tauri-runtime]
        wry_rt[tauri-runtime-wry]
        macros[tauri-macros]
        utils[tauri-utils]
        build[tauri-build]
        codegen[tauri-codegen]
        plugin[tauri-plugin]
        cli[tauri-cli]
        bundler[tauri-bundler]
        sign[tauri-macos-sign]
        schema[tauri-schema-generator]
        schema_w[tauri-schema-worker]
        driver[tauri-driver]
    end
    subgraph "packages/"
        api["@tauri-apps/api"]
        cli_napi["@tauri-apps/cli (NAPI)"]
    end
    CT --> tauri
    CT --> cli
    CT --> api

工作区设置了 resolver = "2",并要求 Rust 最低版本为 1.77.2。发布构建配置对产物体积进行了激进的优化(opt-level = "s"、启用 LTO、单一代码生成单元)——这正体现了 Tauri 对极小二进制体积的核心承诺。

提示: Cargo.toml 底部的 [patch.crates-io] 部分将 tauritauri-plugintauri-utils 固定到本地路径。这意味着工作区内所有 crate 始终使用本地版本,而非 crates.io 上已发布的版本。在阅读依赖版本号时请牢记这一点。

核心 Crate:tauri

tauri crate 是整个项目的核心,也是应用开发者所依赖的直接入口,它几乎重新导出了开发者所需的一切。crates/tauri/src/lib.rs 中的模块声明揭示了内部的组织方式:

模块 可见性 用途
app pub(crate) Builder 模式、App、AppHandle、RunEvent
ipc pub IPC 命令、channel、authority
plugin pub Plugin trait 与 builder
webview pub Webview 与 WebviewWindow 类型
window pub 窗口管理
event 私有 事件系统
manager 私有 AppManager——中央调度器
protocol pub(crate) 自定义 URI 协议处理器
state 私有 基于 TypeId 的状态管理
menu pub(桌面端) 菜单 API
tray pub(桌面端 + feature) 系统托盘 API

该 crate 定义了三个基础 trait——ManagerListenerEmitter——它们共同构成了公开 API 面。AppAppHandleWindowWebviewWebviewWindow 均实现了这些 trait,从而拥有一致的接口来访问状态、发送事件和管理资源。

classDiagram
    class Manager {
        +app_handle() AppHandle
        +config() Config
        +get_webview_window(label) Option~WebviewWindow~
        +state~T~() State~T~
    }
    class Listener {
        +listen(event, handler) EventId
        +once(event, handler) EventId
        +unlisten(id)
    }
    class Emitter {
        +emit(event, payload) Result
        +emit_to(target, event, payload) Result
    }
    class ManagerBase {
        <<sealed>>
        +manager() AppManager
        +runtime() RuntimeOrDispatch
    }

    Manager --|> ManagerBase : depends on
    Listener --|> ManagerBase : depends on
    Emitter --|> ManagerBase : depends on
    App ..|> Manager
    AppHandle ..|> Manager
    Window ..|> Manager
    Webview ..|> Manager
    WebviewWindow ..|> Manager

第 1059 行定义的 sealed trait ManagerBase 体现了一个精妙的设计模式:它要求访问内部的 AppManager,但由于其位于 pub(crate) 模块中,外部代码无法实现它。这意味着 ManagerListenerEmitter 实际上是 sealed 的——你可以使用它们,但永远无法在自己的类型上实现它们。

运行时抽象层

Tauri 将"框架对 webview 运行时的需求"与"WRY/TAO 的具体实现"分离开来,这一设计跨越了两个 crate:

tauri-runtime 定义了抽象 trait。Runtime trait 规定了 dispatcher、handle 和事件循环 proxy 的关联类型,以及创建窗口、webview 和查询显示器的方法。配套的 RuntimeHandle trait 提供了一个满足 Send + Sync + Clone 的 handle,可在任意线程中使用。

tauri-runtime-wry 通过 WRY(webview 渲染)和 TAO(窗口管理)提供具体实现,同时重新导出这两个库并将其桥接到抽象 trait 上。

flowchart TB
    subgraph "Application Code"
        App["Your App"]
    end
    subgraph "tauri crate"
        Manager["Manager / Builder"]
    end
    subgraph "tauri-runtime"
        RT["Runtime trait"]
        RTH["RuntimeHandle trait"]
        WD["WindowDispatch trait"]
        WVD["WebviewDispatch trait"]
    end
    subgraph "tauri-runtime-wry"
        Wry["Wry struct"]
    end
    subgraph "External"
        WRY["WRY (webview)"]
        TAO["TAO (windowing)"]
    end

    App --> Manager
    Manager --> RT
    RT -.->|"impl"| Wry
    Wry --> WRY
    Wry --> TAO

tauri 主 crate 定义了一个类型别名 Wry,对 tauri_runtime_wry::Wry<EventLoopMessage> 进行封装。结合 #[default_runtime] 过程宏,最终用户几乎不需要直接面对泛型参数 R: Runtime——当启用 wry feature 时,它会被自动填入。

编译期 Crate:utils、codegen、macros、build

Tauri 有相当一部分工作发生在编译期,由以下四个 crate 共同构成这条流水线:

flowchart LR
    CONFIG["tauri.conf.json"] --> UTILS["tauri-utils<br/>Config 解析、ACL 类型"]
    UTILS --> CODEGEN["tauri-codegen<br/>资源嵌入、Context 生成"]
    CODEGEN --> MACROS["tauri-macros<br/>#[command], generate_context!, generate_handler!"]
    UTILS --> BUILD["tauri-build<br/>build.rs 辅助工具、ACL 解析"]
    BUILD --> CODEGEN
    MACROS --> APP["编译后的应用"]
    BUILD --> APP

tauri-utils 是整个体系的基础——它定义了 Config 结构体tauri.conf.json 的 Rust 表示)、所有 ACL 类型(能力、权限、作用域)以及共享工具函数。由于它不依赖运行时层,因此可以安全地在构建脚本和过程宏中使用。

tauri-codegen 负责读取配置、发现并压缩前端资源,以及将 Context 结构体生成为 token stream。其中的 get_config 函数处理 TAURI_CONFIG 环境变量的覆盖逻辑——CLI 正是通过这种方式将合并后的配置传递给编译步骤的。

tauri-macros 提供了开发者直接使用的过程宏:用于标注命令函数的 #[command]、用于构建 dispatch 表的 generate_handler!、用于嵌入编译期 context 的 generate_context!,以及用于自动填充泛型运行时参数的 #[default_runtime]

tauri-build 设计用于在用户的 build.rs 中调用,负责解析 ACL 权限、复制资源文件、生成 Windows manifest,并可选地委托 tauri-codegen 完成资源嵌入。

工具链 Crate:CLI、打包器与签名

面向开发者的工具链由三个 crate 组成:

tauri-cli 驱动 cargo tauri 命令。其 main.rs 会检测当前是作为 cargo-tauri(Cargo 子命令)还是直接调用,剥离多余的 tauri 参数,然后转交给 tauri_cli::run()。基于 Clap 构建的命令结构定义在 lib.rs 中,包含 devbuildbundleinitaddandroidiospluginiconsigner 等子命令。

tauri-bundler 接收编译好的二进制文件,生成对应平台的安装包——macOS 的 DMG 和 .app bundle、Windows 的 MSI/NSIS 安装程序,以及 Linux 的 DEB/RPM/AppImage。

tauri-macos-sign 负责处理 macOS 代码签名与公证(notarization)。

CLI 还以 NAPI 模块的形式封装在 packages/cli/ 中,以 @tauri-apps/cli 的形式通过 npm 分发。大多数使用 JavaScript 框架的用户正是通过这种方式与 Tauri 交互的。

JS API 包

packages/api/ 目录下的 @tauri-apps/api 包提供了前端代码与 Rust 后端通信所使用的 TypeScript 接口。核心模块(packages/api/src/core.ts)导出了关键的 invoke() 函数:

async function invoke<T>(
  cmd: string,
  args: InvokeArgs = {},
  options?: InvokeOptions
): Promise<T> {
  return window.__TAURI_INTERNALS__.invoke(cmd, args, options)
}

该函数将调用委托给 window.__TAURI_INTERNALS__,后者由 Tauri 注入每个 webview 的初始化脚本负责设置。Channel 类(第 77–154 行)支持以保序的方式将数据从 Rust 流式传输到 JavaScript——这一模式被各类插件广泛用于进度上报和实时更新。

sequenceDiagram
    participant Frontend as JS Frontend
    participant Internals as __TAURI_INTERNALS__
    participant Protocol as ipc:// Protocol
    participant Rust as Rust Backend

    Frontend->>Internals: invoke("greet", {name: "World"})
    Internals->>Protocol: HTTP POST with headers
    Protocol->>Rust: parse_invoke_request()
    Rust->>Rust: ACL check + command dispatch
    Rust-->>Protocol: Response
    Protocol-->>Frontend: Promise resolves

Crate 依赖关系图

下图展示了各 crate 之间的实际依赖关系。注意编译期 crate(左侧)与运行时 crate(右侧)之间清晰的职责分离:

graph TD
    tauri_utils["tauri-utils"]
    tauri_runtime["tauri-runtime"]
    tauri_runtime_wry["tauri-runtime-wry"]
    tauri_codegen["tauri-codegen"]
    tauri_macros["tauri-macros"]
    tauri_build["tauri-build"]
    tauri_plugin["tauri-plugin"]
    tauri_main["tauri"]
    tauri_cli["tauri-cli"]
    tauri_bundler["tauri-bundler"]

    tauri_utils --> tauri_runtime
    tauri_utils --> tauri_codegen
    tauri_utils --> tauri_build
    tauri_utils --> tauri_plugin
    tauri_utils --> tauri_cli
    tauri_utils --> tauri_bundler
    tauri_runtime --> tauri_runtime_wry
    tauri_runtime --> tauri_main
    tauri_runtime_wry --> tauri_main
    tauri_codegen --> tauri_macros
    tauri_codegen --> tauri_build
    tauri_macros --> tauri_main
    tauri_build -.->|"build.rs"| tauri_main
    tauri_plugin -.->|"build.rs"| tauri_main
    tauri_bundler --> tauri_cli

这里的关键在于清晰的分层结构:tauri-utils 处于最底层(无任何 Tauri 特定依赖),tauri-runtime 定义抽象接口,tauri-runtime-wry 提供具体实现,tauri 负责将一切整合在一起。编译期 crate(codegenmacrosbuild)则在并行轨道上运作,它们消费 tauri-utils 中的类型,但从不依赖运行时层。

提示: 在查找某个功能的代码位置时,可以遵循以下规则:若涉及类型或配置,先看 tauri-utils;若是构建期流程,看 tauri-buildtauri-codegen;若是运行时行为,看 tauri crate 的内部模块;若与 webview 或窗口创建相关,看 tauri-runtimetauri-runtime-wry

在下一篇文章中,我们将完整追踪一个 Tauri 应用的生命周期——从编译期通过 generate_context! 宏嵌入资源,到 Builder 模式的初始化过程,再到驱动应用持续运行的事件循环。