Tauri 的插件体系是功能复用的核心。一个插件通常包含 Rust 端的命令实现和前端侧的 JavaScript/TypeScript 封装。它的结构清晰,开发流程固定,非常适合在多个应用中共享同一套原生能力。
1. 插件结构
一个标准的 Tauri 插件(比如 tauri-plugin-fs)在源码仓库中通常有以下布局:
plugin-name/
├── src/ # Rust 源码
│ ├── lib.rs # 插件入口,注册命令、配置权限
│ ├── commands.rs # 所有命令的具体实现
│ └── error.rs # 自定义错误类型(可选)
├── guest-js/ # 前端 NPM 包(提供给 WebView 调用的 API)
│ ├── src/
│ │ └── index.ts # 封装的对 Rust 命令的调用
│ ├── package.json
│ └── tsconfig.json
├── permissions/ # 插件权限定义(Tauri v2 新增)
│ ├── default.toml # 默认权限列表
│ └── schema.json # 权限配置的 JSON Schema
├── build.rs # 编译构建脚本
└── Cargo.toml # Rust 包配置,声明为 tauri 插件类型
这种结构将 后端逻辑 和 前端调用 完全分离,但共用同一个发布周期。最终用户只需要在 Cargo.toml 和前端 package.json 中分别引入即可。
2. Rust 核心实现
Rust 侧的核心任务是:定义可调用的命令,并通过 tauri::Builder 将它们注册到 IPC 系统中。
一个典型的插件 lib.rs 长这样:
use tauri::plugin::{Builder, TauriPlugin};
use tauri::{Runtime, command};
// 定义命令:这里读取一个文本文件
#[command]
fn read_file(path: String) -> Result<String, String> {
std::fs::read_to_string(path).map_err(|e| e.to_string())
}
// 定义插件的构建函数
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("my-plugin")
.invoke_handler(tauri::generate_handler![read_file])
.build()
}
如果你想带状态(例如数据库连接),可以利用 Builder 的 setup 方法:
Builder::new("db")
.setup(|app, _api| {
let pool = init_db_pool();
app.manage(pool); // 将状态注入 Tauri 全局管理
Ok(())
})
.invoke_handler(tauri::generate_handler![query_db])
所有命令都必须通过 invoke_handler 显式注册,未经注册的 Rust 函数前端无法调用,从架构层面隔绝了未授权访问。
对于 Tauri v2,你还需要在插件中声明 权限。例如在 permissions/default.toml 中定义哪些命令是公开的:
[default]
description = "允许读取用户桌面上的文本文件"
permissions = ["allow-read-file"]
然后在应用级配置中开启该插件的权限。这为命令调用增加了细粒度的访问控制。
3. 前端 API 封装
前端侧通过 @tauri-apps/api 提供的 invoke 函数与后端通信,插件的 JS/TS 封装就是在原始调用之上加一层语义化包装。
import { invoke } from '@tauri-apps/api/core';
/**
* 读取指定路径的文本文件
* @param path 文件路径
* @returns 文件内容
*/
export async function readFile(path: string): Promise<string> {
return invoke('plugin:my-plugin|read_file', { path });
}
// 或者对于 v2 更标准的写法
export function readFile(path: string): Promise<string> {
return invoke('plugin:my-plugin|read_file', { path });
}
在 Tauri v2 中,插件的命令会带上 plugin: 前缀,如 plugin:my-plugin|read_file,这是为了区分应用自身命令和插件命令。前端封装时会把这个细节隐藏,用户直接 import { readFile } from 'tauri-plugin-my-plugin' 即可。
如果要暴露事件或者可取消的操作,可以结合 Channel 或其他通信模式。一般而言,插件作者会提供完整的 TypeScript 类型定义,保证调用时类型安全。
真实开发流程
假设你为 Tauri 应用写一个“获取系统空闲内存”的插件,步骤如下:
- 创建 Rust 插件项目(可以使用
cargo模板或直接改造一个 crate); - 在
commands.rs中用 Rust 系统调用(如sysinfo库)获取空闲内存,并定义get_free_memory命令; - 在
lib.rs中注册该命令,同时定义权限; - 在
guest-js/src/index.ts中用invoke封装一个getFreeMemory()异步函数并导出; - 发布两个包:Rust crate 和前端 NPM 包;
- 应用开发者在其 Cargo.toml 和前端项目中分别添加依赖,配置权限,直接调用。
这种三层的清晰分工,使得社区可以放心地共享插件,任何一位前端开发者都能通过 TypeScript 调用 Rust 实现的高性能原生功能,而无需关注底层的 IPC 细节和安全性。