Read OSS

扩展 Tauri:插件架构与扩展模型

高级

前置知识

  • 第 1-4 篇:架构、生命周期、IPC 与安全机制
  • Rust trait 对象与动态分发
  • 了解 build.rs 脚本与编译期代码生成

扩展 Tauri:插件架构与扩展模型

Tauri 的插件系统并非只是为第三方扩展准备的——框架自身的核心功能也是通过插件实现的。事件系统、窗口管理、webview 管理、托盘图标,全都是插件。这一设计选择揭示了架构层面的一个根本原则:如果框架自身的功能都能以插件的形式表达,那么插件 API 就必须足够强大,能够胜任任何场景。

Plugin Trait:生命周期钩子

Plugin<R> trait 定义了插件可以实现的完整接口:

钩子 调用时机 用途
name() 始终 返回插件的字符串标识符
initialize() App::build() 期间 接收 app handle 与插件配置
initialization_script() webview 创建时 在页面加载前注入的 JS
window_created() 窗口创建后 响应新窗口事件
webview_created() webview 创建后 响应新 webview 事件
on_navigation() 导航发生前 返回 false 可取消导航
on_page_load() 页面加载完成 响应页面加载事件
on_event() 每次事件循环 tick 接收 RunEvent 分发
extend_api() IPC 分发时 处理插件命令
sequenceDiagram
    participant App as App::build()
    participant Plugin as Plugin
    participant WV as New Webview

    App->>Plugin: initialize(app_handle, config)
    Note over Plugin: Setup state, start services
    App->>Plugin: initialization_script()
    Note over Plugin: Return JS to inject

    WV->>Plugin: window_created(window)
    WV->>Plugin: webview_created(webview)
    WV->>Plugin: on_navigation(webview, url)
    WV->>Plugin: on_page_load(webview, payload)

    loop Event Loop
        App->>Plugin: on_event(app_handle, event)
    end

    Note over WV: IPC call arrives
    WV->>Plugin: extend_api(invoke)

所有钩子都有默认的空实现,因此插件只需实现自己关心的部分。该 trait 要求实现 Send——插件存储在 Mutex 后面,可以从任意线程访问。

Plugin Builder:流式构建 API

虽然你可以直接实现 Plugin<R>,但 Builder 提供了更符合人体工程学的方式:

pub struct Builder<R: Runtime, C: DeserializeOwned = ()> {
    name: &'static str,
    invoke_handler: Box<InvokeHandler<R>>,
    setup: Option<Box<SetupHook<R, C>>>,
    js_init_script: Option<InitializationScript>,
    on_navigation: Box<OnNavigation<R>>,
    on_page_load: Box<OnPageLoad<R>>,
    on_window_ready: Box<OnWindowReady<R>>,
    on_webview_ready: Box<OnWebviewReady<R>>,
    on_event: Box<OnEvent<R>>,
    on_drop: Option<Box<OnDrop<R>>>,
    uri_scheme_protocols: HashMap<String, Arc<UriSchemeProtocol<R>>>,
}

泛型参数 C: DeserializeOwned 是插件的配置类型。当 initialize() 被调用时,框架会从 tauri.conf.json 中提取与插件名对应的配置节,将其反序列化为 C,并通过 PluginApi 传递给插件使用。

classDiagram
    class Builder~R, C~ {
        +new(name) Self
        +invoke_handler(handler) Self
        +setup(hook) Self
        +js_init_script(script) Self
        +on_navigation(handler) Self
        +on_page_load(handler) Self
        +on_event(handler) Self
        +register_uri_scheme_protocol(name, handler) Self
        +build() TauriPlugin~R, C~
    }
    class TauriPlugin~R, C~ {
        -name: &'static str
        -app: Option~AppHandle~
        -invoke_handler
        -setup
        // ... all fields from Builder
    }
    class Plugin~R~ {
        <<trait>>
        +name() &str
        +initialize() Result
        +extend_api(Invoke) bool
    }

    Builder --> TauriPlugin : build()
    TauriPlugin ..|> Plugin : implements

惯例是导出一个 init() 函数来构建并返回插件:

pub fn init<R: Runtime>() -> TauriPlugin<R> {
    Builder::new("my-plugin")
        .invoke_handler(tauri::generate_handler![my_command])
        .setup(|app, api| {
            // initialization logic
            Ok(())
        })
        .build()
}

PluginStore 与命令命名空间

PluginStore(位于 crates/tauri/src/app/plugin.rs)负责管理所有已注册的插件。它将插件以 trait 对象(Box<dyn Plugin<R>>)的形式存储在 Vec 中,并提供初始化、事件分发和命令路由等方法。

名称冲突通过保留名称检查来避免:

const RESERVED_PLUGIN_NAMES: &[&str] = &["core", "tauri"];

Builder::try_build() 方法会对这个列表进行检查,如果插件尝试使用 "core""tauri" 作为名称,则返回 BuilderError::ReservedName。这样可以防止与 Tauri 内部插件命名空间产生冲突。

正如第 3 篇所介绍的,插件命令的命名空间格式为 plugin:{name}|{command}。当带有此前缀的 IPC 请求到达时,框架会遍历已注册的插件,找到名称匹配的那个,并将 invoke 传递给其 extend_api() 方法。extend_api 返回 bool——true 表示已处理该命令,false 表示透传给下一个处理者。

核心内置插件

Tauri 使用自己的插件系统来实现核心功能。event/plugin.rs 展示了事件系统以 "core:event" 的名称注册,并提供 listenunlistenemitemit_to 等命令。

类似地,app/plugin.rs 暴露了应用元数据命令,如 versionnametauri_versionidentifier,以及 macOS 专属的 app_showapp_hide

App::register_core_plugins() 中注册的完整核心插件列表如下:

插件 用途 命令
core:event 事件系统 listen, unlisten, emit, emit_to
core:app 应用元数据 version, name, tauri_version, identifier
core:window 窗口管理 create, close, set_title 等
core:webview Webview 管理 create, navigate, eval 等
core:resources 资源表 close(释放资源)
core:menu 菜单操作 new, set_text 等
core:tray 托盘图标 new, set_icon, set_tooltip 等
flowchart TB
    subgraph "Core Plugins"
        EVENT["core:event"]
        APP["core:app"]
        WINDOW["core:window"]
        WEBVIEW["core:webview"]
        RES["core:resources"]
        MENU["core:menu"]
        TRAY["core:tray"]
    end
    subgraph "User Plugins"
        FS["plugin:fs"]
        HTTP["plugin:http"]
        CUSTOM["plugin:my-plugin"]
    end

    FRAMEWORK["Tauri Framework"] --> EVENT
    FRAMEWORK --> APP
    FRAMEWORK --> WINDOW
    FRAMEWORK --> WEBVIEW
    JS["Frontend JS"] --> |"invoke('plugin:core:event|listen')"| EVENT
    JS --> |"invoke('plugin:fs|read_file')"| FS

这种"自我验证"的方式传递出一个强烈的架构信号——它证明插件 API 具备足够的表达能力来支撑真实的框架功能,同时也确保该 API 得到持续维护。

tauri-plugin 构建辅助工具

外部插件在 build.rs 中使用 tauri-plugin crate 来与 ACL 系统集成。这个 crate 提供了一个 Builder(与运行时插件 builder 不同),它负责:

  1. 读取插件的权限定义文件
  2. 生成描述可用权限和默认能力的 ACL manifest
  3. 自动生成权限常量的 Rust 代码
  4. 生成插件配置的 JSON schema

这些编译期工作确保了当 Tauri 应用依赖某个插件时,应用自身的构建过程能够发现所有可用权限,并将其纳入已解析的 ACL 中。

提示: 插件作者应定义细粒度的权限(例如 fs:allow-read-filefs:allow-write-file),而非粗粒度的权限。这样应用开发者就能精确控制要授予哪些能力。可以使用权限集(如 fs:default)来组合常用的权限组合。

移动端插件桥接

插件可以通过 crates/tauri/src/plugin/mobile.rs 中的移动端桥接机制扩展到 Android 和 iOS。PluginHandle 类型提供了 run_mobile_plugin_method() 函数,负责序列化请求、调用原生方法(Android 使用 JNI,iOS 使用 Swift),并反序列化响应。

在 Android 上,android_binding!会生成 JNI 胶水代码。它创建了两个 JNI 函数:

  • handlePluginResponse——接收来自 Kotlin 插件方法的响应
  • sendChannelData——接收来自 Kotlin 的流式 channel 数据

全局的 PENDING_PLUGIN_CALLS 映射负责追踪正在进行中的移动端插件调用,以单调递增的 ID 作为键。当原生侧执行完毕后,会通过 JNI 回调将结果传回,挂起调用的 oneshot sender 随即解析对应的 Rust future。

示例插件详解

仓库在 examples/api/src-tauri/tauri-plugin-sample/ 目录下提供了一个示例插件,展示了完整的项目结构,包括:

  • 使用 tauri-plugin 构建辅助工具的 build.rs
  • 权限定义文件
  • 包含 init()Builder::new("sample").build() 的 Rust 运行时实现
  • 通过 .invoke_handler(tauri::generate_handler![...]) 注册的命令

这个示例是理解所有组成部分如何协作的权威模板——编译期 ACL 生成、运行时插件注册、命令处理以及安全集成,一应俱全。

flowchart TB
    BUILD["build.rs<br/>tauri-plugin::Builder"] --> MANIFEST["ACL manifest<br/>(permissions.json)"]
    INIT["init() function"] --> BUILDER["plugin::Builder::new('sample')"]
    BUILDER --> HANDLER[".invoke_handler(...)"]
    HANDLER --> SETUP[".setup(|app, api| {...})"]
    SETUP --> PLUGIN[".build() → TauriPlugin"]
    PLUGIN --> APP["app.plugin(sample::init())"]
    APP --> STORE["PluginStore"]

下一篇文章将从框架层转向工具链——CLI 如何编排构建流程、配置如何被解析,以及打包器如何生成各平台专属的发布包。