developing-tauri-plugins

安装量: 36
排名: #19312

安装

npx skills add https://github.com/dchuk/claude-code-tauri-skills --skill developing-tauri-plugins

Developing Tauri Plugins

Tauri plugins extend application functionality through modular Rust crates with optional JavaScript bindings and native mobile implementations.

Plugin Architecture

A complete plugin includes:

Rust crate (tauri-plugin-{name}) - Core logic JavaScript bindings (@scope/plugin-{name}) - NPM package Android library (Kotlin) - Optional iOS package (Swift) - Optional Creating a Plugin npx @tauri-apps/cli plugin new my-plugin # Basic npx @tauri-apps/cli plugin new my-plugin --android --ios # With mobile npx @tauri-apps/cli plugin android add # Add to existing npx @tauri-apps/cli plugin ios add

Project Structure tauri-plugin-my-plugin/ ├── src/ │ ├── lib.rs, commands.rs, desktop.rs, mobile.rs, error.rs ├── permissions/ # Permission TOML files ├── guest-js/index.ts # TypeScript API ├── android/, ios/ # Native mobile code ├── build.rs, Cargo.toml

Plugin Implementation Main Plugin File (lib.rs) use tauri::{plugin::{Builder, TauriPlugin}, Manager, Runtime}; mod commands; mod error; pub use error::{Error, Result};

[cfg(desktop)] mod desktop;

[cfg(mobile)] mod mobile;

[cfg(desktop)] use desktop::MyPlugin;

[cfg(mobile)] use mobile::MyPlugin;

pub struct MyPluginState(pub MyPlugin);

pub fn init() -> TauriPlugin { Builder::new("my-plugin") .invoke_handler(tauri::generate_handler![commands::do_something]) .setup(|app, api| { app.manage(MyPluginState(MyPlugin::new(app, api)?)); Ok(()) }) .build() }

Plugin with Configuration use serde::Deserialize;

[derive(Debug, Deserialize)]

pub struct Config { pub timeout: Option, pub enabled: bool }

pub fn init() -> TauriPlugin { Builder::::new("my-plugin") .setup(|app, api| { let config = api.config(); Ok(()) }) .build() }

Commands (commands.rs) use tauri::{command, ipc::Channel, Runtime, State}; use crate::{MyPluginState, Result};

[command]

pub async fn do_something( state: State<'_, MyPluginState>, input: String, ) -> Result { state.0.do_something(input).await }

[command]

pub async fn upload(path: String, on_progress: Channel) -> Result<()> { for i in 0..=100 { on_progress.send(i)?; } Ok(()) }

Desktop Implementation (desktop.rs) use tauri::{AppHandle, Runtime}; use crate::Result;

pub struct MyPlugin { app: AppHandle }

impl MyPlugin { pub fn new(app: &AppHandle, _api: tauri::plugin::PluginApi) -> Result { Ok(Self { app: app.clone() }) } pub async fn do_something(&self, input: String) -> Result { Ok(format!("Desktop: {}", input)) } }

Mobile Implementation (mobile.rs) use tauri::{AppHandle, Runtime}; use serde::{Deserialize, Serialize}; use crate::Result;

[derive(Serialize)] struct MobileRequest

[derive(Deserialize)] struct MobileResponse

pub struct MyPlugin { app: AppHandle }

impl MyPlugin { pub fn new(app: &AppHandle, _api: tauri::plugin::PluginApi) -> Result { Ok(Self { app: app.clone() }) } pub async fn do_something(&self, input: String) -> Result { let response: MobileResponse = self.app .run_mobile_plugin("doSomething", MobileRequest { value: input }) .map_err(|e| crate::Error::Mobile(e.to_string()))?; Ok(response.result) } }

Error Handling (error.rs) use serde::{Serialize, Serializer};

[derive(Debug, thiserror::Error)]

pub enum Error { #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("Mobile error: {0}")] Mobile(String), }

impl Serialize for Error { fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer { serializer.serialize_str(self.to_string().as_str()) } } pub type Result = std::result::Result;

Lifecycle Events Builder::new("my-plugin") .setup(|app, api| { Ok(()) }) // Plugin init .on_navigation(|window, url| url.scheme() != "dangerous") // Block nav .on_webview_ready(|window| {}) // Window created .on_event(|app, event| { match event { tauri::RunEvent::Exit => {} _ => {} }}) .on_drop(|app| {}) // Cleanup .build()

JavaScript Bindings (guest-js/index.ts) import { invoke, Channel } from '@tauri-apps/api/core';

export async function doSomething(input: string): Promise { return invoke('plugin:my-plugin|do_something', { input }); }

export async function upload(path: string, onProgress: (p: number) => void): Promise { const channel = new Channel(); channel.onmessage = onProgress; return invoke('plugin:my-plugin|upload', { path, onProgress: channel }); }

Plugin Permissions Permission File (permissions/default.toml) [ default ] description = "Default permissions" permissions = ["allow-do-something"]

[[ permission ]] identifier = "allow-do-something" description = "Allows do_something command" commands.allow = ["do_something"]

[[ permission ]] identifier = "allow-upload" description = "Allows upload command" commands.allow = ["upload"]

[[ set ]] identifier = "full-access" description = "Full plugin access" permissions = ["allow-do-something", "allow-upload"]

Build Script (build.rs) const COMMANDS: &[&str] = &["do_something", "upload"]; fn main() { tauri_plugin::Builder::new(COMMANDS).build(); }

Scoped Permissions use tauri::ipc::CommandScope; use serde::Deserialize;

[derive(Debug, Deserialize)]

pub struct PathScope { pub path: String }

[command]

pub async fn read_file(path: String, scope: CommandScope<'_, PathScope>) -> Result { let allowed = scope.allows().iter().any(|s| path.starts_with(&s.path)); let denied = scope.denies().iter().any(|s| path.starts_with(&s.path)); if denied || !allowed { return Err(Error::PermissionDenied); } // Read file... }

Android Plugin (Kotlin) package com.example.myplugin

import android.app.Activity import app.tauri.annotation.Command import app.tauri.annotation.InvokeArg import app.tauri.annotation.TauriPlugin import app.tauri.plugin.Invoke import app.tauri.plugin.JSObject import app.tauri.plugin.Plugin import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch

@InvokeArg class DoSomethingArgs { lateinit var value: String // Required var optional: String? = null // Optional var withDefault: Int = 42 // Default value }

@TauriPlugin class MyPlugin(private val activity: Activity) : Plugin(activity) { @Command fun doSomething(invoke: Invoke) { val args = invoke.parseArgs(DoSomethingArgs::class.java) CoroutineScope(Dispatchers.IO).launch { // Use IO for blocking ops try { invoke.resolve(JSObject().apply { put("result", "Android: ${args.value}") }) } catch (e: Exception) { invoke.reject(e.message) } } } }

Android Permissions @TauriPlugin(permissions = [ Permission(strings = [android.Manifest.permission.CAMERA], alias = "camera") ]) class MyPlugin(private val activity: Activity) : Plugin(activity) { @Command override fun checkPermissions(invoke: Invoke) { super.checkPermissions(invoke) } @Command override fun requestPermissions(invoke: Invoke) { super.requestPermissions(invoke) } }

Android Events & JNI // Emit event trigger("dataReceived", JSObject().apply { put("data", "value") })

// Lifecycle override fun onNewIntent(intent: Intent) { trigger("newIntent", JSObject().apply { put("action", intent.action) }) }

// Call Rust via JNI companion object { init { System.loadLibrary("my_plugin") } } external fun processData(input: String): String // Java_com_example_myplugin_MyPlugin_processData

iOS Plugin (Swift) import SwiftRs import Tauri import UIKit

class DoSomethingArgs: Decodable { let value: String // Required var optional: String? // Optional }

class MyPlugin: Plugin { @objc public func doSomething(_ invoke: Invoke) throws { let args = try invoke.parseArgs(DoSomethingArgs.self) invoke.resolve(["result": "iOS: (args.value)"]) } }

@_cdecl("init_plugin_my_plugin") func initPlugin() -> Plugin { return MyPlugin() }

iOS Permissions import AVFoundation

class MyPlugin: Plugin { @objc override func checkPermissions(_ invoke: Invoke) { var result: [String: String] = [:] switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: result["camera"] = "granted" case .denied, .restricted: result["camera"] = "denied" default: result["camera"] = "prompt" } invoke.resolve(result) }

@objc override func requestPermissions(_ invoke: Invoke) {
    AVCaptureDevice.requestAccess(for: .video) { _ in self.checkPermissions(invoke) }
}

}

iOS Events & FFI // Emit event trigger("dataReceived", data: ["data": "value"])

// Call Rust via FFI @silgen_name("process_data_ffi") private static func processDataFFI( input: UnsafePointer) -> UnsafeMutablePointer?

@objc public func hybrid(_ invoke: Invoke) throws { let args = try invoke.parseArgs(DoSomethingArgs.self) guard let ptr = MyPlugin.processDataFFI(args.value) else { invoke.reject("FFI failed"); return } invoke.resolve(["result": String(cString: ptr)]) ptr.deallocate() }

Using the Plugin Register (src-tauri/src/lib.rs) pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_my_plugin::init()) .run(tauri::generate_context!()) .expect("error running application"); }

Configure (tauri.conf.json) { "plugins": { "my-plugin": { "timeout": 60, "enabled": true } } }

Permissions (capabilities/default.json)

Frontend Usage import { doSomething, upload } from '@myorg/plugin-my-plugin'; const result = await doSomething('hello'); await upload('/path/to/file', (p) => console.log(${p}%));

Best Practices Separate platform code in desktop.rs and mobile.rs Use thiserror for structured error handling Use async for I/O operations; request only necessary permissions Android: Commands run on main thread - use coroutines for blocking work iOS: Clean up FFI resources properly; use invoke.reject()/invoke.resolve() Android 16KB Page Size

For NDK < 28, add to .cargo/config.toml:

[ target.aarch64-linux-android ] rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]

返回排行榜