从 Web Demo 到 Tauri v2 桌面应用:一个小说阅读器的重生
起点
项目最初是一个 React + Express 的 Web Demo,前端 3 个页面(搜索/目录/阅读),后端代理光遇 API(番茄小说聚合接口)。能跑,但只是一个玩具——没有持久化、没有真正的书源解析、依赖第三方 API。
目标:把它变成真正的桌面应用。Tauri v2(Rust 后端 + React 前端),支持 7208 个书源、SQLite 书架/历史、规则引擎解析任意网站。
开发流程
阶段 1:架构设计(30 分钟)
先画出完整模块图,再写代码。核心模块:
1 | ┌─ React 前端 ──────────────────────────────────┐ |
阶段 2:脚手架搭建(15 分钟)
1 | # 手动创建目录结构(没用脚手架,完全掌控) |
阶段 3:Rust 后端(核心开发,2 小时)
source_manager.rs — 书源加载器
- 从
test_sources.json加载 CSS 规则源(简单格式,直接映射) - 从
shuyuan_7208.json加载 Legado 格式源(复杂,需解析嵌套 JSON 规则) - 输出统一的
ParsedSource结构
rule_engine.rs — CSS 链规则引擎
- 规则格式:
CSS选择器@text|href|html|src - 用
scrapercrate 解析 HTML,regex做后处理 - URL 规范化:相对路径 → 绝对路径
generic_parser.rs — HTTP 解析器
- 优先源(前 3 个)5 秒超时,结果不够 5 本则启动扩展源(6 秒)
- GBK/UTF-8 自动检测解码
- 结果按 (书名, 作者) 去重
db.rs — SQLite 存储
bookshelf表:书架书籍 CRUDhistory表:阅读历史,LEFT JOIN 书架获取书名
mock_source.rs — 内置测试源
- 搜索返回 3 本假书
- 目录生成 85/120/200 章
- 正文生成 16 段中文阅读内容
- 作用:离线验证全流程
阶段 4:前端适配(1 小时)
核心改变:fetch() → invoke()
1 | // 旧:fetch('/api/search?keyword=xxx') |
类型对齐:camelCase → snake_case(匹配 Rust 序列化)
1 | // 旧:Book.bookId, Chapter.itemId |
HashRouter:Tauri 生产环境用自定义协议,BrowserRouter 失效。
阶段 5:测试与修复(1 小时)
- Mock 源验证全流程:搜索 → 目录 → 阅读 → 翻页
- CSS 书源验证:HTTP 请求 → 解析 → 返回
- 网络错误排查:TLS backend 缺失 → 添加
native-tls
我踩过的坑
坑 1:MutexGuard 不能跨 .await
错误:future cannot be sent between threads safely
1 | // 错误写法 |
原因:std::sync::MutexGuard 不是 Send。tokio 可能在 .await 时切换线程,锁不能跨线程传递。
修复:先 clone 数据,再 drop 锁,再 await。
1 | let sources = { |
教训:Rust async 里,锁的生命周期不能跨越 .await 点。这是 Rust 初学者最容易踩的 async 坑。
坑 2:reqwest 缺 TLS backend
错误:所有 HTTPS 请求返回 error sending request for url
原因:Cargo.toml 里只写了
1 | reqwest = { version = "0.12", features = ["charset", "gzip", "brotli"] } |
没指定 TLS backend。reqwest 不会自动选 TLS——需要显式启用。
修复:加 native-tls。
1 | reqwest = { version = "0.12", features = ["charset", "gzip", "brotli", "native-tls"] } |
教训:Rust 生态不像 Node.js 那样电池自带。TLS、编码、压缩都要显式声明 feature。
坑 3:SQLite db 文件导致 Tauri 无限重编译
现象:npm run tauri dev 启动后,应用不断重启。
原因:SQLite 数据库文件 yueduqi.db 创建在 src-tauri/ 目录内。Tauri dev 模式监控 src-tauri/ 的文件变化。每次 SQLite 写入(WAL 日志、shm 共享内存)触发文件变更事件 → Tauri 重编译 → 应用重启 → SQLite 再次写入 → 死循环。
修复:把 db 文件移到项目根目录(不在 src-tauri/ 监控范围内):
1 | let db_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) |
教训:文件监控工具和数据库文件是死对头。数据库文件永远不要放在被监控的目录里。
坑 4:cargo build vs tauri build
现象:Release exe 双击运行,窗口显示 ERR_NETWORK_CHANGED。
原因:cargo build --release 只编译 Rust 二进制。不会把 React 前端打包进去。Tauri 启动时找不到前端文件,就尝试连接 localhost:3000(开发服务器),连不上就报错。
正确做法:npm run tauri build 会先执行 beforeBuildCommand(npm run build 生成 dist/),再把 dist/ 嵌入 Rust 二进制。这就是”Tauri 打包”和”Rust 编译”的区别。
教训:cargo build ≠ 可分发版本。必须用 tauri build 才能得到独立的 exe。
坑 5:CSS 选择器线上不匹配
现象:HTML 请求成功(200),但返回 0 本。
原因:书源 JSON 里的 CSS 选择器是写死的。网站改版、换模板、加 CDN 都会导致选择器失效。离线写好的选择器到线上可能完全对不上。
实际案例:猜到 4 个站点的搜索页 CSS 结构,只有 1 个 HTTP 通了,0 个 CSS 匹配到元素。
缓解:加了 Mock 内置源,保证离线测试全流程可跑。网络源的选择器需要持续维护。
教训:依赖外部网站 HTML 结构的爬虫,选择器是消耗品。要么持续维护,要么用 API。
坑 6:Legado 书源 ≠ 简单 CSS 规则
现象:7208 个书源只解析出几个能用。
原因:Legado 的书源分两类:
- CSS 规则源(简单):
搜索URL + CSS选择器→ 直接请求 → 解析 HTML - JS 动态源(复杂):
内嵌 JavaScript → 计算签名/MD5/AES → 构造请求 → 解密响应
第一种我们的规则引擎能处理。第二种需要 QuickJS 在 Rust 里执行 JavaScript。7208 个源里大部分是 JS 动态源,解析出来规则字段为空。
状态:搁置。后续可接 quickjs-rs 支持。
教训:调研数据格式再设计解析器。不要想当然。
坑 7:端口占用导致启动失败
现象:npm run tauri dev 报 Port 3000 is already in use。
原因:Vite dev server 上次没正常退出,进程残留占用 3000 端口。
修复:
1 | Get-NetTCPConnection -LocalPort 3000 | % { Stop-Process -Id $_.OwningProcess -Force } |
教训:开发脚本应该加进程清理逻辑。
坑 8:GitHub Release exe 不能直接用
现象:Release 页下载的 exe 双击报 ERR_NETWORK_CHANGED。
原因链:
- 第一次用
cargo build --release编译 → 没嵌入前端 - 手动
gh release create上传了这个残缺 exe - 用户下载后双击 → 连 localhost:3000 → 没人监听 → 报错
修复链:
- 用
npm run tauri build重新编译 → 前端嵌入 exe - 删除旧 Release,重建 → 上传正确 exe
教训:Release 前必须在真机上双击验证 exe 能启动。
技术栈总结
| 层 | 技术 | 选型理由 |
|---|---|---|
| 桌面框架 | Tauri v2 | 比 Electron 小 20 倍,Rust 后端 |
| 前端 | React 19 + TypeScript + Vite | 生态成熟,组件化 |
| 后端 | Rust (tokio, reqwest, scraper, rusqlite) | 性能好,类型安全 |
| 规则引擎 | CSS 选择器链 | Legado 兼容,简单可扩展 |
| 存储 | SQLite | 零配置,嵌入式 |
| 打包 | Tauri build → exe | 单文件分发,7.8MB |
当前状态
- 前端 6 页面,功能完整
- Rust 后端 6 模块,编译通过,测试 3/3
- SQLite 书架/历史正常
- Mock 源可离线验证全流程
- 网络书源取决于站点可访问性
- JS 动态书源(QuickJS)未实现
- 仅 Windows x64
给 AI 辅助开发的建议
- 先画架构图再写代码:AI 能写代码,但架构决策要人来做
- 每次只改一个模块:别同时改 5 个文件,出错没法定位
- 编译频繁验证:
cargo check(5 秒)比cargo build(5 分钟)快 60 倍 - Mock 数据保底:外部依赖不可靠,用 Mock 保证核心流程可测
- 错误信息仔细读:Rust 的编译错误信息是教科书级别的,认真看就能解决
- 先跑通再优化:Mock 源先验证全流程,再折腾网络书源