5.3 系统能力封装:文件、网络与进程


芯的价值在接口:把操作系统能力包装成安全、好用的命令。本节过三大件——文件读写、网络请求、外部进程——每件都先给"正姿势",再点破对应的"险姿势"。读完本节,你封装的每个能力都自带两样东西:路径与输入校验、明确的失败语义。

文件:标准库加路径校验

自带笔记的笔记本体是本地目录下的 Markdown 文件。封装读写的最小可用版:

use std::path::{Path, PathBuf}; fn notes_dir(app: tauri::AppHandle) -> Result<PathBuf, String> { let dir = app.path().app_data_dir().map_err(|e| e.to_string())?; Ok(dir.join("notes")) } fn ensure_within(base: &Path, target: &Path) -> Result<(), String> { if !target.starts_with(base) { return Err("路径越界".into()); // 拒绝逃出笔记目录的任何路径 } Ok(()) } #[tauri::command] fn read_note(app: tauri::AppHandle, name: String) -> Result<String, String> { let base = notes_dir(app)?; let target = base.join(&name); ensure_within(&base, &target)?; std::fs::read_to_string(&target).map_err(|e| format!("读取失败: {e}")) }

三个要点拆开看。目录从 AppHandle 取而非手拼路径——app_data_dir 返回系统约定的应用数据目录(随 identifier 走),跨平台正确且不用关心权限。ensure_within 是路径越界的闸门:前端传来的 name 若含上跳段(拼出父目录路径),starts_with 检查直接拒绝。这是命令输入校验里最重要的一条,凡是接受路径参数的命令都必须有它,第 6 章安全章会再强调。错误带上下文:把系统错误转成"读取失败加原因",前端能直接展示。

顺带说清"自写命令还是用官方 fs 插件"的分工:fs 插件(第 7 章)给前端直接的文件 API 加目录作用域,适合"界面要读写用户选的文件"这类通用需求;自写命令适合带业务语义的封装——"读取笔记"含格式解析与越界检查,是业务不是通用文件访问。两者不冲突,业务封装常常内部仍用标准库。

网络:封装超时与重试,而不是裸转发

芯侧发请求用 reqwest(Tauri 生态事实标准)。要封装的不是"发出去",而是三件容易被漏掉的事:超时、重试、错误分级:

use std::time::Duration; pub struct Http { client: reqwest::Client, } impl Http { pub fn new() -> Self { Self { client: reqwest::Client::builder() .timeout(Duration::from_secs(15)) // 总超时:慢接口不再无限等 .connect_timeout(Duration::from_secs(5)) .build() .expect("client 构建失败"), } } pub async fn get_json(&self, url: &str) -> Result<serde_json::Value, String> { let mut last_err = String::new(); for attempt in 0..3 { match self.client.get(url).send().await { Ok(resp) if resp.status().is_success() => { return resp.json().await.map_err(|e| format!("解析失败: {e}")); } Ok(resp) => return Err(format!("服务端错误: {}", resp.status())), Err(e) => { last_err = e.to_string(); tokio::time::sleep(Duration::from_millis(300 * (attempt + 1))).await; } } } Err(format!("网络不可达: {last_err}")) } }

取舍说明:只对网络层瞬态错误重试(连接失败),服务端明确报错不重试——重试语义错了会把"参数错误"放大成三次请求。超时是默认必配——桌面应用的网络请求没有浏览器页签的"用户会自己刷新"兜底,15 秒总超时是体面的上限。最后给前端留什么粒度:命令返回业务结果或"带原因的失败",不要把 reqwest 的错误类型直接序列化出去——实现细节泄漏到前端,换库即破坏契约。

进程:外部程序与侧车

"调用外部程序"有两类场景。场景一:系统命令(打开文件管理器、调 ffmpeg 转码)——优先用官方 shell 或 opener 插件的能力面,别手写进程管理。场景二:随应用分发的辅助程序(侧车,sidecar)——比如内置一个小型转码器。侧车三步走:Cargo 或 npm 配置里登记外部二进制名;打包时把对应平台二进制放进指定目录随包分发(第 8 章);运行时用 shell 插件按侧车方式拉起并读输出:

use tauri_plugin_shell::ShellExt; #[tauri::command] async fn transcode(app: tauri::AppHandle, input: String) -> Result<String, String> { let output = app.shell() .sidecar("my-encoder") .map_err(|e| e.to_string())? .args(["--input", &input, "--fast"]) .output() .await .map_err(|e| format!("启动失败: {e}"))?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).into_owned()) } else { Err(format!("转码失败: {}", String::from_utf8_lossy(&output.stderr))) } }

进程封装的红线只有一条:参数必须是结构化的(args 数组逐项传),永远不要把用户输入拼进一条 shell 字符串再执行——那是把命令注入的口子亲手开在芯里。

能力封装清单

  • 目录一律从 AppHandle 的路径 API 取,不手拼、不写死;
  • 接受路径的命令必过 ensure_within 类越界检查;
  • 网络客户端必配超时;重试只针对瞬态错误,重试带退避;
  • 错误转译成业务语言再过桥,不泄漏实现类型;
  • 外部进程用结构化参数拉起,杜绝拼接命令串;
  • 通用文件访问交给官方插件,业务封装才自写命令。

本节要点回顾

  • 文件三件套:路径 API 取目录、越界检查必上、错误带上下文;
  • 插件与命令分工:通用访问用 fs 插件,业务语义自封装;
  • 网络封装三件事:超时必配、瞬态才重试、错误转译再过桥;
  • 侧车三步:登记、随包分发、shell 插件拉起;参数结构化防注入。

三大件装上,芯的能力面齐了。下一道上质检工序:给所有失败路径一个体面的、可被前端分流处理的错误体系。


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