把一个 FastAPI 网页塞进 macOS 的 Dock:Tauri 2 + PyInstaller 实战

读论文时遇到一个词,打开词札要两步:先开终端启动后端,再开浏览器输入地址。功能没毛病,但每天重复两次准备工作,就是摩擦。

我想要的很具体:双击图标,窗口出现,直接查词。业务代码一行不改,只加一层壳。


为什么不选 Electron

Electron 的好处是省事,代价是体积:它自带一整套 Chromium,基线就是上百 MB。Tauri 用的是系统自带的 WebView------macOS 上是 WKWebView,Windows 是 WebView2,Linux 是 WebKitGTK,壳层本身很小。

但词札的难点不在前端,而在后端是 Python。所以最终方案不是「WebView 套网页」,而是:

  • Tauri 壳:负责窗口、托盘、快捷键、进程生命周期
  • PyInstaller sidecar :把 FastAPI + SQLite + 前端产物打成一个独立后端程序,由壳层拉起

壳层只做四件事

  1. spawn 后端子进程
  2. 等后端就绪(读 ready 文件 → 健康检查)
  3. 把窗口导航到 http://127.0.0.1:{port}/
  4. 收尾:崩溃重启、退出清理、关窗隐藏

业务逻辑全部留在原来的 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 都在这里:

跑不起来直接把报错贴上来,我改。

相关推荐
像风一样自由20204 小时前
3.Dockerfile如何构建一个Python算法镜像
开发语言·人工智能·python·大模型
Dmz丶4 小时前
前端转 AI 100 天 Day 36:Prompt 工程入门——从“感觉对了就行”到方法论
python
日日大王学编程5 小时前
AI 时代后端方向新人的未来规划与看法
人工智能·后端·python·ai·golang
CCblandy5 小时前
缓存淘汰实测:一次全表扫描把 LRU 命中率从 46% 打到 38%,W-TinyLFU 守住 50% 却贵 11 倍
后端·python·缓存·性能优化·架构
汤米粥5 小时前
cmd手动创建的Django项目怎么用社区版Pycharm运行起来
python·pycharm·django
涵玥5 小时前
python题目练习12-小玉家的电费//小玉在游泳//小鱼的航程
python·python每日一练·洛谷题目
计算机毕业编程指导师5 小时前
基于深度学习的花卉图像分类识别系统(PyTorch + ResNet50 + Django)
pytorch·python·深度学习·分类·毕业设计·迁移学习·图像分类
2601_962885726 小时前
vnpy 用什么数据源?A 股历史行情接入与 BarData 映射
python
日日大王学编程6 小时前
deerflow-memory-design-blog
python·langchain·开源