Chapter 08

插件生态实战

充分利用 Tauri 官方插件生态,快速实现存储、数据库、网络等核心功能

官方插件清单

插件功能npm 包
plugin-fs文件系统操作@tauri-apps/plugin-fs
plugin-dialog系统对话框@tauri-apps/plugin-dialog
plugin-notification系统通知@tauri-apps/plugin-notification
plugin-store持久化键值存储@tauri-apps/plugin-store
plugin-sqlSQLite 数据库@tauri-apps/plugin-sql
plugin-httpHTTP 客户端@tauri-apps/plugin-http
plugin-shell运行外部命令@tauri-apps/plugin-shell
plugin-updater自动更新@tauri-apps/plugin-updater
plugin-global-shortcut全局快捷键@tauri-apps/plugin-global-shortcut
plugin-clipboard-manager剪贴板@tauri-apps/plugin-clipboard-manager

plugin-store:持久化键值存储

plugin-store 提供简单的持久化键值存储,数据自动保存到应用数据目录,适合存储用户偏好设置:

npm install @tauri-apps/plugin-store
import { Store } from '@tauri-apps/plugin-store';

// 创建/打开 store(文件名即存储标识)
const store = await Store.load('settings.json', {
  autoSave: true,  // 每次修改后自动保存
});

// 写入值
await store.set('theme', 'dark');
await store.set('windowSize', { width: 1200, height: 800 });
await store.set('recentFiles', ['/tmp/a.txt', '/tmp/b.txt']);

// 读取值(带默认值)
const theme = await store.get<string>('theme') ?? 'light';
const windowSize = await store.get<{ width: number; height: number }>('windowSize');

// 检查 key 是否存在
const hasTheme = await store.has('theme');

// 删除
await store.delete('recentFiles');

// 手动保存(autoSave 为 false 时需要)
await store.save();

// 获取所有 key
const keys = await store.keys();
console.log(keys); // ['theme', 'windowSize']

plugin-sql:SQLite 数据库

npm install @tauri-apps/plugin-sql
# Cargo.toml
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
# 也支持 "mysql" 和 "postgres"
import Database from '@tauri-apps/plugin-sql';

// 连接 SQLite 数据库(文件位于 appDataDir)
const db = await Database.load('sqlite:app.db');

// 创建表
await db.execute(`
  CREATE TABLE IF NOT EXISTS todos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    completed BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  )
`);

// 插入数据
const result = await db.execute(
  'INSERT INTO todos (title) VALUES ($1)',
  ['Learn Tauri']
);
console.log('Inserted ID:', result.lastInsertId);

// 查询数据
interface Todo {
  id: number;
  title: string;
  completed: boolean;
  created_at: string;
}
const todos = await db.select<Todo[]>(
  'SELECT * FROM todos WHERE completed = $1 ORDER BY created_at DESC',
  [false]
);

// 更新
await db.execute(
  'UPDATE todos SET completed = $1 WHERE id = $2',
  [true, 1]
);

// 事务
await db.execute('BEGIN');
try {
  await db.execute('INSERT INTO todos (title) VALUES ($1)', ['Task A']);
  await db.execute('INSERT INTO todos (title) VALUES ($1)', ['Task B']);
  await db.execute('COMMIT');
} catch {
  await db.execute('ROLLBACK');
}

plugin-http:突破 CORS 限制

WebView 中直接发 fetch 请求受 CORS 限制,plugin-http 使用 Rust 的 reqwest 库发出请求,绕过浏览器的 CORS 检查:

import { fetch } from '@tauri-apps/plugin-http';

// 与浏览器 fetch API 兼容,但从 Rust 后端发出请求
const response = await fetch('https://api.example.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token',
  },
  body: JSON.stringify({ query: 'search term' }),
});

const data = await response.json();

plugin-global-shortcut:全局快捷键

import { register, unregister, isRegistered } from '@tauri-apps/plugin-global-shortcut';

// 注册全局快捷键(应用不在前台也有效)
await register('CommandOrControl+Shift+Space', () => {
  console.log('Global shortcut triggered!');
});

// 注册多个快捷键
await register(['CommandOrControl+K', 'F1'], (shortcut) => {
  console.log('Pressed:', shortcut);
});

// 检查是否已注册
const registered = await isRegistered('CommandOrControl+K');

// 注销
await unregister('CommandOrControl+K');

自定义 Rust 插件

# 使用 tauri-plugin 模板创建插件
cargo generate --git https://github.com/tauri-apps/tauri-plugin --name tauri-plugin-myplugin
# 或手动创建
cargo new --lib tauri-plugin-myplugin
// tauri-plugin-myplugin/src/lib.rs
use tauri::{
    plugin::{Builder, TauriPlugin},
    Runtime,
};

// 插件暴露的 Command
#[tauri::command]
fn my_custom_function(input: String) -> String {
    format!("Processed: {}", input.to_uppercase())
}

// 插件初始化函数
pub fn init<R: Runtime>() -> TauriPlugin<R> {
    Builder::new("myplugin")
        .invoke_handler(tauri::generate_handler![my_custom_function])
        .setup(|app, _api| {
            // 插件初始化逻辑(可以注册状态等)
            println!("MyPlugin initialized");
            Ok(())
        })
        .build()
}

// 在主应用中使用
// .plugin(tauri_plugin_myplugin::init())
社区插件资源

除官方插件外,社区提供了大量实用插件:tauri-plugin-log(结构化日志)、tauri-plugin-autostart(开机自启)、tauri-plugin-window-state(记住窗口大小/位置)等。访问 tauri-apps/plugins-workspace 查看全部官方插件,或在 crates.io 搜索 "tauri-plugin" 发现社区贡献。

Tauri 插件系统的工作原理

插件的架构层次

Tauri 插件是 Tauri 功能扩展的标准机制,理解其架构有助于正确选择是使用插件还是直接写 Command:

插件与 Command 的区别
Command(#[tauri::command])是应用内部的功能,直接写在 src-tauri/src/ 中。插件是可复用的、可分发的 Tauri 扩展模块,有独立的 Cargo 包,可以发布到 crates.io 让其他 Tauri 应用使用。如果你只是给自己的应用加功能,用 Command;如果要复用或分享,用 Plugin。
插件的生命周期钩子
插件 Builder 提供了多个生命周期钩子:setup(应用启动时初始化,注册状态、启动后台线程)、on_page_load(每次 WebView 加载页面时)、on_event(应用事件,如窗口创建/销毁)。这允许插件在应用的关键时机执行初始化和清理逻辑。
permissions 系统(Tauri 2.0 特性)
Tauri 2.0 引入了细粒度的权限系统(Capabilities):每个插件声明自己的权限集,应用通过 capabilities/*.json 配置文件显式授予前端哪些权限。这是 Tauri 2.0 安全模型的核心——前端只能调用被显式允许的 API,即使前端被 XSS 攻击,攻击者也无法访问未授权的系统 API。

什么时候应该写自定义插件?

用 Command(最常见):
  ✓ 功能只供当前应用使用
  ✓ 需要访问应用特定的状态(数据库连接、配置)
  ✓ 快速原型或一次性功能

用 Plugin(可复用模块):
  ✓ 功能多个应用都需要(如统一的认证模块)
  ✓ 封装复杂的 Rust 库(如密码学、图像处理)
  ✓ 需要生命周期管理(setup/teardown)
  ✓ 有独立的 npm 包提供 TypeScript 类型

Plugin 的典型结构:
  tauri-plugin-myplugin/
  ├── src/           ← Rust 代码
  │   ├── lib.rs     ← Plugin Builder + Commands
  │   └── models.rs  ← 类型定义
  ├── guest-js/      ← TypeScript 封装(可选)
  │   └── index.ts   ← JS API,供前端 import 使用
  ├── Cargo.toml
  └── package.json   ← npm 包配置

plugin-shell:运行外部命令

plugin-shell 允许 Tauri 应用运行外部命令(如调用 FFmpeg、打开终端、运行脚本),支持捕获输出和状态码:

import { Command } from '@tauri-apps/plugin-shell';

// 运行命令并等待结果
const result = await Command.create('ffmpeg', [
  '-i', '/path/to/input.mp4',
  '-vcodec', 'h264',
  '/path/to/output.mp4',
]).execute();

console.log('Exit code:', result.code);
console.log('stdout:', result.stdout);
console.log('stderr:', result.stderr);

// 流式读取输出(实时日志)
const command = Command.create('git', ['clone', 'https://github.com/tauri-apps/tauri.git']);

command.stdout.on('data', line => {
  console.log('stdout:', line);
});
command.stderr.on('data', line => {
  console.log('stderr:', line);
});
command.on('close', data => {
  console.log('Finished with code:', data.code);
});

await command.spawn();  // 异步运行,不阻塞
// capabilities/default.json — shell 权限
{
  "permissions": [
    {
      "identifier": "shell:allow-execute",
      "allow": [
        {
          "name": "ffmpeg",     // 允许执行的命令名(白名单)
          "cmd": "ffmpeg",
          "args": true         // 允许任意参数(或用数组限制参数)
        },
        {
          "name": "git",
          "cmd": "git",
          "args": ["clone", "pull", "push"]  // 只允许指定子命令
        }
      ]
    }
  ]
}

plugin-updater:自动更新

plugin-updater 提供应用自动更新功能,检查更新服务器并下载安装新版本:

import { check } from '@tauri-apps/plugin-updater';
import { relaunch } from '@tauri-apps/plugin-process';

async function checkAndUpdate() {
  const update = await check();

  if (!update) {
    console.log('Already on the latest version.');
    return;
  }

  console.log(`Update available: ${update.version}`);
  console.log(`Release notes: ${update.body}`);

  // 下载并安装更新(带进度回调)
  let downloaded = 0;
  let total = 0;

  await update.downloadAndInstall((event) => {
    if (event.event === 'Started') {
      total = event.data.contentLength ?? 0;
    } else if (event.event === 'Progress') {
      downloaded += event.data.chunkLength;
      const percent = total > 0 ? (downloaded / total * 100).toFixed(1) : '?';
      console.log(`Downloading... ${percent}%`);
    } else if (event.event === 'Finished') {
      console.log('Download complete.');
    }
  });

  // 重启应用以完成更新
  await relaunch();
}
// tauri.conf.json — 配置更新服务器
{
  "bundle": {
    "updater": {
      "active": true,
      "endpoints": [
        "https://releases.example.com/{{target}}/{{arch}}/{{current_version}}"
      ],
      "dialog": true,           // 显示内置更新对话框
      "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6..."  // 用 tauri signer generate 生成
    }
  }
}
更新必须签名

plugin-updater 要求更新包必须使用 tauri signer 工具生成的密钥签名。公钥存在 tauri.conf.json 中,私钥存在 CI 的 secrets 里。这防止中间人攻击——即使攻击者控制了更新服务器 URL,没有私钥签名的包也无法被安装。

本章小结

本章核心要点