- 新增 RDEPUBReaderSearchCoordinator 与 RDEPUBSelectionState 管理搜索和选中状态 - 新增 BookmarkChromeStateTests、NavigationBackwardTests、SelectionAnnotateTests 等 UI 测试 - 新增多个边界测试 epub 样本(损坏结构、空归档、缺失文件、流式外链验证) - 重构阅读器 chrome 状态管理,统一 tool bar 与 search bar 交互 - 优化大书分页缓存策略(RDEPUBChapterSummaryDiskCache、RDEPUBPageCountCache) - 移除废弃的 RDEPUBLocationConverter 和 RDEPUBPageBreakPolicy - 更新 epub-bridge.js 与 JS bridge 通信协议 - 全面更新现有 UI 测试以适配新的 helper 和状态管理
22 KiB
当前阅读器问题修复开发清单
最后更新:2026-06-13 依据说明:本清单仅基于当前仓库代码实现整理,不以现有说明文档是否准确为前提。
1. 文档目标
本文将“当前阅读器还存在的问题”展开为可执行的逐任务开发清单,目标是把工作从“方向建议”落到“具体改哪里、怎么改、如何验收”。
本文聚焦以下四类问题:
- 核心交互链路不稳定:选区、高亮、书签、工具栏状态分散。
- 默认安全策略偏弱:外链、解压、日志、持久化默认行为需要收口。
- 资源与分页稳定性不足:大资源内存峰值、离屏分页 WebView 边界控制不足。
- SDK 契约与回归手段不足:默认 no-op 容易误用,自动化观测点不够结构化。
2. 总体实施顺序
建议按以下顺序执行,减少返工:
bookmark-chrome-state-unificationselection-state-refactorexternal-link-policy-hardeningepub-archive-path-validationwebview-debug-sanitizationpersistence-defaults-hardeningresource-scheme-streaming-and-limitspagination-webview-hardeningcache-hardeningsdk-contract-and-test-hardening
原因:
- 任务 2 是已确认的重复 UI 状态写入问题,收益最高、风险相对低,应优先处理。
- 任务 1 更偏“状态模型收口优化”,问题真实存在,但严重度低于任务 2。
- 第 3-6 项收口默认安全行为,改动局部、收益高。
- 第 7-9 项涉及资源与分页底层,副作用更大,放在前面稳定后更容易验证。
- 最后一项负责把前面改动固化为稳定契约和测试基线。
3. 任务清单
任务 1:selection-state-refactor
3.1 目标
在不破坏当前清晰分层的前提下,收口选区相关状态的冗余表达,降低 view 层与 controller 层各持有一份选区状态所带来的维护成本和时序问题。
3.2 主要问题代码
Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextSelectionController.swiftSources/RDReaderView/EPUBUI/TextPage/RDEPUBTextContentView.swiftSources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAnnotationCoordinator.swiftSources/RDReaderView/EPUBUI/RDEPUBReaderController.swift
3.3 修改方案
当前代码并不是“混乱散写”,而是相对清晰的分层流:
- gesture
- view
- coordinator
- context/controller
其中真正值得优化的点主要有两个:
- view 层和 controller 层各自持有一份
currentSelection menuSelection是为UIMenuController时序保底引入的 workaround,属于合理存在,但可以尝试被更明确的状态模型替代
新增一个统一的选区状态模型仍然是可行方案,例如:
enum RDEPUBSelectionState: Equatable {
case idle
case selecting(anchor: Int)
case selected(RDEPUBSelection)
case committingAction(RDEPUBSelection, action: RDEPUBAnnotationMenuAction)
}
建议新增文件:
Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBSelectionState.swift
建议在 RDEPUBReaderAnnotationCoordinator 内新增或抽出一个单一入口:
applySelectionState(_ state: RDEPUBSelectionState)currentSelectionState
具体修改:
-
RDEPUBTextSelectionController- 移除“自己就是最终状态源”的职责。
- 保留字符范围计算与
RDEPUBSelection?构建。 isSelecting可保留为手势内部临时状态,但不再作为 UI 的最终依据。
-
RDEPUBTextContentView- 不再直接通过
currentSelection != nil作为所有 UI 行为的唯一判断条件。 - 长按、拖拽、结束只上报“开始选择 / 更新选择 / 结束选择 / 取消选择”事件。
menuSelection不建议直接删除。- 第一版应先保留
menuSelection,并把它明确标注为菜单时序缓冲状态。 - 待状态模型稳定后,再评估是否能安全移除。
- 不再直接通过
-
RDEPUBReaderAnnotationCoordinator.updateCurrentSelection(_:)- 改造成
applySelectionState(_:)。 - 在
.selected时统一:- 更新
controller.currentSelection - 决定是否展示工具栏
- 更新底部高亮按钮可用性
- 通知 delegate
- 更新
- 在
.idle时统一清理:- 清空
controller.currentSelection - 关闭菜单
- 刷新按钮状态
- 清空
- 改造成
-
RDEPUBReaderControllercurrentSelection继续保留为公开只读语义,但底层来源改为selectionState推导,而不是多处直接赋值。
3.4 实施步骤
- 新增
RDEPUBSelectionState.swift - 在
RDEPUBReaderAnnotationCoordinator中接管选区状态 - 重写
RDEPUBTextContentView中选区相关回调 - 先收口零散的
currentSelection != nil控制逻辑 - 保留
menuSelection,待验证不再需要后再移除 - 统一 delegate 通知路径
3.5 风险点
- 选区菜单弹出时机可能变化。
menuSelection是现有时序补丁,若贸然删除,菜单动作可能拿不到正确 selection。- Web 路径和 Native Text 路径都要走统一状态流,避免只修一条。
3.6 验收标准
- 长按后必然进入“选区中”状态。
- 松手后有有效文本时必然进入“已选中”状态。
- 复制/高亮/批注后必然回到空闲状态。
selection=1的 Demo 状态与按钮启用状态始终一致。
3.7 优先级修正
本任务应视为“结构优化 + 降低后续维护成本”,不是当前代码库里最严重的缺陷。建议优先级下调到与任务 2 同级,排在任务 2 之后实施。
任务 2:bookmark-chrome-state-unification
3.8 目标
统一顶部书签按钮、底部书签按钮、高亮按钮、添加高亮按钮的可用状态计算,避免不同协调器各自改一部分 UI。
3.9 主要问题代码
Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderChromeCoordinator.swiftSources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAnnotationCoordinator.swiftSources/RDReaderView/EPUBUI/RDEPUBReaderTopToolView.swiftSources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift
3.10 修改方案
新增统一 UI 状态模型:
struct RDEPUBReaderUIState {
let canToggleBookmark: Bool
let hasBookmarkAtCurrentLocation: Bool
let canShowBookmarks: Bool
let canAddHighlight: Bool
let canShowHighlights: Bool
}
建议新增文件:
Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderUIState.swift
具体修改:
-
RDEPUBReaderChromeCoordinator- 新增
makeUIState()和applyUIState(_:) updateReaderChrome()内不再直接散写多个setXxxEnabled
- 新增
-
RDEPUBReaderAnnotationCoordinatorupdateBookmarkChrome()不再直接写 view,改为触发context.updateReaderChrome()
-
RDEPUBReaderTopToolView/RDEPUBReaderBottomToolView- 保持 dumb view,只接收状态,不参与规则判断
3.11 实施步骤
- 新增
RDEPUBReaderUIState.swift - 改造
updateReaderChrome() - 不要简单删除
AnnotationCoordinator内的按钮状态刷新 - 区分两类触发时机:
ChromeCoordinator负责整页/整轮 chrome 同步AnnotationCoordinator负责用户操作后的即时反馈触发
- 将二者统一收口到同一个状态计算入口,例如
context.updateReaderChrome() - 调整书签新增/删除/跳转后的刷新入口为统一
updateReaderChrome()
3.12 风险点
- 如果简单删除
AnnotationCoordinator内的调用,可能丢失“用户操作后立即反馈”的刷新时机。 - 如果某些按钮当前依赖副作用显示,需要顺便梳理显示时机。
3.13 验收标准
- 添加第一个书签后,底部书签列表按钮立即可用。
- 删除最后一个书签后,底部书签列表按钮立即禁用。
- 当前位置已加书签时,顶部书签按钮始终是选中态。
任务 3:external-link-policy-hardening
3.14 目标
把“外链点击后直接打开系统应用”改成受控策略,避免 SDK 默认行为过于激进。
3.15 主要问题代码
Sources/RDReaderView/EPUBCore/RDEPUBWebView+JavaScriptBridge.swiftSources/RDReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swiftSources/RDReaderView/EPUBUI/Settings/RDEPUBReaderConfiguration.swift
3.16 修改方案
在 RDEPUBReaderConfiguration 新增:
allowedExternalURLSchemes: Set<String>requiresExternalLinkConfirmation: Bool- 可选:
allowedExternalURLHosts: Set<String>?
默认值建议:
- scheme 仅允许
https - 需要确认框
具体修改:
-
RDEPUBWebView+JavaScriptBridge.swift- 继续识别外链,但不再表示“允许直接打开”。
- 只负责把外链事件上抛。
-
RDEPUBReaderController+ContentDelegates.swift- 新增私有方法:
shouldAllowExternalURL(_:)presentExternalLinkConfirmation(for:)openExternalURLIfAllowed(_:)
didActivateExternalLink改为:- 先 delegate 回调
- 再走配置校验
- 再弹确认框
- 最后调用
UIApplication.shared.open
- 新增私有方法:
-
可选扩展
RDEPUBReaderDelegate- 增加可选拦截钩子:
epubReader(_:shouldOpenExternalURL:) -> Bool
- 增加可选拦截钩子:
3.17 实施步骤
- 改配置模型与默认值
- 改控制器外链处理逻辑
- 增加确认对话框
- 增加测试用 mock URL 覆盖
https、mailto、未知 scheme`
3.18 风险点
- 某些 Demo 书可能依赖
mailto或tel,要明确它们现在默认不再直接打开。
3.19 验收标准
https外链在确认后打开。- 默认配置下
mailto和tel不直接打开。 - 未知 scheme 一律拒绝。
任务 4:epub-archive-path-validation
3.20 目标
为 EPUB 解压路径增加显式安全校验,防止恶意压缩包通过相对路径写出解压根目录。
3.21 主要问题代码
Sources/RDReaderView/EPUBCore/RDEPUBParser+Archive.swift
3.22 修改方案
新增私有方法:
validatedExtractionDestination(for:extractionRoot:) -> URL?
校验规则:
- 拒绝绝对路径。
- 拒绝包含
..的路径段。 standardizedFileURL.path必须位于extractionRoot.standardizedFileURL.path下。
具体修改:
-
extractArchiveIfNeeded(epubURL:)- 在循环内先调用
validatedExtractionDestination - 无效 entry 可:
- 直接抛错终止解析,或
- 记日志并跳过
- 建议优先抛错,行为更清晰
- 在循环内先调用
-
增加自定义错误:
- 若当前
RDEPUBParserError里没有合适 case,可新增invalidArchiveEntryPath(String)
- 若当前
3.23 实施步骤
- 增加路径校验方法
- 替换原始
destinationURL生成逻辑 - 增加针对恶意 entry path 的单元测试
3.24 风险点
- 少数历史 EPUB 可能带奇怪路径分隔符,需要兼容性验证。
3.25 验收标准
- 正常 EPUB 仍可正常解压。
- 带
../的恶意 entry 会被拒绝。
任务 5:webview-debug-sanitization
3.26 目标
减少默认调试日志泄露阅读内容的风险,并关闭不必要的 inspectable 默认值。
3.27 主要问题代码
Sources/RDReaderView/EPUBCore/RDEPUBWebViewDebug.swiftSources/RDReaderView/EPUBCore/RDEPUBPaginator.swiftSources/RDReaderView/EPUBCore/RDEPUBWebView+Configuration.swift
3.28 修改方案
-
RDEPUBWebViewDebuglogMessage改为只打印:- message name
- 字段名列表
- 文本长度
- URL 摘要
- 不再打印完整 message body
-
新增更细粒度配置
RDEPUBWebViewDebugEnabledRDEPUBVerboseWebViewDebugEnabled
-
RDEPUBPaginator与正常阅读 WebViewisInspectable改为受配置控制- 默认关闭
-
RDEPUBReaderConfiguration- 增加:
allowsInspectableWebViewsenablesVerboseWebViewLogging
- 增加:
3.29 实施步骤
- 改日志输出格式
- 改 inspectable 的默认逻辑
- 为 verbose 模式保留调试后门
3.30 风险点
- 调试某些 JS 问题时信息会变少,因此要保留显式 verbose 开关。
3.31 验收标准
- 默认 DEBUG 包不输出完整选中文本。
- 默认分页 WebView 不可 inspect。
- 打开 verbose 开关后仍能深度调试。
任务 6:persistence-defaults-hardening
3.32 目标
降低默认 UserDefaults 持久化的误用风险和数据膨胀风险,同时不破坏当前 API 可用性。
3.33 主要问题代码
Sources/RDReaderView/EPUBUI/RDEPUBReaderPersistence.swiftSources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAnnotationCoordinator.swiftSources/RDReaderView/EPUBUI/RDEPUBReaderController.swift
3.34 修改方案
短期目标:
- 明确
RDEPUBUserDefaultsPersistence是轻量实现。 - 高亮持久化优先依赖:
locationrangeInfostylenote
text字段作为冗余缓存,而不是唯一恢复依据。
中期可选目标:
新增文件持久化实现:
Sources/RDReaderView/EPUBUI/RDEPUBFilePersistence.swift
3.35 具体修改
-
RDEPUBReaderPersistence.swift- 为默认实现增加 DEBUG 警告,提示 bookmarks/settings 默认实现是 no-op
-
RDEPUBUserDefaultsPersistence- 增加大小保护,例如当高亮集合序列化后超阈值时打印告警
- 可选增加版本字段,便于后续迁移
-
RDEPUBReaderController- 初始化默认 persistence 时,在注释与命名上明确这是默认轻量实现
3.36 实施步骤
- 增加 warning/assert 机制
- 优化高亮持久化字段策略
- 视时间增加
RDEPUBFilePersistence
3.37 风险点
- 如果外部代码依赖
text永远存在,需要做好兼容。
3.38 验收标准
- 未实现书签持久化时,DEBUG 下能看到明确提示。
- 高亮即使
text丢失,也可基于范围信息恢复定位。
任务 7:resource-scheme-streaming-and-limits
3.39 目标
降低 WKURLSchemeHandler 对大资源的内存冲击,避免每次都整块 Data(contentsOf:) 读入。
3.40 主要问题代码
Sources/RDReaderView/EPUBCore/RDEPUBResourceURLSchemeHandler.swift
3.41 修改方案
分两步做:
第一步,先加大小阈值和告警:
- 读取文件属性获取大小
- 超过阈值时走分块发送
第二步,再补流式发送:
- 使用
FileHandle或InputStream - 按固定 chunk size 反复
didReceive(data)
建议新增私有方法:
resourceMetadata(for:)respondWithStreaming(fileURL:requestURL:taskID:urlSchemeTask:)respondWithInMemoryData(...)
3.42 实施步骤
- 先加文件大小检测
- 小文件保留内存直读
- 大文件切换到分块发送
- 增加取消任务时的中断判断
3.43 风险点
WKURLSchemeHandler分块发送要注意 stop 之后不继续回调。
3.44 验收标准
- 大图片/字体加载时内存峰值下降。
- 取消任务不会继续发送数据。
任务 8:pagination-webview-hardening
3.45 目标
加强离屏分页 WebView 的边界控制,减少外部资源波动和调试副作用。
3.46 主要问题代码
Sources/RDReaderView/EPUBCore/RDEPUBPaginator.swift
3.47 修改方案
-
为分页 WebView 增加严格导航限制
- 仅允许
file:// - 仅允许
ss-reader:// - 拒绝
http/https
- 仅允许
-
将
isInspectable受配置控制 -
增加测量稳定性日志
- 记录三轮测量结果
- 当三轮差异过大时告警
建议新增私有方法:
shouldAllowPaginatorNavigation(url:)recordMeasurement(pass:value:)
3.48 实施步骤
- 扩展
WKNavigationDelegate - 加入导航 allowlist
- 记录多轮测量差异
3.49 风险点
- 某些 EPUB 样式若依赖外链资源,分页结果可能与当前行为不同,但这本身就是更安全的策略。
3.50 验收标准
- 分页 WebView 不发起外部导航。
- 三轮测量波动可观测。
任务 9:cache-hardening
3.51 目标
提升章节摘要磁盘缓存的健壮性、可观测性和可清理能力。
3.52 主要问题代码
Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterSummaryDiskCache.swift
3.53 修改方案
-
原子写入
- 先写
.tmp - 再 replace 到正式文件
- 先写
-
读失败分类
- 文件不存在
- 读取失败
- 解码失败
-
精细化清理
removeAll()removeAll(forBookID:)removeAll(forRenderSignature:)
-
增加统计接口
- 缓存文件数
- 总大小
3.54 实施步骤
- 改写入策略
- 增加读失败日志
- 增加清理接口
- 补测试覆盖损坏文件、半写文件
3.55 风险点
- 清理策略接口若公开,需要评估是否作为 public API。
3.56 验收标准
- 缓存文件损坏不会影响整本书重新打开。
- 能按需清理缓存,而不是只能全删。
任务 10:sdk-contract-and-test-hardening
3.57 目标
把前面所有修复固化为更清晰的 SDK 契约和更稳定的测试观测点。
3.58 主要问题代码
Sources/RDReaderView/EPUBUI/RDEPUBReaderPersistence.swiftSources/RDReaderView/EPUBUI/RDURLReaderController.swiftReadViewDemo/ReadViewDemoUITests/
3.59 修改方案
-
强化 persistence 契约
- 继续保留默认实现,但在 DEBUG 下对 no-op 行为给出明确信号
-
Demo 状态能力修正
- 当前仓库并非完全没有结构化状态。
ReadViewDemo/ReadViewDemoUITests/Helpers/DemoReaderState.swift已经提供了状态解析器和waitForDemoReaderState(...)结构化等待能力。- 真正需要推进的不是再造一个
DemoReaderSnapshot,而是把仍然大量存在的waitForReaderState(containing:)迁移到结构化断言。
建议方向:
- 继续保留隐藏 label 输出
- 保持
key=value格式 - 统一测试侧只通过
DemoReaderState解析和断言 - 逐步淘汰
containing:子串匹配,避免highlights=1命中highlights=10之类误判
- 补测试
- 单元测试:
RDEPUBParserArchiveSafetyTestsRDEPUBExternalLinkPolicyTestsRDEPUBChapterSummaryDiskCacheTests
- UI 测试:
- 选区状态流
- 书签启用/禁用
- 外链确认框
- 单元测试:
3.60 实施步骤
- 改 Demo 状态串生成逻辑
- 优先将
waitForReaderState(containing:)迁移为waitForDemoReaderState(...) - 调整 UI 测试断言
- 增加单元测试 target 或现有测试目录内的新文件
3.61 风险点
- 现有 UI 测试如果强依赖旧状态串,需要同步迁移。
3.62 验收标准
- UI 测试失败时能明确知道是选区状态、书签状态还是导航状态失配。
- persistence 的默认 no-op 行为在开发期不再“静默”。
4. 补充清理项
以下问题已在代码中可见,但未纳入前 10 个主任务,建议作为补充任务或穿插清理项处理。
4.1 pageBreakBefore/pageBreakAfter 相关死代码审计
涉及代码:
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swiftSources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBSemanticMarkerInjector.swiftSources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBPageBreakPolicy.swiftSources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBChapterPageCounter.swift
建议动作:
- 审计
pageBreakBefore/pageBreakAfter从注入到消费的完整链路 - 判断是否有“已定义、已注入、但实际无有效触发”的死路径
- 若确认无效,删除或补测试锁定其真实行为
4.2 PAGINATION-DEBUG 输出清理
涉及代码:
Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBCoreTextPageFrameFactory.swiftSources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBChapterPageCounter.swift
建议动作:
- 将裸
print改为受控调试开关 - 默认关闭
- 避免分页细节和正文片段直接进入控制台
4.3 ChapterRuntime 旧代码与遗留路径清理
建议动作:
- 梳理
ChapterRuntime下是否存在已不再被主流程使用的辅助类型或过渡实现 - 对“仍被引用但语义已经过时”的代码先补注释标记
- 对“完全无调用”的代码建立删除候选清单
5. 推荐里程碑切分
里程碑 A:核心交互稳定
包含任务:
selection-state-refactorbookmark-chrome-state-unification
完成标准:
- 选区、高亮、书签相关 UI 测试恢复稳定
里程碑 B:默认安全行为收口
包含任务:
external-link-policy-hardeningepub-archive-path-validationwebview-debug-sanitizationpersistence-defaults-hardening
完成标准:
- 默认配置下不再直接打开高风险外链
- 解压路径具备显式校验
- 默认调试日志不泄露完整阅读内容
里程碑 C:底层稳定性优化
包含任务:
resource-scheme-streaming-and-limitspagination-webview-hardeningcache-hardening
完成标准:
- 大资源加载和分页更稳定
- 缓存更健壮、可清理
里程碑 D:契约与测试固化
包含任务:
sdk-contract-and-test-hardening
完成标准:
- SDK 默认行为更清晰
- 回归测试对关键状态具备稳定观测能力
6. 建议的开发节奏
若按 2 周一个小迭代,可参考:
- 第 1 周:
bookmark-chrome-state-unification+selection-state-refactor - 第 2 周:
external-link-policy-hardening+epub-archive-path-validation+webview-debug-sanitization - 第 3 周:
persistence-defaults-hardening+resource-scheme-streaming-and-limits+pagination-webview-hardening - 第 4 周:
cache-hardening+sdk-contract-and-test-hardening与回归收口
如果只想先做最值的部分,建议最小交付集合为:
bookmark-chrome-state-unificationselection-state-refactorexternal-link-policy-hardeningepub-archive-path-validation
这四项完成后,阅读器的“主观可用性”和“默认安全性”会先提升一个台阶。