扩展 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" 的名称注册,并提供 listen、unlisten、emit 和 emit_to 等命令。
类似地,app/plugin.rs 暴露了应用元数据命令,如 version、name、tauri_version、identifier,以及 macOS 专属的 app_show 和 app_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 不同),它负责:
- 读取插件的权限定义文件
- 生成描述可用权限和默认能力的 ACL manifest
- 自动生成权限常量的 Rust 代码
- 生成插件配置的 JSON schema
这些编译期工作确保了当 Tauri 应用依赖某个插件时,应用自身的构建过程能够发现所有可用权限,并将其纳入已解析的 ACL 中。
提示: 插件作者应定义细粒度的权限(例如
fs:allow-read-file、fs: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 如何编排构建流程、配置如何被解析,以及打包器如何生成各平台专属的发布包。