我最终选择了使用 Tauri 来构建我的桌面回测应用。本文记录了开发过程中需求梳理、初始化步骤、遇到的坑以及一些性能优化技巧。
需求梳理
核心功能
- 跨平台运行
- 触发 Python 脚本执行
- 与 MongoDB 后端数据库交互
- 前端展示任务管理卡片(Card)、结果表格(Table)、收益图表(Chart)
任务流程设计
- 在本地生成待回测任务列表
- 将任务列表推送到远程
- 远程启动任务
- 支持中途暂停任务执行
- 任务的执行要求是:异步限流 + 分组并发
- 这类任务需要数据库分页管理,适合使用“生产者-消费者”模型来处理。
- 在代码中循环(或监听)数据库的更新, 适应随时插入priority任务.
初始化与开发环境搭建
| 工具/依赖 | 说明 |
|---|---|
| node.js + npm包管理 | https://nodejs.org/ 下载与安装 |
| Rust工具链 | 安装命令: curl https://sh.rustup.rs -sSf | sh |
| Rust 依赖(macOS) | 安装命令: xcode-select –install |
| Tauri CLI | 命令:npm create tauri-app,可自动创建 Tauri 2 项目框架 |
| 启动项目 | 进入项目目录后运行:npm install && npm run tauri dev |
| 开启调试模式 | 编辑 tauri.conf.json 中的 "window": { "devtools": true } |
🧱 开发与常见问题记录
前端部分(Vue + Naive UI)
- 打开 DevTools:mac 快捷键为
Cmd + Option + I,不是 F12。 - 状态共享:使用
Pinia的taskStore来管理任务列表,实现多组件共享,减少无意义的读数据库, 可以理解成一种本地缓存吧。 - 错误调试:如
Unhandled Promise Rejection: TypeError: null is not an object (evaluating 'instance.subTree.component'),可类比为 Python 的 KeyError,说明代码访问了不存在的变量。 - 禁用拼写检查:在
v-input中添加spellcheck: false。 - 请求竞态问题(Race Condition):
- 输入搜索框
1 → 15会同时触发1和15的请求,后发请求(15)可能被先返回的(1)结果覆盖。 - 解决方法:前端维护请求版本号,只保留最新的请求结果。
- 输入搜索框
- 分页问题:
naive-ui的data-table不会记录当前页- 初步尝试自己管理分页 + 滚动加载,但最后放弃了这个方法😮💨, 因为不仅是翻页, 排序也被naive-ui封装在内部, 自己实现太麻烦。
- 最终选择:从渲染后的 DOM(Document object Model) 中直接读取当前页元素 ID。
- DOM(Document object Model) 是浏览器解析html之后生成的树形结构
- 最后我还是自己管理了分页, 因为不想一次性加载全部数据库. 并且把query, 排序都移到后端处理.
- css样式问题:
- 有些样式必须加
!important才能覆盖naive-ui的默认值。 - 表格显示不全时, 我希望增加行高显示完全. 但是在用ai写代码的时候, 我一直自认为这是expand. 误导了ai, 一直在给我推expand组件, 走了很多弯路😮💨 其实应该用定义css实现。
- 在调整样式时, 用调试功能
inspect element确认组件的真实名称. 不要让ai从代码猜测.
- 有些样式必须加
后端部分(Rust)
invoke的使用:位于@tauri-apps/api/core(tauri 2.0的情况),只能在 WebView 中调用,不能在npm run dev打开的浏览器端口中调试- 参数匹配问题:Rust 会自动将 小驼峰(camelCase) 转成 蛇形(snake_case),需确保参数名一致。
- 函数注册:所有后端 API 函数都需在
main.rs中注册 - 日志调试:打印完整log用于调试非常重要, 尤其调用外部脚本时, 统一调用函数, 把log打印写在里面.
- 用async函数和tokio 库设置stdout, stderr实时刷新
- 异步数据库访问:使用
futures_util::stream::TryStreamExt提供的try_next,需手动引入 trait。 - 共享状态管理(State):
- 使用
tauri::State来注册共享数据,如数据库客户端、全局监听状态, 脚本环境和参数等。 - Rust 的所有多线程共享必须显式声明同步策略。
- 使用
OnceCell实现全局状态的懒加载,避免重复初始化。
- 使用
数据库部分(MongoDB)
- 监听任务变化:使用
ChangeStreamOptions监听数据库变更,需开启replica set(副本集)模式。- 打开本地数据库命令:
mongod --config /usr/local/mongodb/mongod.conf --replSet rs0 --fork - 用全局的监听, 否则每次app运行时间一长, 加载就变慢.
- 打开本地数据库命令:
- 任务删除后自动停止监听。
- 使用异步库 Motor:
- 替代传统的 PyMongo(伪异步),性能提升明显。
- 同步数据问题:
- 使用 BSON 快速传输数据时,会产生10倍以上的瞬时占用,超过 MongoDB Atlas免费 500MB 限额。
- 解决方法:开启
mongorestore的numInsertionWorkersPerCollection限制。
- 增量拉取时防止重复数据。
- MongoDB API 使用:
- 在 MongoDB Atlas 中创建 Service Account 与 API Key: 在 IDENTITY&ACCESS-> Applications 里创建
- Projectid是网址 可从浏览器 URL 中获取。
- 查询优化:
- 创建索引加速查询。
- 查询时排除大字段字段,避免无谓传输。
脚本触发与执行
- JSON 参数转义地狱,最终选择使用temp file进行参数传递。
- 所有脚本路径、参数与环境变量统一写入
config.toml进行集中管理。 - 使用事件循环运行带异常捕获的主逻辑(
asyncio.get_event_loop()和traceback.format_exc()), 输出定位到错误代码级别的log.
release部分
- 把配置的逻辑全部从代码中抽出, 写在配置文件里. 用设置界面显示和修改配置文件
- build时给用户生成一个resource, 把配置文件放在里面, 让用户可以不通过源码更改配置
Mongodb数据库总结
回测过程(相比csv)
- 存储具备压缩特性,节省大量空间。
- 使用异步 API 调用,避免频繁本地 I/O。
- 支持实时监听任务进度。
结果查看
- 支持灵活的条件查询。
- 前后端分离,API 接口易于调用。
- 缺点是必须配合前端,无法像 CSV 文件直接系统自带app打开处理。