ARTICLE · 1076028
Tauri 插件源码怎么读:以一个官方插件为例
一、插件仓库的目录结构
官方插件都集中在一个 monorepo(plugins-workspace)里,每个插件一个目录,结构基本一致。挑关键的几个看:
plugins-workspace/plugins/clipboard-manager/├── src/ # Rust 源码:命令实现、插件初始化│ ├── lib.rs # init() 入口、Builder、命令注册│ ├── commands.rs # #[tauri::command] 命令本体│ └── error.rs # 插件自己的错误类型├── guest-js/ # JS 侧源码(TypeScript)│ └── index.ts # 对 invoke 的封装,发布成 npm 包├── permissions/ # 权限定义(ACL)│ ├── default.toml # 默认权限集│ └── allow-read-text.toml / deny-write-text.toml ...├── Cargo.toml # Rust 包配置(tauri-plugin-clipboard-manager)├── package.json # npm 包配置(@tauri-apps/plugin-clipboard-manager)└── build.rs # 构建脚本:从命令自动生成权限文件
二、一条调用的完整链路
先看全景。从调用 readText() 到拿到剪贴板内容,中间经历这几步:

这张图是读任何插件源码的地图。下面按这个顺序看两侧代码。
三、JS 侧:guest-js 里就是 invoke 封装
打开 guest-js/index.ts,核心代码比想象中短:
import { invoke } from '@tauri-apps/api/core';export async function readText(): Promise<string> { return invoke('plugin:clipboard-manager|read_text');}export async function writeText(text: string): Promise<void> { await invoke('plugin:clipboard-manager|write_text', { text });}
注意命令名的格式:plugin:插件名|命令名。这是插件命令和应用自有 command 的区别:我们平时 invoke('get_config') 调的是自己写的命令,插件命令带 plugin: 前缀,由插件注册进来,权限也是独立管理的。读源码时看到这个前缀,就能顺着去 Rust 侧找对应的 #[tauri::command]。
四、Rust 侧:命令注册与系统调用
Rust 侧先看入口 lib.rs 的 init():
pub fn init<R: Runtime>() -> TauriPlugin<R> { Builder::new("clipboard-manager") // 插件名,与 JS 侧对应 .invoke_handler(tauri::generate_handler![ commands::read_text, commands::write_text, // 其他命令... ]) .setup(|app| { // 插件级初始化:管理状态、平台差异处理 #[cfg(desktop)] app.handle().plugin(...)?; // 部分能力只在桌面端注册 Ok(()) }) .build()}
再看 commands.rs 里的命令本体,就是普通的 #[tauri::command]:
#[tauri::command]pub async fn read_text<R: Runtime>( clipboard: tauri::State<'_, Clipboard<R>>,) -> Result<String, Error> { clipboard.read_text().map_err(Into::into)}
真干活的 Clipboard 结构里才是平台差异所在:Windows 走一套 API,Linux 要处理 Wayland / X11,macOS 又是另一套。你遇到的「Linux 上这个方法不行」,答案就在这些 #[cfg(target_os)] 分支里。
五、permissions 目录:权限从哪来
permissions/ 下那些 toml 文件是插件的权限清单。以 clipboard-manager 为例,有 allow-read-text、allow-write-text、deny-write-text 等一组文件,default.toml 定义默认给哪些:
# permissions/default.toml(示意)"$schema" = "schemas/schema.json"[default]description = "允许读写剪贴板文本"permissions = ["allow-read-text", "allow-write-text"]
关键机制在 build.rs:权限文件是构建时从命令自动生成的,每个 #[tauri::command] 对应一个 allow-命令名 和 deny-命令名。这正是「capabilities 白名单红线」能精确到单个命令的原因——权限系统那篇讲的能力模型,插件就是照这套机制自己声明权限的。
六、一次真实的源码排查
分享一次我自己的经历。做剪贴板监听时,我发现轮询 readText() 偶尔会拿到上一次的内容,本地怎么都复现不了稳定规律。翻源码看到 Clipboard 的实现注释才明白:Linux 下部分桌面环境的剪贴板是「请求时才提供内容」,太快轮询时可能命中空值,插件内部对空值做了保留上次内容的处理。搞清楚机制后,把轮询间隔放宽并加了内容判空,问题消失。
文档里不会有这段,issue 里也只有零星讨论,但源码就在那里。这也是我想传达的:插件源码不难读,结构就这几样——guest-js 的 invoke 封装、src 下的命令实现、permissions 的权限声明,比业务代码还规整。
七、该自己动手了
读完官方插件的源码,你会发现「写一个插件」并没有多神秘:同样的目录结构、同样的命令注册、同样的权限声明。下一篇就自己动手写一个完整的插件——从脚手架搭起来,到定义权限,最后封装成 Vue 组合式函数在组件里用。觉得有用的话,点赞、在看、转发三连支持一下。