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

11 KiB

书签能力方案讨论

需求背景

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 为核心记录位置,支持添加、取消、列表管理、跳转与持久化恢复,用来覆盖“只记录位置、不做摘录”的常见阅读需求。