读论文时遇到一个词,打开词札要两步:先开终端启动后端,再开浏览器输入地址。功能没毛病,但每天重复两次准备工作,就是摩擦。
我想要的很具体:双击图标,窗口出现,直接查词。业务代码一行不改,只加一层壳。
为什么不选 Electron
Electron 的好处是省事,代价是体积:它自带一整套 Chromium,基线就是上百 MB。Tauri 用的是系统自带的 WebView------macOS 上是 WKWebView,Windows 是 WebView2,Linux 是 WebKitGTK,壳层本身很小。
但词札的难点不在前端,而在后端是 Python。所以最终方案不是「WebView 套网页」,而是:
- Tauri 壳:负责窗口、托盘、快捷键、进程生命周期
- PyInstaller sidecar :把 FastAPI + SQLite + 前端产物打成一个独立后端程序,由壳层拉起

壳层只做四件事
- spawn 后端子进程
- 等后端就绪(读 ready 文件 → 健康检查)
- 把窗口导航到
http://127.0.0.1:{port}/ - 收尾:崩溃重启、退出清理、关窗隐藏
业务逻辑全部留在原来的 FastAPI 里,壳层不碰。
六个决策,每个都放弃了点什么
| # | 决策 | 放弃的方案 | 原因 |
|---|---|---|---|
| 1 | 后端用 onedir | onefile | onefile 每次启动都要解压到临时目录,冷启动明显更慢;onedir 首次约 2 秒,之后 0.5 秒内 |
| 2 | --port 0 + ready 文件 |
先探测空闲端口再传给后端 | 探测和 bind 之间有竞态,两个进程可能抢同一个端口 |
| 3 | 先显示启动页 | 等后端就绪再 show | 用户不该对着空白窗口等 |
| 4 | spawn 时清空信号屏蔽字 | 直接继承父进程 | 否则后端收不到 SIGTERM,只能等超时被强杀 |
| 5 | 单实例插件最先注册 | 靠文件锁 | 否则会出现两个托盘图标、两个后端抢同一个 SQLite |
| 6 | 启动时抢前台 | 只 show() + set_focus() |
macOS 上这两下不够,窗口会躲在别的应用后面 |
第 2 条是整个方案的地基。壳层传给后端的是:
bash
cizha-backend --host 127.0.0.1 --port 0 --ready-file /tmp/cizha-xxx.port
后端绑定成功后把真实端口写进 ready 文件,壳层轮询读到端口,再请求一次 /api/health,确认返回 200 才导航。
五个坑,都是实际踩出来的
坑 1:CI 里找不到 .spec
现象:
text
ERROR: Spec file "build/pyinstaller/cizha-backend.spec" not found!
根因是 workflow 的 step 设了 working-directory: backend,PyInstaller 于是从 backend/ 起找 spec,而 spec 在仓库根的 build/pyinstaller/。修法是把工作目录切回仓库根,或显式写 ../build/pyinstaller/...。
坑 2:用管道接后端输出会把后端卡死
一开始为了看日志,给子进程接了 Stdio::piped()。跑一会儿后端就不响应了------缓冲区写满,进程阻塞在写 stdout。改成输出重定向到日志文件,问题消失。
坑 3:已经回收的进程不能再 kill
进程退出的那一瞬间,PID 可能已经被系统复用。先 try_wait() 判断是否已回收,已回收就不再 kill。
坑 4:Gatekeeper
没签名、没公证,双击会提示「无法验证开发者」。这不是 bug,是成本选择:绕过要 Apple Developer 账号和公证流程。当前版本给用户三种放行方式(右键打开 / 系统设置放行 / xattr -cr)。
坑 5:图标改了四遍
这是最花时间、也最值得记的一段。
| 版本 | 结果 | 原因 |
|---|---|---|
| 1 | Dock 里比别的 App 小一圈 | 图标四周留白 |
| 2 | 出现白色方框 | 铺满画布后,圆角外的纸色被当成底色 |
| 3 | 四角发尖 | 自己画的圆角和系统蒙版叠加 |
| 4 | 终于对齐 | 内容缩到 85%,四周留透明,并清掉 Dock 缓存 |
结论:macOS 会给图标加系统圆角,图标内容不要自带圆角,也不要自带白边。

打包与发布
本地打包的顺序不能颠倒:
bash
# 1. 前端
cd frontend && npm run build && cd ..
# 2. 后端(onedir)
cd backend && ./venv/bin/python -m PyInstaller ../build/pyinstaller/cizha-backend.spec \
--noconfirm --workpath ../_pybuild --distpath ../dist --clean && cd ..
# 3. 把后端产物复制成 Tauri 的 sidecar 资源
python3 scripts/prepare-sidecar.py
# 4. 打 .app 和 .dmg
cd src-tauri && ~/.cargo/bin/cargo tauri build --bundles app,dmg
第 3 步最容易漏。漏了这一步,打出来的 App 里还是旧后端------build.rs 在 release 构建时会直接拦下来,算是加了一道保险。
CI 那边按平台跑矩阵,PyInstaller 负责产出 macOS / Linux / Windows 三份后端二进制,Release 页面自动挂附件;macOS 的 .dmg 由本地构建后上传。当前 .dmg 约 42 MB,安装后的 .app 约 95 MB。
边界
- 本轮只做 macOS(Apple Silicon);Windows / Linux 的安装包延后
- 签名与公证暂缓,首次打开需要手动放行
- 不做云同步:数据在
~/Library/Application Support/cizha/,卸载 App 不删 - Web 端完全不受影响,桌面化全部是增量改动
代码和 Release 都在这里:
跑不起来直接把报错贴上来,我改。