ReadViewSDK/Doc/FeatureSolution/书签能力方案讨论.md
2026-05-21 19:40:51 +08:00

11 KiB
Raw Blame History

书签能力方案讨论

需求背景

Doc/EPUB_MAINTENANCE.md 已将“书签能力”列为 EPUB 阅读器下一阶段的高优先级能力之一。

当前阅读器已经具备:

  1. EPUB 打开与解析
  2. WebView / DTCoreText / fixed-layout 三条正文渲染路径
  3. 目录跳转、阅读位置恢复、主题与字号调整
  4. 高亮、划线、批注、搜索等阅读交互能力

但现阶段“位置记录”仍主要依赖:

  1. 自动保存的上次阅读位置
  2. 需要选中文本的高亮 / 批注能力

这会遗漏一种非常常见、也非常轻量的阅读诉求:

  1. 只想标记当前位置
  2. 稍后回来继续读
  3. 不需要摘录正文
  4. 不需要输入批注

因此,“书签能力”这项需求的目标,不是扩展现有标注功能,而是补上一条面向“位置收藏”的轻量链路,用来覆盖“只记录位置、不做摘录”的常见阅读需求。

代码事实

已有基础

当前仓库其实已经具备不少可直接复用的基础设施:

  1. RDEPUBLocation 已能稳定表达 href + progression + fragment
  2. RDEPUBReaderController 已具备“当前位置读取”和“跳转到位置”的主链路
  3. RDEPUBReaderPersistence 已形成按 bookIdentifier 分书持久化的模式
  4. 高亮管理页已经具备“列表展示 -> 点击跳转 -> 删除”的成熟交互范式

结论:

  1. 书签不需要重新设计位置模型
  2. 书签可以复用现有定位和跳转能力
  3. 更需要新增的是独立模型、持久化存储位和 UI 入口

当前实现存在的问题

问题 1当前只有单一阅读位置没有“多书签”能力

RDEPUBReaderPersistence 当前只提供:

  1. loadLocation / saveLocation
  2. loadHighlights / saveHighlights
  3. loadReaderSettings / saveReaderSettings

这意味着:

  1. 系统只能保存“上次读到哪”
  2. 不能保存“用户主动收藏的多个位置”

而这两者的语义并不相同:

  1. 阅读位置是自动覆盖的
  2. 书签是主动创建、可长期保留多个的

问题 2当前标注模型不适合直接承载书签

虽然看起来可以用 RDEPUBHighlight 勉强模拟书签,但这会带来明显问题:

  1. 书签不一定有选中文本
  2. 书签不应依赖 rangeInfo
  3. 书签列表应以“位置”为中心,而不是以“摘录内容”为中心
  4. 书签与高亮、划线、批注的展示和管理语义不同

因此,不建议把书签硬塞进现有高亮模型。

问题 3当前 UI 没有书签入口与状态反馈

RDEPUBReaderBottomToolView 当前结构来看,只有:

  1. 目录
  2. 批注列表
  3. 添加标注
  4. 设置

当前并没有:

  1. 当前页添加 / 取消书签入口
  2. 书签列表入口
  3. 当前阅读位置是否已加书签的状态反馈

这意味着即便补了数据层,用户仍然无法自然感知和使用这项能力。

需求拆解

建议把“书签能力”拆成四部分推进。

1. 独立书签模型

需要新增一个面向“位置记录”的独立模型,而不是复用高亮模型。

2. 独立书签持久化

需要新增书签读写接口,支持每本书存储多个书签。

3. 书签入口与状态反馈

需要让用户在当前页能轻量创建 / 取消书签,并能看到当前位置的书签状态。

4. 书签管理与跳转

需要补齐书签列表、点击跳转、删除等基本管理闭环。

技术路线对比

路线 A把书签并入 RDEPUBHighlight

思路:

  1. RDEPUBHighlight 增加一个新的 style 或特殊标识
  2. 将书签存入 highlights
  3. 复用现有标注列表页

优点:

  1. 表面上改动范围较小
  2. 可以快速借用现有列表管理代码

风险:

  1. 模型语义不清晰
  2. 书签没有文本选区时会很别扭
  3. 后续展示、筛选、持久化逻辑会越来越绕
  4. 容易让“书签”和“标注”边界变得混乱

路线 B完全独立一条书签链路

思路:

  1. 新建 RDEPUBBookmark
  2. 新增 loadBookmarks / saveBookmarks
  3. 新增书签列表页
  4. 独立接入添加、删除、跳转、状态更新

优点:

  1. 模型语义清晰
  2. 更符合产品能力边界
  3. 后续扩展空间更好

风险:

  1. 会新增一部分 UI 与管理代码
  2. 与高亮列表在形态上会有一定重复

路线 C模型独立交互模式局部复用

思路:

  1. 数据模型与持久化独立
  2. 定位、跳转和列表交互模式尽量复用现有高亮链路
  3. 第一阶段先做轻量闭环,再考虑更复杂管理能力

优点:

  1. 语义清晰,同时控制改动范围
  2. 能复用现有阅读位置链路和管理交互经验
  3. 更适合当前仓库的演进方式

风险:

  1. 仍需补一组新的数据与列表代码
  2. 需要明确书签状态判定规则,避免重排后误判

推荐结论

建议采用:

主路线:路线 C模型独立位置链路复用UI 先做轻量闭环

原因:

  1. 书签和标注的语义不同,模型应保持独立
  2. 当前仓库已有成熟的位置和跳转主链路,可以直接复用
  3. 第一阶段先满足“记录位置”的核心需求,不需要把书签做得过重

推荐方案

方案总览

书签能力第一阶段建议形成如下结构:

  1. 新增 RDEPUBBookmark 独立模型
  2. 扩展 RDEPUBReaderPersistence,新增 bookmarks 读写
  3. RDEPUBReaderController 中接入书签状态、添加、删除、跳转能力
  4. 增加轻量级书签入口和书签列表页
  5. 支持退出重进后的书签持久化恢复

推荐数据模型

建议书签模型至少包含以下字段:

  1. id
  2. bookIdentifier
  3. location
  4. chapterTitle
  5. displayText 或摘要信息(可选)
  6. note(可选)
  7. createdAt

其中第一阶段真正必要的最小集合是:

  1. id
  2. bookIdentifier
  3. location
  4. createdAt

建议同时补上 chapterTitle,这样书签列表在展示时会更友好,也更接近真实阅读器使用习惯。

推荐持久化结构

建议在 RDEPUBReaderPersistence 中新增:

  1. loadBookmarks(for:)
  2. saveBookmarks(_:for:)

RDEPUBUserDefaultsPersistence 中新增:

  1. bookmarksPrefix

第一阶段继续沿用当前:

  1. UserDefaults
  2. JSONEncoder / JSONDecoder
  3. bookIdentifier 作用域隔离

这样能在最小改动下形成可用闭环。

推荐 UI 入口

第一阶段建议至少补两类入口:

  1. 当前页添加 / 取消书签入口
  2. 书签列表入口

比较自然的放置方式是:

  1. 顶部工具栏增加“当前页书签”切换按钮
  2. 底部工具栏增加“书签列表”入口

这样可以把两种动作区分开:

  1. “加书签”是当前上下文动作
  2. “看书签列表”是全局管理动作

推荐交互闭环

第一阶段建议用户路径收敛成下面这条主链路:

  1. 用户在当前阅读位置点击书签按钮
  2. 如果当前语义位置未加书签,则创建书签
  3. 如果当前语义位置已加书签,则取消该书签
  4. 用户可从书签列表查看当前书籍的全部书签
  5. 点击任意书签后跳转到对应位置
  6. 支持从列表中删除书签

关键实现建议

Task K1新增书签模型

文件:

  1. Sources/RDReaderView/EPUBCore/RDEPUBReadingModels.swift

改动:

  1. 新增 RDEPUBBookmark: Codable, Equatable
  2. 字段以 location 为核心,不依赖选区文本

完成定义:

  1. 可以表达一本书中的一个可持久化书签位置

Task K2扩展书签持久化协议

文件:

  1. Sources/RDReaderView/EPUBUI/RDEPUBReaderPersistence.swift

改动:

  1. 协议增加 loadBookmarks / saveBookmarks
  2. RDEPUBUserDefaultsPersistence 增加 bookmarksPrefix

完成定义:

  1. 每本书可以稳定读取和保存多个书签

Task K3控制器接入书签状态与操作

文件:

  1. Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift

改动:

  1. 增加 activeBookmarks
  2. 打开图书时加载 bookmarks
  3. 增加 addBookmark
  4. 增加 removeBookmark
  5. 增加 toggleBookmark
  6. 增加 go(to: bookmark)
  7. 增加当前阅读位置是否已书签的状态计算

完成定义:

  1. 书签与阅读主链路、跳转链路、持久化链路打通

Task K4补书签 UI 与列表管理

文件:

  1. Sources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift
  2. 新增 Sources/RDReaderView/EPUBUI/RDEPUBReaderBookmarksViewController.swift
  3. 视情况补充顶部工具栏按钮

改动:

  1. 添加书签入口
  2. 添加书签列表入口
  3. 点击书签跳转
  4. 支持删除
  5. 提供当前位置书签状态反馈

完成定义:

  1. 用户能看到、添加、取消、查看和管理书签

当前页书签判定建议

“当前位置是否已加书签”不建议简单按“完全相同的 progression”判断,因为:

  1. 字号变化会触发重分页
  2. 视口尺寸变化会导致 progression 细微漂移
  3. WebView 和文本分页的进度精度并不完全一致

更合理的判定策略是:

  1. 优先比较标准化后的 href
  2. 如果存在 fragment,优先使用 fragment 作为更强锚点
  3. reflowable 路径允许一定 progression 容差
  4. fixed-layout 路径优先按页级位置判断

这样可以减少用户在重排、重进或轻微位置变化时的书签状态抖动。

验收矩阵建议

第一阶段至少要覆盖以下验证维度:

  1. 当前页可添加书签
  2. 同一位置可取消书签
  3. 书签列表展示正常
  4. 点击书签可以跳转
  5. 删除书签后列表更新正确
  6. 退出重进后书签仍存在
  7. 字号变化后书签仍能回到同章节或邻近语义位置
  8. webInteractive 路径正常
  9. textReflowable 路径正常
  10. webFixedLayout 路径至少能按页恢复

风险与注意点

风险 1把书签与高亮强行合并

这样短期看似省事,长期会让模型、持久化和 UI 语义都变重。

风险 2过度依赖精确 progression 匹配

如果书签命中规则太严格,重分页后会显得“不稳定”,用户体验会很差。

风险 3第一阶段把书签做得过重

书签能力的首要目标是“轻量记录位置”,不是一开始就做成复杂的收藏管理系统。

推荐实施顺序

如果目标是“最小投入换最大需求闭环”,建议按下面顺序推进:

  1. 先补独立 RDEPUBBookmark 模型
  2. 先扩展 RDEPUBReaderPersistence 的书签读写能力
  3. 再在 RDEPUBReaderController 接入书签主链路
  4. 最后补 UI 入口和书签列表页
  5. 待第一阶段稳定后,再考虑备注、排序、筛选等增强能力

推荐结论

“书签能力”这项需求,建议最终收敛为下面这句话:

为 EPUB 阅读器补齐一条独立的轻量书签链路,以 RDEPUBLocation 为核心记录位置,支持添加、取消、列表管理、跳转与持久化恢复,用来覆盖“只记录位置、不做摘录”的常见阅读需求。