造芯从定型开始。命令是芯的门面,门面的契约——参数怎么进来、结果怎么出去——全部由 serde 序列化框架定义。本节把契约的四个部位装全:标量与结构体参数、Result 返回、命名转换、以及"两端类型只写一遍"的工程方案。读完本节,第 4 章前端 bridge 里"手写对齐"的隐患就在芯侧根除了。
前端与芯之间的载荷是 JSON,serde 负责 Rust 结构与 JSON 的互转。它的工作方式是派生宏:给结构体贴上标记,编译期生成转换代码,零运行时反射。命令契约因此落成一条铁律:凡是过桥的类型,必派生 Serialize 或 Deserialize。参数结构派生 Deserialize(JSON 进来变结构体),返回结构派生 Serialize(结构体出去变 JSON)。
标量参数第 4 章已见过,实战里超过三个参数就应升级为结构体——参数成组、校验集中、改名只动一处:
use serde::Deserialize; #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct SearchQuery { pub keyword: String, pub only_pinned: bool, pub limit: u32, } #[tauri::command] fn search_notes(query: SearchQuery) -> Result<Vec<NoteSummary>, String> { if query.keyword.trim().is_empty() { return Err("关键词不能为空".into()); } let limit = query.limit.clamp(1, 100); // 上限钳制,防恶意大请求 do_search(&query.keyword, query.only_pinned, limit) }
前端对应:
export interface SearchQuery { keyword: string; onlyPinned: boolean; limit: number; } export const searchNotes = (q: SearchQuery) => invoke<NoteSummary[]>('searchNotes', { query });
注意嵌套规则:结构体参数是一个命名参数,前端传 { query: {...} };rename_all = "camelCase" 把 only_pinned 自动转成 onlyPinned——转换发生在 serde 层,与 Tauri 的命令名转换互不干扰。再点两处安全细节:clamp 钳制 limit 防止"一次拉百万条"的滥用;trim 后判空拒绝无意义请求。芯侧校验不是可选项,第 6 章会把它上升为安全原则。
最简形式是 Result<T, String>——快速原型够用,但字符串错误的调用方无法程序化分流。规范形态是自定义错误类型(5.4 节专讲),本节先把返回侧的另一半装好:大结构体的输出成本。返回 Vec 千条笔记,序列化与传输都可观;工程做法是返回"摘要列表加按需详情"两级接口——列表只带 id、标题、时间,正文等点开时按 id 再取。这既是性能习惯(2.1 的序列化边界),也是接口设计习惯。
把散落在 4.2 的规则收口成一张表:
| Rust 侧 | 前端侧 | 机制 |
|---|---|---|
| fn add_note | invoke('addNote') | 命令名自动蛇形转驼峰 |
| created_at 字段 | createdAt | 参数与返回字段自动转换 |
| #[serde(rename_all="camelCase")] | 显式接管 | 覆盖默认,嵌套结构推荐 |
| rename = "xyz" | 单字段改名 | 特殊对齐场景 |
默认转换覆盖不到的坑在嵌套:外层参数对象的键由 Tauri 转换,嵌套结构体内部的键由 serde 决定——未派生 rename_all 的嵌套结构保持蛇形。两端写法不一致时症状是"字段永远是默认值",记住查嵌套。
手写两遍 TS 与 Rust 结构,改一处漏一处只是时间问题。工程方案是单向生成:Rust 侧是事实源,用类型生成工具在构建时把结构体导出成 TypeScript 定义。做法三步:Cargo.toml 加生成依赖;结构体补导出标记;构建脚本把生成的类型文件放进前端工程:
// Cargo.toml 加:specta = { version = "0.22", features = ["typescript"] }(示意版本,以官方文档为准) #[derive(Deserialize, Serialize, specta::Type)] #[serde(rename_all = "camelCase")] pub struct NoteSummary { pub id: u64, pub title: String, pub updated_at: i64, }
生成物进前端后,bridge 模块的 interface 全部改为从生成文件 re-export。从此类型漂移在编译期报错,而不是在用户桌面报错。项目小到十个命令以内、结构稳定,手写也能忍;超过这个规模,生成立刻回本。
两个进阶特性提前认识,用到时不会手忙脚乱。其一,隐藏命令:#[tauri::command] 支持把函数从生成的调用面里隐藏,仅作为内部辅助被其他命令调用——工具函数别急着暴露成命令,调用面越小越好(第 6 章的安全逻辑在背后支持这个直觉:每个命令都是一张要发卡的门)。其二,命令重命名:Rust 函数名与前端调用名可通过属性显式指定,接历史命名或对齐旧接口时有用;但重命名是特例不是习惯——保持默认的蛇形转驼峰规则,两端的对应关系才能靠约定自动成立。
还有一个实战技巧放在这里:命令粒度对齐用户操作。一个"保存笔记"的用户动作可能涉及校验、写盘、更新索引、记录历史四步,这四步应该在一条命令内完成,而不是前端连发四条命令拼装——前者把事务性留在芯侧,失败可整体回滚;后者把中间状态暴露给前端,多一種失败形态,多一倍排错面。命令是业务动作的镜像,不是底层函数的直通管。
契约定型了。下一节给芯上夹具:应用状态怎么管、并发怎么不炸、耗时任务怎么不冻界面。