插件开发
插件能够挂接到 Tauri 生命周期,公开依赖于 Web 视图 API 的 Rust 代码,使用 Rust、Kotlin 或 Swift 代码处理命令等等。
🌐 Plugins are able to hook into the Tauri lifecycle, expose Rust code that relies on the web view APIs, handle commands with Rust, Kotlin or Swift code, and much more.
Tauri 提供了一个带有网页视图功能的窗口系统、一种在 Rust 进程和网页视图之间发送消息的方式,以及一个事件系统和若干用以增强开发体验的工具。按照设计,Tauri 核心不包含并非每个人都需要的功能。取而代之的是,它提供了一种将外部功能添加到 Tauri 应用中的机制,称为插件。
🌐 Tauri offers a windowing system with web view functionality, a way to send messages between the Rust process and the web view, and an event system along with several tools to enhance the development experience. By design, the Tauri core does not contain features not needed by everyone. Instead it offers a mechanism to add external functionalities into a Tauri application called plugins.
一个 Tauri 插件由一个 Cargo 包和一个可选的 NPM 包组成,该 NPM 包为其命令和事件提供 API 绑定。此外,一个插件项目可以包含一个 Android 库项目和一个用于 iOS 的 Swift 包。你可以在移动插件开发指南中了解有关为 Android 和 iOS 开发插件的更多信息。
🌐 A Tauri plugin is composed of a Cargo crate and an optional NPM package that provides API bindings for its commands and events. Additionally, a plugin project can include an Android library project and a Swift package for iOS. You can learn more about developing plugins for Android and iOS in the Mobile Plugin Development guide.
🌐 Naming Convention
Tauri 插件有一个前缀,后跟插件名称。插件名称在插件配置的 tauri.conf.json > plugins 下指定。
🌐 Tauri plugins have a prefix followed by the plugin name. The plugin name is specified on the plugin configuration under tauri.conf.json > plugins.
默认情况下,Tauri 会在你的插件 crate 前加上 tauri-plugin- 前缀。这有助于你的插件被 Tauri 社区发现,并可与 Tauri CLI 一起使用。在初始化新的插件项目时,你必须提供它的名称。生成的 crate 名称将是 tauri-plugin-{plugin-name},JavaScript NPM 包名称将是 tauri-plugin-{plugin-name}-api(尽管我们建议如果可能的话使用 NPM 范围)。Tauri 对 NPM 包的命名规范是 @scope-name/plugin-{plugin-name}。
🌐 By default Tauri prefixes your plugin crate with tauri-plugin-. This helps your plugin to be discovered by the Tauri community and to be used with the Tauri CLI. When initializing a new plugin project, you must provide its name. The generated crate name will be tauri-plugin-{plugin-name} and the JavaScript NPM package name will be tauri-plugin-{plugin-name}-api (although we recommend using an NPM scope if possible). The Tauri naming convention for NPM packages is @scope-name/plugin-{plugin-name}.
🌐 Initialize Plugin Project
要引导一个新的插件项目,请运行 plugin new。如果你不需要 NPM 包,请使用 --no-api CLI 标志。如果你想初始化带有 Android 和/或 iOS 支持的插件,请使用 --android 和/或 --ios 标志。
🌐 To bootstrap a new plugin project, run plugin new. If you do not need the NPM package, use the --no-api CLI flag. If you want to initialize the plugin with Android and/or iOS support, use the --android and/or --ios flags.
安装后,你可以运行以下命令来创建插件项目:
🌐 After installing, you can run the following to create a plugin project:
npx @tauri-apps/cli plugin new [name]这将在目录 tauri-plugin-[name] 初始化插件,并且根据所使用的 CLI 标志,生成的项目将如下所示:
🌐 This will initialize the plugin at the directory tauri-plugin-[name] and, depending on the used CLI flags, the resulting project will look like this:
. tauri-plugin-[name]/├── src/ - Rust code│ ├── commands.rs - Defines the commands the webview can use| ├── desktop.rs - Desktop implementation| ├── error.rs - Default error type to use in returned results│ ├── lib.rs - Re-exports appropriate implementation, setup state...│ ├── mobile.rs - Mobile implementation│ └── models.rs - Shared structs├── permissions/ - This will host (generated) permission files for commands├── android - Android library├── ios - Swift package├── guest-js - Source code of the JavaScript API bindings├── dist-js - Transpiled assets from guest-js├── Cargo.toml - Cargo crate metadata└── package.json - NPM package metadata如果你有一个现有的插件,并希望为其添加 Android 或 iOS 功能,你可以使用 plugin android add 和 plugin ios add 来引导移动库项目,并指导你完成所需的更改。
🌐 If you have an existing plugin and would like to add Android or iOS capabilities to it, you can use plugin android add and plugin ios add to bootstrap the mobile library projects and guide you through the changes needed.
🌐 Mobile Plugin Development
插件可以运行用 Kotlin(或 Java)和 Swift 编写的原生移动代码。默认的插件模板包括一个使用 Kotlin 的 Android 库项目和一个 Swift 包。它还包含一个示例移动命令,展示如何从 Rust 代码触发其执行。
🌐 Plugins can run native mobile code written in Kotlin (or Java) and Swift. The default plugin template includes an Android library project using Kotlin and a Swift package. It includes an example mobile command showing how to trigger its execution from Rust code.
在移动插件开发指南中了解有关为移动端开发插件的更多信息。
🌐 Read more about developing plugins for mobile in the Mobile Plugin Development guide.
🌐 Plugin Configuration
在使用该插件的 Tauri 应用中,插件配置在 tauri.conf.json 上指定,其中 plugin-name 是插件的名称:
🌐 In the Tauri application where the plugin is used, the plugin configuration is specified on tauri.conf.json where plugin-name is the name of the plugin:
{ "build": { ... }, "tauri": { ... }, "plugins": { "plugin-name": { "timeout": 30 } }}插件的配置设置在 Builder 上,并在运行时解析。下面是使用 Config 结构体来指定插件配置的示例:
🌐 The plugin’s configuration is set on the Builder and is parsed at runtime. Here is an example of the Config struct being used to specify the plugin configuration:
use serde::Deserialize;use tauri::{ plugin::{Builder, TauriPlugin}, Runtime,};
// Define the plugin config#[derive(Deserialize)]pub struct Config { timeout: usize,}
pub fn init<R: Runtime>() -> TauriPlugin<R, Config> { // Make the plugin config optional // by using `Builder::<R, Option<Config>>` instead Builder::<R, Config>::new("<plugin-name>") .setup(|app, api| { let timeout = api.config().timeout; Ok(()) }) .build()}🌐 Lifecycle Events
插件可以挂接到多个生命周期事件:
🌐 Plugins can hook into several lifecycle events:
- setup:插件正在初始化
- on_navigation:网页视图正在尝试执行导航
- on_webview_ready:正在创建新窗口
- on_event:事件循环事件
- on_drop:插件正在被拆解
移动插件还有额外的lifecycle events。
🌐 There are additional lifecycle events for mobile plugins.
- 时间:插件正在初始化
- 原因:注册移动插件,管理状态,运行后台任务
use tauri::{Manager, plugin::Builder};use std::{collections::HashMap, sync::Mutex, time::Duration};
struct DummyStore(Mutex<HashMap<String, String>>);
Builder::new("<plugin-name>") .setup(|app, api| { app.manage(DummyStore(Default::default()));
let app_ = app.clone(); std::thread::spawn(move || { loop { app_.emit("tick", ()); std::thread::sleep(Duration::from_secs(1)); } });
Ok(()) })- 何时:网页视图正尝试执行导航
- 原因:验证导航或跟踪 URL 的变化
返回 false 会取消导航。
🌐 Returning false cancels the navigation.
use tauri::plugin::Builder;
Builder::new("<plugin-name>") .on_navigation(|window, url| { println!("window {} is navigating to {}", window.label(), url); // Cancels the navigation if forbidden url.scheme() != "forbidden" })- 何时:新窗口已创建
- 原因:为每个窗口执行初始化脚本
use tauri::plugin::Builder;
Builder::new("<plugin-name>") .on_webview_ready(|window| { window.listen("content-loaded", |event| { println!("webview content has been loaded"); }); })- 时间:事件循环事件
- 为什么:处理核心事件,例如窗口事件、菜单事件和应用退出请求
通过这个生命周期钩子,你可以收到任何事件循环事件的通知。
🌐 With this lifecycle hook you can be notified of any event loop events.
use std::{collections::HashMap, fs::write, sync::Mutex};use tauri::{plugin::Builder, Manager, RunEvent};
struct DummyStore(Mutex<HashMap<String, String>>);
Builder::new("<plugin-name>") .setup(|app, _api| { app.manage(DummyStore(Default::default())); Ok(()) }) .on_event(|app, event| { match event { RunEvent::ExitRequested { api, .. } => { // user requested a window to be closed and there's no windows left
// we can prevent the app from exiting: api.prevent_exit(); } RunEvent::Exit => { // app is going to exit, you can cleanup here
let store = app.state::<DummyStore>(); write( app.path().app_local_data_dir().unwrap().join("store.json"), serde_json::to_string(&*store.0.lock().unwrap()).unwrap(), ) .unwrap(); } _ => {} } })- 何时:插件正在被拆解
- 原因:在插件被销毁时执行代码
有关更多信息,请参见 Drop。
🌐 See Drop for more information.
use tauri::plugin::Builder;
Builder::new("<plugin-name>") .on_drop(|app| { // plugin has been destroyed... })🌐 Exposing Rust APIs
项目的 desktop.rs 和 mobile.rs 中定义的插件 API 以与插件同名(Pascal 大小写)的结构体形式导出给用户。当插件被设置时,会创建这个结构体的实例并作为状态进行管理,以便用户可以通过插件中定义的扩展特性,使用 Manager 实例(例如 AppHandle、App 或 Window)在任何时间点检索它。
🌐 The plugin APIs defined in the project’s desktop.rs and mobile.rs are exported to the user as a struct with the same name as the plugin (in pascal case). When the plugin is setup, an instance of this struct is created and managed as a state so that users can retrieve it at any point in time with a Manager instance (such as AppHandle, App, or Window) through the extension trait defined in the plugin.
例如,global-shortcut plugin 定义了一个 GlobalShortcut 结构体,可以通过 GlobalShortcutExt 特质的 global_shortcut 方法来读取:
🌐 For example, the global-shortcut plugin defines a GlobalShortcut struct that can be read by using the global_shortcut method of the GlobalShortcutExt trait:
use tauri_plugin_global_shortcut::GlobalShortcutExt;
tauri::Builder::default() .plugin(tauri_plugin_global_shortcut::init()) .setup(|app| { app.global_shortcut().register(...); Ok(()) })🌐 Adding Commands
命令在 commands.rs 文件中定义。它们是常规的 Tauri 应用命令。它们可以直接访问 AppHandle 和 Window 实例,访问状态,并以与应用命令相同的方式接收输入。有关 Tauri 命令的更多详细信息,请阅读 命令指南。
🌐 Commands are defined in the commands.rs file. They are regular Tauri applications commands. They can access the AppHandle and Window instances directly, access state, and take input the same way as application commands. Read the Commands guide for more details on Tauri commands.
此命令显示了如何通过依赖注入访问 AppHandle 和 Window 实例,并接受两个输入参数(on_progress 和 url):
🌐 This command shows how to get access to the AppHandle and Window instance via dependency injection, and takes two input parameters (on_progress and url):
use tauri::{command, ipc::Channel, AppHandle, Runtime, Window};
#[command]async fn upload<R: Runtime>(app: AppHandle<R>, window: Window<R>, on_progress: Channel, url: String) { // implement command logic here on_progress.send(100).unwrap();}要将命令暴露给 webview,你必须钩子 lib.rs 中的 invoke_handler() 调用:
🌐 To expose the command to the webview, you must hook into the invoke_handler() call in lib.rs:
Builder::new("<plugin-name>") .invoke_handler(tauri::generate_handler![commands::upload])在 webview-src/index.ts 中定义一个绑定函数,以便插件用户可以在 JavaScript 中轻松调用该命令:
🌐 Define a binding function in webview-src/index.ts so that plugin users can easily call the command in JavaScript:
import { invoke, Channel } from '@tauri-apps/api/core'
export async function upload(url: string, onProgressHandler: (progress: number) => void): Promise<void> { const onProgress = new Channel<number>() onProgress.onmessage = onProgressHandler await invoke('plugin:<plugin-name>|upload', { url, onProgress })}请务必在测试之前构建 TypeScript 代码。
🌐 Be sure to build the TypeScript code prior to testing it.
🌐 Command Permissions
默认情况下,前端无法访问你的命令。如果你尝试执行其中一个命令,你将收到拒绝错误。要实际公开命令,你还需要定义允许每个命令的权限。
🌐 By default your commands are not accessible by the frontend. If you try to execute one of them, you will get a denied error rejection. To actually expose commands, you also need to define permissions that allow each command.
🌐 Permission Files
权限被定义为位于 permissions 目录中的 JSON 或 TOML 文件。每个文件可以定义权限列表、权限集列表以及插件的默认权限。
🌐 Permissions are defined as JSON or TOML files inside the permissions directory. Each file can define a list of permissions, a list of permission sets and your plugin’s default permission.
🌐 Permissions
权限描述了你的插件命令的特权。它可以允许或拒绝命令列表,并关联特定命令和全局范围。
🌐 A permission describes privileges of your plugin commands. It can allow or deny a list of commands and associate command-specific and global scopes.
"$schema" = "schemas/schema.json"
[[permission]]identifier = "allow-start-server"description = "Enables the start_server command."commands.allow = ["start_server"]
[[permission]]identifier = "deny-start-server"description = "Denies the start_server command."commands.deny = ["start_server"]🌐 Scope
作用域允许你的插件对单个命令定义更深层次的限制。每个权限可以定义一个作用域对象的列表,这些对象规定某些操作是允许还是禁止,可以是针对特定命令的,也可以是针对整个插件的。
🌐 Scopes allow your plugin to define deeper restrictions to individual commands. Each permission can define a list of scope objects that define something to be allowed or denied either specific to a command or globally to the plugin.
让我们定义一个示例结构体,用于保存 shell 插件允许生成的二进制文件列表的作用域数据:
🌐 Let’s define an example struct that will hold scope data for a list of binaries a shell plugin is allowed to spawn:
#[derive(Debug, schemars::JsonSchema)]pub struct Entry { pub binary: String,}🌐 Command Scope
你的插件使用者可以在他们的功能文件中为特定命令定义一个范围(请参阅文档)。
你可以使用 tauri::ipc::CommandScope 结构读取特定命令的范围:
🌐 Your plugin consumer can define a scope for a specific command in their capability file (see the documentation).
You can read the command-specific scope with the tauri::ipc::CommandScope struct:
use tauri::ipc::CommandScope;use crate::scope::Entry;
async fn spawn<R: tauri::Runtime>(app: tauri::AppHandle<R>, command_scope: CommandScope<'_, Entry>) -> Result<()> { let allowed = command_scope.allows(); let denied = command_scope.denies(); todo!()}🌐 Global Scope
当权限未定义任何要允许或拒绝的命令时,它被视为范围权限,并且它应仅为你的插件定义全局范围:
🌐 When a permission does not define any commands to be allowed or denied, it’s considered a scope permission and it should only define a global scope for your plugin:
[[permission]]identifier = "allow-spawn-node"description = "This scope permits spawning the `node` binary."
[[permission.scope.allow]]binary = "node"你可以使用 tauri::ipc::GlobalScope 结构读取全局范围:
🌐 You can read the global scope with the tauri::ipc::GlobalScope struct:
use tauri::ipc::GlobalScope;use crate::scope::Entry;
async fn spawn<R: tauri::Runtime>(app: tauri::AppHandle<R>, scope: GlobalScope<'_, Entry>) -> Result<()> { let allowed = scope.allows(); let denied = scope.denies(); todo!()}🌐 Schema
作用域条目需要 schemars 依赖来生成 JSON 模式,以便插件使用者了解作用域的格式并在他们的 IDE 中获得自动补齐。
🌐 The scope entry requires the schemars dependency to generate a JSON schema so the plugin consumers know the format of the scope and have autocomplete in their IDEs.
要定义模式,首先将依赖添加到你的 Cargo.toml 文件中:
🌐 To define the schema, first add the dependency to your Cargo.toml file:
# we need to add schemars to both dependencies and build-dependencies because the scope.rs module is shared between the app code and build script[dependencies]schemars = "0.8"
[build-dependencies]schemars = "0.8"在你的构建脚本中,添加以下代码:
🌐 In your build script, add the following code:
#[path = "src/scope.rs"]mod scope;
const COMMANDS: &[&str] = &[];
fn main() { tauri_plugin::Builder::new(COMMANDS) .global_scope_schema(schemars::schema_for!(scope::Entry)) .build();}🌐 Permission Sets
权限集是一组单独的权限,帮助用户以更高的抽象级别管理你的插件。 例如,如果单个 API 使用多个命令,或者一组命令之间存在逻辑关联,你应该定义一个包含它们的集合:
🌐 Permission sets are groups of individual permissions that helps users manage your plugin with a higher level of abstraction. For instance if a single API uses multiple commands or if there’s a logical connection between a collection of commands, you should define a set containing them:
"$schema" = "schemas/schema.json"[[set]]identifier = "allow-websocket"description = "Allows connecting and sending messages through a WebSocket"permissions = ["allow-connect", "allow-send"]🌐 Default Permission
默认权限是一个标识符为 default 的特殊权限集。建议你默认启用所需的命令。例如,没有允许 request 命令,http 插件将毫无用处:
🌐 The default permission is a special permission set with identifier default. It’s recommended that you enable required commands by default.
For instance the http plugin is useless without the request command allowed:
"$schema" = "schemas/schema.json"[default]description = "Allows making HTTP requests"permissions = ["allow-request"]🌐 Autogenerated Permissions
为每个命令定义权限的最简单方法是使用在插件的构建脚本中定义的自动生成选项,该脚本位于 build.rs 文件中。在 COMMANDS 常量中,按 snake_case 定义命令列表(应与命令函数名称匹配),Tauri 将自动生成 allow-$commandname 和 deny-$commandname 权限。
🌐 The easiest way to define permissions for each of your commands is to use the autogeneration option defined in your plugin’s build script defined in the build.rs file.
Inside the COMMANDS const, define the list of commands in snake_case (should match the command function name) and Tauri will automatically generate an allow-$commandname and a deny-$commandname permissions.
以下示例生成 allow-upload 和 deny-upload 权限:
🌐 The following example generates the allow-upload and deny-upload permissions:
const COMMANDS: &[&str] = &["upload"];
fn main() { tauri_plugin::Builder::new(COMMANDS).build();}有关更多信息,请参见 权限概览 文档。
🌐 See the Permissions Overview documentation for more information.
🌐 Managing State
插件可以像 Tauri 应用一样管理状态。更多信息请阅读《状态管理指南》。
🌐 A plugin can manage state in the same way a Tauri application does. Read the State Management guide for more information.
Tauri 中文网 - 粤ICP备13048890号
Nodejs.cn 旗下网站