5.1 命令定义:参数、返回与serde序列化


造芯从定型开始。命令是芯的门面,门面的契约——参数怎么进来、结果怎么出去——全部由 serde 序列化框架定义。本节把契约的四个部位装全:标量与结构体参数、Result 返回、命名转换、以及"两端类型只写一遍"的工程方案。读完本节,第 4 章前端 bridge 里"手写对齐"的隐患就在芯侧根除了。

serde 是谁:两端唯一的翻译官

前端与芯之间的载荷是 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 的两种打开方式

最简形式是 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 函数名与前端调用名可通过属性显式指定,接历史命名或对齐旧接口时有用;但重命名是特例不是习惯——保持默认的蛇形转驼峰规则,两端的对应关系才能靠约定自动成立。

还有一个实战技巧放在这里:命令粒度对齐用户操作。一个"保存笔记"的用户动作可能涉及校验、写盘、更新索引、记录历史四步,这四步应该在一条命令内完成,而不是前端连发四条命令拼装——前者把事务性留在芯侧,失败可整体回滚;后者把中间状态暴露给前端,多一種失败形态,多一倍排错面。命令是业务动作的镜像,不是底层函数的直通管。

本节要点回顾

  • 过桥必派生:参数 Deserialize、返回 Serialize,serde 派生宏编译期生成转换;
  • 三参数即结构体:成组、集中校验、rename_all 统一驼峰,limit 类字段必钳制;
  • 返回分级:列表给摘要、详情按需取,尊重序列化边界;
  • 嵌套是命名坑:外层 Tauri 转、内层 serde 转,未派生 rename_all 的嵌套保持蛇形;
  • 类型单向生成:Rust 为源生成 TS,漂移在编译期暴露。

契约定型了。下一节给芯上夹具:应用状态怎么管、并发怎么不炸、耗时任务怎么不冻界面。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U