Makepad 应用如何读文件、调接口、保存数据

前言

上一期做完 JSON 查看器之后,我自己用了几天。

第三天我就烦了。每次打开应用,都要重新点"打开文件"选同一个 JSON。关闭再打开,上次展开的节点全收了。窗口位置也不记得,永远出现在屏幕左上角。

我意识到一件事:一个能跑的桌面应用和一个能用的桌面应用,中间差的就是这些"本地能力"。

文件系统、配置存储、HTTP 请求、异步任务------这些东西在 Web 开发里是标配,在 Makepad 里每一件都要自己想办法。这一期我把每种能力的做法和限制都写清楚,不讲虚的。

1. 文件读写:最基础也最容易被忽视

1.1 打开文件对话框

第七期 JSON 查看器里已经用过了,但只写了最简版本。完整一点的用法是这样的:

rust 复制代码
// UI 描述里先声明 file_dialog
file_dialog := FileDialog {
    // 可以预设过滤器
    // filter: "JSON Files (*.json)|*.json"
}

#[event]
fn handle_open_file(&mut self, cx: &mut Cx, actions: &Actions) {
    if self.ui.button(cx, ids!(open_btn)).clicked(actions) {
        self.ui.file_dialog(cx, ids!(file_dialog)).open(cx);
    }
}

#[event]
fn handle_file_selected(&mut self, cx: &mut Cx, actions: &Actions) {
    if self.ui.file_dialog(cx, ids!(file_dialog)).selected(actions) {
        let path = self.ui.file_dialog(cx, ids!(file_dialog)).selected_path(cx);
        if let Some(path) = path {
            self.current_file_path = Some(path.clone());
            self.load_file(cx, &path);
        }
    }
}

有几个细节值得注意。第一,FileDialog 是异步的------你调用 open 之后不是立刻拿到结果,而是在后续帧里通过 selected 事件拿到路径。第二,文件过滤器在不同平台表现不一致,依赖 Makepad 的平台层实现,别太依赖。

1.2 读文件内容

读文件本身是标准 Rust,不需要 Makepad 参与:

rust 复制代码
fn load_file(&mut self, cx: &mut Cx, path: &str) {
    match std::fs::read_to_string(path) {
        Ok(content) => {
            self.file_content = content;
            self.status_message = format!("已加载: {}", path);
        }
        Err(e) => {
            self.status_message = format!("读取失败: {}", e);
        }
    }
    self.ui.redraw(cx);
}

但这里有一个坑。std::fs::read_to_string 是同步的,在 Makepad 的主事件循环里直接调用,大文件会卡界面。第七期 JSON 查看器里我处理 2MB 文件时踩过这个坑,窗口直接卡了三秒。

后面的章节会讲怎么解决这个问题。

1.3 写文件

写文件同样简单,Rust 标准库就够了:

rust 复制代码
fn save_file(&mut self, cx: &mut Cx, path: &str) {
    match std::fs::write(path, &self.editor_content) {
        Ok(_) => {
            self.status_message = "已保存".into();
            self.is_modified = false;
        }
        Err(e) => {
            self.status_message = format!("保存失败: {}", e);
        }
    }
    self.ui.redraw(cx);
}

写文件通常比读文件快很多(除非你在写几个 G),所以在主线程直接写问题不大。但如果你要写大文件,考虑分块写入或者在帧间拆分。目前 Makepad 没有内置方案,用标准库的 BufWriter 分块写是临时解。

2. 配置持久化:让应用有"记忆"

2.1 最简单的方案:JSON 配置文件

一个桌面应用至少应该记住:

  • 窗口位置和大小
  • 最近打开的文件列表
  • 用户偏好(主题、字体大小、默认目录等)

最简单的做法是把配置序列化为 JSON,存在应用数据目录里:

rust 复制代码
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Default)]
struct AppConfig {
    window_x: i32,
    window_y: i32,
    window_width: i32,
    window_height: i32,
    recent_files: Vec<String>,
    theme: String,
    font_size: f32,
}

impl AppConfig {
    fn config_path() -> Option<std::path::PathBuf> {
        dirs::config_dir().map(|p| p.join("my-app").join("config.json"))
    }

    fn load() -> Self {
        if let Some(path) = Self::config_path() {
            if let Ok(content) = std::fs::read_to_string(&path) {
                if let Ok(config) = serde_json::from_str(&content) {
                    return config;
                }
            }
        }
        Self::default()
    }

    fn save(&self) {
        if let Some(path) = Self::config_path() {
            if let Some(parent) = path.parent() {
                let _ = std::fs::create_dir_all(parent);
            }
            if let Ok(json) = serde_json::to_string_pretty(self) {
                let _ = std::fs::write(&path, json);
            }
        }
    }
}

几个注意点:

  • dirs::config_dir() 获取系统标准的配置目录,别自己硬编码路径
  • 首次启动时目录可能不存在,记得 create_dir_all
  • 反序列化失败时不要崩溃------返回默认值,应用照常启动

2.2 什么时候写配置

配置不需要用户点"保存按钮"才写。有几个自然的触发点:

应用关闭时:

rust 复制代码
#[event]
fn handle_close(&mut self, cx: &mut Cx, actions: &Actions) {
    if self.ui.window(cx, ids!(main_window)).close_requested(actions) {
        self.config.save();
    }
}

窗口移动/调整大小时: 这个比较高频,建议做防抖。在 #[event] 里检查窗口位置,变化超过一定阈值再写,或者定时批量写。

最近文件列表更新时: 每次成功打开新文件就追加到列表并保存。

rust 复制代码
fn add_recent_file(&mut self, path: &str) {
    self.config.recent_files.retain(|f| f != path);
    self.config.recent_files.insert(0, path.to_string());
    self.config.recent_files.truncate(10); // 最多保留 10 个
    self.config.save();
}

2.3 配置的默认值和迁移

一个很容易漏的事:配置结构将来会变。你今天存了 5 个字段,下个版本加了 3 个。

最简单的防御:所有字段用 #[serde(default)],新增字段不会导致旧配置文件加载失败。

rust 复制代码
#[derive(Serialize, Deserialize)]
struct AppConfig {
    window_x: i32,
    window_y: i32,
    #[serde(default = "default_width")]
    window_width: i32,
    #[serde(default)]
    recent_files: Vec<String>,
    #[serde(default)]
    theme: String,
    #[serde(default = "default_font_size")]
    font_size: f32,
}

3. HTTP 请求:怎么接 API

桌面应用接 API 的场景比你想象的多------检查更新、同步数据、获取在线资源。Makepad 本身没有 HTTP 客户端,但 Rust 生态有成熟的方案。

3.1 选 reqwest,别自己造轮子

toml 复制代码
[dependencies]
reqwest = { version = "0.12", features = ["blocking"] }

blocking 特征是因为 Makepad 没有内置的 async runtime。后面会讲怎么处理阻塞调用。

一个最简单的 GET 请求:

rust 复制代码
fn fetch_data(&mut self, url: &str) {
    match reqwest::blocking::get(url) {
        Ok(response) => {
            if let Ok(body) = response.text() {
                self.api_result = body;
                self.status_message = "数据获取成功".into();
            }
        }
        Err(e) => {
            self.status_message = format!("请求失败: {}", e);
        }
    }
    self.ui.redraw(cx);
}

3.2 不要在主线程做网络请求

这是最重要的警告。reqwest::blocking::get 在主线程调用会阻塞整个 UI,直到请求返回。网络慢的时候,你的应用看起来像卡死了。

解决思路和第七期处理大文件解析一样:把耗时操作拆到多帧执行。

但目前 Makepad 没有完整的异步支持。下面几种临时方案,选哪个看你的场景。

方案一:小请求直接在主线程做。

如果你的 API 响应很快(比如本地服务、内网接口),几百毫秒内就返回,用户可能不会注意到。但别在生产环境这么做,只适合开发调试。

方案二:用标准库的线程。

rust 复制代码
use std::thread;

fn fetch_async(&mut self, url: String) {
    self.is_loading = true;
    let url_clone = url.clone();
    
    thread::spawn(move || {
        match reqwest::blocking::get(&url_clone) {
            Ok(response) => {
                if let Ok(body) = response.text() {
                    // 问题:怎么把结果传回主线程?
                    // 用 std::sync::mpsc::channel 或 Arc<Mutex<...>>
                }
            }
            Err(_) => {}
        }
    });
}

thread::spawn 能让请求不阻塞 UI,但结果传回主线程需要额外的同步机制。最简单的做法是用 std::sync::mpsc::channel

rust 复制代码
script_mod!(
    mod my_app {
        // 线程间通信
        pending_result: Option<String>,

        fn check_thread_result(&mut self) {
            if let Ok(result) = self.result_receiver.try_recv() {
                self.pending_result = Some(result);
                self.is_loading = false;
            }
        }
    }
);

然后在每一帧的 #[event] 函数里调用 check_thread_result,或者在专门的 update_display handler 里处理。

这个方案能跑,但写起来不太舒服。线程生命周期管理、panic 处理、请求取消都是你手动管。

方案三(推荐):拆分任务,用帧间状态机。

如果你的 API 调用不是高频操作(比如启动时检查更新、用户手动点"同步"按钮),可以不用线程。把请求拆成多帧执行的小步骤:

复制代码
帧 1:发起请求前的准备工作
帧 2:执行请求(用户看到加载状态)
帧 3:处理响应,更新 UI

本质上就是把一个长任务切成多个 #[event] 函数的调用。算不上真正的异步,但至少不会卡 UI。

这个方向 Makepad 团队也在做。官方正在推进内置的异步支持,等它成熟了,这些临时方案就可以扔了。

4. 文件系统监控(可选但好用)

JSON 查看器里还有一个我后来加的功能:如果打开的文件被外部程序修改了,自动提示重新加载。

实现思路:存下文件的 modified 时间戳,定时检查是否变化。Rust 标准库就够:

rust 复制代码
use std::fs;

fn get_modified_time(path: &str) -> Option<std::time::SystemTime> {
    fs::metadata(path).ok()?.modified().ok()
}

fn check_file_changed(&mut self) {
    if let Some(path) = &self.current_file_path {
        let current_mtime = get_modified_time(path);
        if current_mtime != self.last_modified {
            self.last_modified = current_mtime;
            self.status_message = "⚠ 文件已被外部修改,请重新加载".into();
        }
    }
}

update_display handler 里每隔一定帧数调用一次 check_file_changed。不用太频繁,每 30 帧检查一次就够了。

5. 把这些能力串起来:一个完整的启动和关闭流程

下面是第七期 JSON 查看器加上本章所有本地能力后的完整流程:

应用启动时:

markdown 复制代码
1. 加载配置文件 (AppConfig::load())
2. 恢复窗口位置和大小
3. 检查最近文件列表,如果上次有打开的文件,自动加载
4. 恢复上次展开的节点状态(记住展开状态也可以存进配置)

应用运行时:

diff 复制代码
- 用户打开文件 → 先读文件 → 解析 → 更新最近文件列表 → 保存配置
- 用户调整窗口 → 防抖后保存窗口位置到配置
- 用户展开/折叠节点 → 更新展开状态集合,定期写入配置
- 检查文件是否被外部修改 → 提示重新加载
- (可选)启动时检查更新 → HTTP 请求 + 线程

应用关闭时:

markdown 复制代码
1. 保存配置 (config.save())
2. 如果文件有未保存的修改,弹出提示
3. 清理临时文件

5.1 应用启动时自动加载上次文件

rust 复制代码
#[run]
fn main(&mut self, cx: &mut Cx) -> Result<(), String> {
    self.config = AppConfig::load();

    // 恢复窗口位置
    if self.config.window_width > 0 {
        // 设置窗口位置和大小...
    }

    // 自动打开上次的文件
    if let Some(last_file) = self.config.recent_files.first() {
        let path = last_file.clone();
        self.load_file(cx, &path);
    }

    Ok(())
}

5.2 退出前保存

rust 复制代码
#[event]
fn handle_events(&mut self, cx: &mut Cx, actions: &Actions) {
    if self.ui.window(cx, ids!(main_window)).close_requested(actions) {
        // 保存展开状态到配置
        self.config.expanded_nodes = self.expanded_nodes.iter().cloned().collect();
        self.config.save();
    }
}

6. Makepad 中 Rust 标准库的可用性

一个常被问到的问题:Makepad 应用里能用所有 Rust crate 吗?

答案是:大部分能,但有几个要注意的。

能用的:

  • std::fs --- 文件读写,完全可用
  • std::net --- 网络,但要开线程
  • serde / serde_json --- 序列化,完全可用
  • dirs --- 获取系统标准目录,跨平台可用
  • chrono / time --- 时间处理,完全可用

需要小心的:

  • reqwest --- 使用 blocking 模式,注意线程安全
  • tokio --- Makepad 没有内置 tokio runtime,如果你非要用,需要自己起 runtime
  • rusqlite --- SQLite 绑定,可以用但注意文件锁和线程

基本不能用的:

  • 依赖系统 GUI 组件的库(比如某些弹窗、系统托盘库)
  • 和 Makepad 自己的平台抽象冲突的库

一个很实用的经验:如果你不确定一个 crate 能不能在 Makepad 里用,看它有没有"同步 API"版本。 有同步 API 的几乎都能直接用,纯异步的需要额外处理。

7. 当前阶段的权衡

写到这里,必须坦诚一件事。

Makepad 在本地能力接入上,目前更像是一个"你要自己来"的框架,而不是一个"开箱即用"的平台。跟 Tauri 比,Tauri 的文件对话框、系统通知、自动更新都有现成插件。Makepad 里每件小事都要你自己写。

但这不全是坏事。因为你自己写的代码没有中间层,出了问题你能看到全部调用链。用 Tauri 插件遇到 bug 的时候,你不是在修自己的代码,是在翻插件源码。

现阶段我的建议:把本地能力当基础设施自己搭。 写一遍 load_configsave_configadd_recent_file,后面所有项目都能复用。花的时间是一次性的。

总结

这一期解决的就是"桌面应用不只是个壳子"的问题。

六个能力点:

  1. 文件读写 --- std::fs,大文件注意别卡主线程
  2. 配置持久化 --- JSON + dirs::config_dir(),默认值用 #[serde(default)]
  3. 最近文件列表 --- 打开文件时追加,启动时恢复
  4. HTTP 请求 --- reqwest::blocking,耗时调用拆到线程里
  5. 文件变更检测 --- 比较 modified 时间戳
  6. 应用生命周期 --- 启动加载配置 → 运行按需保存 → 退出持久化

每件事单看都不复杂。但全部接上之后,你的应用从"能演示"变成了"能日常用"。这一步的价值,比多写一个炫酷动画大多了。

下一期聊工程化实践:模块划分、错误处理、日志和调试、代码可维护性。从 demo 作者视角升级到工程作者视角。

相关推荐
阿正的梦工坊1 小时前
【Rust】05-结构体、枚举与模式匹配
java·数据库·rust
阿正的梦工坊1 小时前
【Rust】10-Cargo、测试与实用开发工作流
java·rust·log4j
qq_466302451 小时前
office 2021 下载安装激活
前端
新新学长搞科研1 小时前
【广东省博促会主办】2026年第七届先进材料与智能制造国际学术会议(ICAMIM 2026)
大数据·前端·数据库·人工智能·物联网
铁皮饭盒1 小时前
用bunjs代码讲解XSS/CSRF/SQL注入/DDos等10种前后端安全防护
前端·后端
琹箐1 小时前
chrome 插件下载安装;Manifest file is missing or unreadable
前端·chrome
云飞云共享云桌面1 小时前
面向机械研发:双服务器架构搭配云飞云运行 SolidWorks 方案详解
运维·服务器·前端·网络·架构·制造
乐兮创想 小林2 小时前
B2B 内容营销的工程化运营:从内容矩阵建模到 SEO/GEO 联动的完整体系
前端·线性代数·矩阵·网站建设·北京网站建设公司
2501_940041742 小时前
全栈开发提速指南:可以直接用的项目生成提示词
前端·prompt