宇宙纪元

hinder110 的思考、读书与代码札记。

0%

从 Web Demo 到 Tauri v2 桌面应用:一个小说阅读器的重生

从 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/capabilities

# 写配置
package.json # Tauri v2 + React 依赖
vite.config.ts # Tauri 兼容构建
Cargo.toml # Rust 依赖
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()

1
2
// 旧:fetch('/api/search?keyword=xxx')
// 新:invoke<SearchResult[]>('search_books', { keyword })

类型对齐:camelCase → snake_case(匹配 Rust 序列化)

1
2
// 旧:Book.bookId, Chapter.itemId
// 新:Book.book_id, Chapter.item_id

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()?; // MutexGuard 持有锁
generic_parser::search(&sources).await // .await 时锁未释放,不满足 Send
}

原因std::sync::MutexGuard 不是 Send。tokio 可能在 .await 时切换线程,锁不能跨线程传递。

修复:先 clone 数据,再 drop 锁,再 await。

1
2
3
4
5
let sources = {
let guard = state.sources.lock()?;
guard.clone() // 复制数据
}; // guard 在此 drop,锁释放
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 会先执行 beforeBuildCommandnpm 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 的书源分两类:

  1. CSS 规则源(简单):搜索URL + CSS选择器 → 直接请求 → 解析 HTML
  2. JS 动态源(复杂):内嵌 JavaScript → 计算签名/MD5/AES → 构造请求 → 解密响应

第一种我们的规则引擎能处理。第二种需要 QuickJS 在 Rust 里执行 JavaScript。7208 个源里大部分是 JS 动态源,解析出来规则字段为空。

状态:搁置。后续可接 quickjs-rs 支持。

教训:调研数据格式再设计解析器。不要想当然。

坑 7:端口占用导致启动失败

现象npm run tauri devPort 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

原因链

  1. 第一次用 cargo build --release 编译 → 没嵌入前端
  2. 手动 gh release create 上传了这个残缺 exe
  3. 用户下载后双击 → 连 localhost:3000 → 没人监听 → 报错

修复链

  1. npm run tauri build 重新编译 → 前端嵌入 exe
  2. 删除旧 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 辅助开发的建议

  1. 先画架构图再写代码:AI 能写代码,但架构决策要人来做
  2. 每次只改一个模块:别同时改 5 个文件,出错没法定位
  3. 编译频繁验证cargo check(5 秒)比 cargo build(5 分钟)快 60 倍
  4. Mock 数据保底:外部依赖不可靠,用 Mock 保证核心流程可测
  5. 错误信息仔细读:Rust 的编译错误信息是教科书级别的,认真看就能解决
  6. 先跑通再优化:Mock 源先验证全流程,再折腾网络书源