从 Web Demo 到 Tauri v2 桌面应用:一个小说阅读器的重生 起点 项目最初是一个 React + Express 的 Web Demo,前端 3 个页面(搜索/目录/阅读),后端代理光遇 API(番茄小说聚合接口)。能跑,但只是一个玩具——没有持久化、没有真正的书源解析、依赖第三方 API。
目标:把它变成真正的桌面应用。Tauri v2(Rust 后端 + React 前端),支持 7208 个书源、SQLite 书架/历史、规则引擎解析任意网站。
开发流程 阶段 1:架构设计(30 分钟) 先画出完整模块图,再写代码。核心模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ┌─ React 前端 ──────────────────────────────────┐ │ SearchPage ChaptersPage ReaderPage │ │ BookshelfPage HistoryPage SettingsPage │ │ api.ts (invoke) ←──IPC──→ Rust 后端 │ └────────────────────────────────────────────────┘ ┌─ Rust 后端 (src-tauri/) ───────────────────────┐ │ lib.rs → 10 个 IPC 命令 + AppState │ │ source_manager → 加载/解析 JSON 书源 │ │ rule_engine → CSS 选择器链 + 正则提取 │ │ generic_parser → HTTP 请求 + 多源 fallback │ │ mock_source → 离线测试源 │ │ db.rs → SQLite 书架/历史 │ └────────────────────────────────────────────────┘
阶段 2:脚手架搭建(15 分钟) 1 2 3 4 5 6 7 8 mkdir src/pages src-tauri/src src-tauri/icons src-tauri/capabilitiespackage.json vite.config.ts Cargo.toml tauri.conf.json
阶段 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
用 scraper crate 解析 HTML,regex 做后处理
URL 规范化:相对路径 → 绝对路径
generic_parser.rs — HTTP 解析器
优先源(前 3 个)5 秒超时,结果不够 5 本则启动扩展源(6 秒)
GBK/UTF-8 自动检测解码
结果按 (书名, 作者) 去重
db.rs — SQLite 存储
bookshelf 表:书架书籍 CRUD
history 表:阅读历史,LEFT JOIN 书架获取书名
mock_source.rs — 内置测试源
搜索返回 3 本假书
目录生成 85/120/200 章
正文生成 16 段中文阅读内容
作用:离线验证全流程
阶段 4:前端适配(1 小时) 核心改变:fetch() → invoke()
类型对齐:camelCase → snake_case(匹配 Rust 序列化)
HashRouter:Tauri 生产环境用自定义协议,BrowserRouter 失效。
阶段 5:测试与修复(1 小时)
Mock 源验证全流程:搜索 → 目录 → 阅读 → 翻页
CSS 书源验证:HTTP 请求 → 解析 → 返回
网络错误排查:TLS backend 缺失 → 添加 native-tls
我踩过的坑 坑 1:MutexGuard 不能跨 .await 错误 :future cannot be sent between threads safely
1 2 3 4 5 6 #[tauri::command] async fn search_books (state: State<'_ , AppState>) -> Result <..., String > { let sources = state.sources.lock ()?; generic_parser::search (&sources).await }
原因 :std::sync::MutexGuard 不是 Send。tokio 可能在 .await 时切换线程,锁不能跨线程传递。
修复 :先 clone 数据,再 drop 锁,再 await。
1 2 3 4 5 let sources = { let guard = state.sources.lock ()?; guard.clone () }; generic_parser::search (&sources).await
教训 :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 2 3 4 5 let db_path = std::path::Path::new (env! ("CARGO_MANIFEST_DIR" )) .parent () .unwrap () .join ("yueduqi.db" ); Connection::open (&db_path)
教训 :文件监控工具和数据库文件是死对头。数据库文件永远不要放在被监控的目录里。
坑 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 源先验证全流程,再折腾网络书源