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