ReadViewSDK/Doc/EPUB_MAINTENANCE.md
shen c0aac56083 docs: 同步 Doc 文档与当前代码状态
- 更新模块文件数(EPUBCore 31+2, EPUBTextRendering 13, ReaderView 5, EPUBUI 19)
- 更新 Swift 版本(5.10)、iOS 版本(15.0)、行数等基本信息
- 补充新增文件文档(TextAnchor, RenderRequest, WebViewDebug, AssetRepository,
  TextIndexTable, TextPerformanceSampler, ChapterData, PageInteractionController 等)
- 更新 RDEPUBLocation 模型(新增 rangeAnchor 字段)
- 更新 RDEPUBReaderConfiguration(13 项配置)、Delegate 签名、Persistence(8 方法)
- 修正 RDURLReaderController 归属(ReaderView → EPUBUI)
- 移除过时的 LegacyRDReaderController 引用
- 标记 SS→RD 命名迁移为已完成
2026-05-25 20:31:10 +08:00

17 KiB
Raw Blame History

EPUB 阅读模块维护与排查指南

1. 文档目的

本文档说明 EPUB 阅读模块的关键文件职责、主要数据流、常见问题排查和后续扩展建议,供维护时快速建立上下文。

完整架构设计请参考 ARCHITECTURE.md。 编码规范请参考 CODING_STYLE.md


2. 关键文件速查

2.1 EPUBCore 层

文件 职责
RDEPUBParser.swift + 5 扩展 EPUB 解压、container.xml、OPF、manifest、spine、TOC/NCX 解析、阅读配置文件判断
RDEPUBPublication.swift 解析结果的唯一聚合入口,外部不直接访问 parser 字段
RDEPUBModels.swift metadata / manifest / spine / TOC 基础模型
RDEPUBReadingModels.swift RDEPUBLocation、RDEPUBViewport、RDEPUBHighlight、RDEPUBBookmark、选区模型
RDEPUBResourceResolver.swift href → fileURL / schemeURL 的统一入口,normalizedHref 标准化路径
RDEPUBPaginator.swift 离屏 WKWebView 分页,计算每个 spine 资源被视口切成多少页
RDEPUBReadingSession.swift 状态机 + 会话协调,管理 staged/active/pending 页模型
RDEPUBNavigatorState.swift 状态枚举initializing/loading/idle/jumping/moving/repaginating
RDEPUBWebView.swift + 5 扩展 承载 spine 资源的 WKWebView分页 CSS 注入、JS bridge、fixed wrapper、搜索装饰
RDEPUBResourceURLSchemeHandler.swift ss-reader:// 协议,从本地解压目录提供所有 EPUB 资源
RDEPUBStyleSheetBuilder.swift 生成注入 WebView 的 CSS
RDEPUBJavaScriptBridge.swift JS ↔ Swift 消息定义和编解码
RDEPUBSearchEngine.swift 搜索协议定义 + WebView 全文搜索引擎
RDEPUBSearchModels.swift 搜索模型SearchMatch/Result/State/Presentation
RDEPUBTextAnchor.swift 文本锚点和范围锚点(精确定位字符位置)
RDEPUBRenderRequest.swift 渲染请求模型、展示样式、固定版式适配/Spread 枚举
RDEPUBWebViewDebug.swift WebView 调试日志工具(导航/JS/消息/Scheme
RDEPUBAssetRepository.swift 静态资源加载器JS 脚本、HTML 模板、模板变量替换)
RDEPUBPreferences.swift 用户阅读偏好聚合
RDEPUBNavigatorLayoutContext.swift 容器布局上下文
RDEPUBFixedLayoutTemplate.swift 固定版式 HTML 模板生成

2.2 EPUBTextRendering 层

文件 职责
RDEPUBTextRenderer.swift 渲染引擎协议 + 错误类型 + 样式 / 内容模型
RDEPUBDTCoreTextRenderer.swift DTCoreText 实现HTML → NSAttributedString
RDEPUBTextRendererSupport.swift fragment 标记注入、fragment offset 提取、属性标准化
RDEPUBTextPaginationSupport.swift NSAttributedString 切页算法
RDEPUBTextBookBuilder.swift 驱动逐章节渲染和分页,生成 RDEPUBTextBook
RDEPUBTextLayouter.swift CoreText 分页引擎(~810 行,含 4 级语义边界调整)
RDEPUBTextLayoutFrame.swift 单帧分页结果模型contentRange、breakReason、语义提示
RDEPUBTextBookCache.swift 分页结果磁盘缓存
RDEPUBChapterData.swift 章节数据聚合模型(高亮、搜索结果、页面查询)
RDEPUBTextSearchEngine.swift 纯文本全文搜索引擎
RDPlainTextBookBuilder.swift 纯文本(.txt书籍构建器
RDEPUBTextIndexTable.swift 全书文本索引表(章节偏移、行列映射、锚点转换)
RDEPUBTextPerformanceSampler.swift 性能采样器(渲染/分页耗时、缓存命中率)

2.3 EPUBUI 层

文件 职责
RDEPUBReaderController.swift 开箱即用读者控制器(~1995 行),整合 session / readerView / UI
RDEPUBReaderConfiguration.swift 外部配置项13 个:字号、主题、翻页模式等)
RDEPUBReaderPersistence.swift 阅读位置、高亮、书签的本地持久化8 个方法)
RDEPUBReaderSettings.swift 可持久化用户设置模型 + DisplayMode/ThemePreset 枚举
RDEPUBReaderTheme.swift 6 个内置主题预设 + 颜色属性
RDEPUBReaderDelegate.swift 12 个可选委托方法
RDEPUBReaderToolView.swift 工具栏基类(分隔线 + 主题适配)
RDEPUBReaderTopToolView.swift 顶部导航栏(返回 + 书签 + 标题)
RDEPUBReaderBottomToolView.swift 底部工具栏(目录/书签/标注/设置)
RDEPUBReaderChapterListController.swift 目录列表面板
RDEPUBReaderHighlightsViewController.swift 标注管理面板(过滤、编辑、删除、跳转)
RDEPUBReaderSettingsViewController.swift 设置面板(亮度、字号、行高、模式、主题)
RDEPUBReaderTableOfContentsItem.swift 展平目录条目模型
RDEPUBWebContentView.swift WKWebView 内容视图,供 RDReaderView 渲染 WebView 类 spine
RDEPUBTextContentView.swift 文本内容视图(~728 行),供 RDReaderView 渲染文本类 spine
RDEPUBPageInteractionController.swift CoreText 页面级交互控制器
RDEPUBSelectionOverlayView.swift 选区覆盖视图
RDEPUBPageLayoutSnapshot.swift 页面几何模型
RDURLReaderController.swift 最简 URL 入口(.epub/.txt 自动识别)

3. 资源服务协议说明

加载 spine 资源统一使用 ss-reader:// 协议,不直接使用 file://

// 正确方式
let schemeURL = publication.resourceResolver.schemeURL(forHref: spineItem.href)
webView.load(URLRequest(url: schemeURL))

// 禁止方式relative 资源、图片、CSS 会失效)
webView.loadHTMLString(htmlString, baseURL: nil)

这样 HTML 内部的相对 CSS、图片、字体和 iframe 资源都会通过同一个 schemeHandler 解析,避免 loadHTMLStringallowingReadAccessTo 导致的资源失效问题。

3.1 DTCoreText 实现逻辑textReflowable 路径)

readingProfile == .textReflowable 时,渲染链路不再走 WebView 列分页,而是走 EPUBTextRendering/ 的 DTCoreText 文本渲染链。

端到端流程:

  1. RDEPUBTextBookBuilder 按 spine 顺序读取章节 HTML。
  2. RDEPUBTextRendererSupport.injectFragmentMarkers(into:) 注入 fragment 标记,保证目录锚点可映射到文本偏移。
  3. RDEPUBDTCoreTextRenderer.renderChapter(html:baseURL:style:) 执行章节渲染。
  4. 渲染成功时,内部通过 DTHTMLAttributedStringBuilder 将 HTML 转为 NSAttributedString
  5. RDEPUBTextRendererSupport.extractFragmentOffsets(from:) 提取 fragment -> 字符偏移 映射。
  6. RDEPUBTextRendererSupport.normalizeReadingAttributes(in:style:) 统一字体、行距、颜色等阅读属性。
  7. RDEPUBTextPaginationSupport 将富文本按可视区域切页,构建页模型。
  8. RDEPUBTextBookBuilder 汇总章节与页映射,交给 RDEPUBTextContentView 呈现。

关键实现点:

  1. 条件编译:#if canImport(DTCoreText)
    • 可用时走 DTCoreText 路径。
    • 不可用时走 fallbackNSAttributedString 降级),确保工程可编译可运行。
  2. dtOptions(baseURL:style:) 会注入:
    • 默认字体族 / 字体名 / 字号
    • 行高倍数(由字体行高与行距折算)
    • NSBaseURLDocumentOption(保证相对资源解析)
    • 默认文本颜色
  3. fragment 偏移在“属性标准化前后”都要保持稳定,避免目录跳转漂移。
  4. 文本分页以字符范围为准,不以 WebView 像素滚动位置为准。

失败与降级策略:

  1. HTML 转 UTF-8 失败:抛出 htmlEncodingFailed
  2. DTCoreText builder 失败:进入 fallback 渲染路径。
  3. fallback 仍会执行 fragment 提取与阅读属性标准化,保证目录跳转和阅读样式能力不丢失。

与定位恢复的关系:

  1. 仍使用 RDEPUBLocationhref + progression)作为跨会话恢复模型。
  2. textReflowable 模式下,fragmentOffsets 用于将目录锚点映射到分页后的字符区间,再换算到页索引。
  3. 字号/行距变化触发重分页后,优先按 href + progression 回落恢复,再用 fragment 做细化定位。

推荐日志点DTCoreText 专用):

  1. RDEPUBDTCoreTextRenderer.renderChapter(...):记录章节 href、输入 HTML 长度、渲染耗时。
  2. makeAttributedString(from:baseURL:style:):记录 DTCoreText 是否成功返回 attributed string。
  3. extractFragmentOffsets(from:):记录 fragment 数量与关键锚点是否命中。
  4. RDEPUBTextPaginationSupport:记录总页数、每页字符范围边界。

4. 常见问题排查

4.1 EPUB 打不开或解析失败

优先检查:

  1. RDEPUBParser.parse(epubURL:) 是否抛出异常,打印具体错误
  2. RDEPUBParser+Archive.swift — 解压是否成功
  3. META-INF/container.xml 是否存在
  4. OPF 中 manifest / spine 是否完整

常见原因:

  • 文件不是标准 EPUB ZIP 结构
  • container.xml 找不到 OPF 路径
  • OPF manifest 或 spine 格式异常
  • 文件路径包含特殊字符导致解压目录路径错误

排查顺序:

  1. 确认 EPUB 已成功复制到沙盒
  2. 确认解压目录存在(extractionRootURL
  3. 确认 opfDirectoryURL 不为 nil
  4. 确认 spine.count > 0

4.2 EPUB 显示空白页

优先检查:

  1. RDEPUBWebContentView.loadPage(...) 传入的 spineIndex 是否在范围内
  2. RDEPUBWebView+Reflowable — 分页 CSS 注入是否成功
  3. WKNavigationDelegate.didFinish 是否正常触发
  4. RDEPUBResourceURLSchemeHandler 是否正确返回资源数据

常见原因:

  1. scheme URL 拼装错误,资源请求 404
  2. 注入的 CSS 破坏了原始布局(尤其是 column-width 样式冲突)
  3. fixed-layout 书籍走了 reflowable 渲染路径readingProfile 判定错误)

排查顺序:

  1. 确认 schemeURL(forHref:) 返回的 URL 对应文件真实存在
  2. RDEPUBWebViewDebug 打开 WebView inspector 查看网络请求
  3. 确认 readingProfile 判定正确webInteractive / webFixedLayout / textReflowable

4.3 修改字号后位置恢复不准

原因: 字号变化触发重新分页repaginating页号失效。

正确做法:

当前实现依赖 href + progression 恢复位置,不依赖纯页号。排查重点:

  1. RDEPUBLocation.href 是否已通过 normalizedHref 标准化
  2. progression 是否在字号变化后重新从 WebView 读取
  3. RDEPUBReadingSession.navigatorState 是否在重分页后回到 .idle 并消费了 staged snapshot

4.4 目录点击后跳错位置

优先检查:

  1. RDEPUBParser+TOC.swift — TOC href 是否正确标准化为相对 OPF 路径
  2. RDEPUBResourceResolver.normalizedHref(_:) — 跳转时是否使用了标准化 href
  3. fragment 是否正确传入 WebView 锚点滚动

常见原因:

  1. TOC 链接是相对 Nav 或 NCX 文件目录,未转为相对 OPF
  2. href#fragment 只命中了资源,未命中锚点
  3. 同一资源中多个 TOC 条目被折叠成同一页

排查步骤:

  1. 打印原始 TOC href 和标准化后 href 进行比较
  2. 确认 fragment 字段被保留在 RDEPUBLocation 中
  3. 确认 WebView 收到 fragment 后执行了 scrollToFragment(fragment:) JS 调用

4.5 正文内部链接不工作

优先检查:

  1. RDEPUBWebView+JavaScriptBridge 中内部链接拦截逻辑
  2. RDEPUBReaderController.navigateToLocation(_:) — 跨资源跳转路径

常见原因:

  1. 链接是空 #fragment,但当前 DOM 中找不到对应元素
  2. 跨资源链接 href 未被 normalizedHref 处理
  3. 对应 spine 资源没有分页结果pageCounts[spineIndex] == 0

4.6 图片多时位置跳动

原因: 图片异步加载导致 DOM 高度变化,进度报告时机早于实际布局稳定。

排查:

  1. 检查 JS ResizeObserver 是否在图片加载后触发了进度重同步
  2. 确认 didUpdateLocation 不在 pending navigation 未消费前覆盖目标位置
  3. 确认 scrollToLocation() JS 调用时序在 didFinish 之后

4.7 高亮未显示或恢复失败

优先检查:

  1. RDEPUBHighlight 是否成功写入 persistence
  2. RDEPUBReaderController.activeHighlights 是否正确过滤出当前 spine 的高亮
  3. RDEPUBWebView+JavaScriptBridgesetHighlights() JS 调用是否触发
  4. JS 中 rangeFromInfo() 是否能找到对应 DOM 节点(嵌套节点恢复容易失败)

排查顺序:

  1. 确认有效选区(currentSelection 不为 nil
  2. 确认 addHighlight() 调用后 persistence 中数据已写入
  3. 确认当前页传入的 highlights 数组不为空
  4. 确认 JS 收到了 highlights 数据并尝试了 rangeFromInfo 恢复

4.8 fixed-layout 书籍显示异常

优先检查:

  1. RDEPUBPublication.readingProfile 是否判定为 .webFixedLayout
  2. RDEPUBFixedLayoutTemplate — wrapper HTML 是否正确生成
  3. viewport meta 尺寸是否与书籍 OPF 中声明一致
  4. spread 模式是否正确(单页 vs 双页)

5. 推荐日志点

遇到复杂问题时,建议先在以下位置加日志:

  1. RDEPUBParser.parse(epubURL:) — 打印解压路径、OPF 路径、spine 数量
  2. RDEPUBReadingSession.transition(to:) — 打印状态跃迁
  3. RDEPUBPaginator — 打印每个 spine 资源的分页结果
  4. RDEPUBReaderController.restoreLocation(_:) — 打印恢复位置的 href / progression
  5. RDEPUBWebView+JavaScriptBridge — 打印所有 JS 消息收发
  6. RDEPUBResourceURLSchemeHandler — 打印资源请求 URL 和响应状态

6. 标准排查顺序

当 EPUB 功能出问题时,建议按以下顺序排查:

  1. 解析是否成功 — parser.spine.count > 0metadata 正确?
  2. readingProfile 是否正确 — 用了对的渲染路径?
  3. TOC href 是否标准化 — 与 spine href 格式一致?
  4. 分页结果是否合理 — 每个 spine 资源的 pageCount > 0
  5. 资源是否能正常加载 — ss-reader:// 请求是否 200
  6. JS bridge 是否正常 — progression 上报是否到达 Swift 侧?
  7. 状态机是否流转正确 — session.navigatorState 是否符合预期?
  8. 控制器是否正确映射页号 — EPUBPage 列表与 RDReaderView 页号对应是否正确?

7. 后续扩展建议

  1. 拆分 RDEPUBReaderController:约 1995 行EPUB 加载、UI、数据源职责仍混在一起建议继续向 library 层迁移
  2. 基线验证:用四本样书跑一轮分页耗时、目录命中率、末页事件、设置恢复稳定性数据
  3. 横竖屏支持回归矩阵:控制器级 viewWillTransition() 与 viewport 去重已接入,下一步重点是补齐 pageCurl / scroll、三条正文路径和 iPad 视口变化场景的固定回归
  4. Pod 分层:评估是否将 EPUBCore / EPUBTextRendering / EPUBUI 拆为独立 subspec
  5. RDEPUBParser 继续收敛OPF metadata 细节处理仍偏重,可考虑按 OPFMetadataParser 单独拆出

8. 阅读器能力现状与下一阶段规划

8.1 当前已具备的核心能力

当前 SDK 已具备一套可用的 EPUB 阅读器主链路,包含:

  1. EPUB 打开与解析:支持 ZIP 解压、OPF / spine / TOC 解析,以及三类 readingProfile 分流
  2. 正文渲染与分页:支持 WebView 路径和 DTCoreText 文本路径,覆盖固定版式、交互式可重排和纯文本可重排 EPUB
  3. 完整阅读交互:支持目录跳转、翻页、阅读位置恢复、主题切换、字号和行高调整、亮度调整
  4. 标注能力:支持高亮、划线、批注、标注列表、编辑、删除、跳转与持久化恢复
  5. 书签能力:支持当前位置书签添加/取消、书签列表、删除、跳转与持久化恢复
  6. 自动化测试基础能力:已接入 ReadViewSDKDemoTests / ReadViewSDKDemoUITests、样书夹具定位工具,以及 parser / resolver / persistence 首批 XCTest 和阅读器启动 smoke 用例
  7. 搜索能力:支持全文搜索、匹配项跳转、当前匹配高亮

8.2 高优先级待补能力

从“可用 MVP”走向“稳定可交付阅读器”当前最值得优先完成的是

  1. 样书基线验证 需要基于四本样书补齐分页耗时、目录命中率、末页事件、设置恢复稳定性等基线数据

8.3 面向完整商业阅读器的下一阶段能力

若目标是继续向完整阅读产品演进,建议后续补齐以下能力:

  1. 阅读进度展示 包括全书进度、章节进度、页码信息、剩余页数或剩余章节等
  2. 更完整的标注管理 包括按章节分组、排序、筛选、批量删除、标注导出、复制批注内容等
  3. 更成熟的搜索体验 包括搜索结果列表页、章节聚合、上下文预览、搜索历史等
  4. 更多阅读增强能力 包括字体切换、自定义主题、翻页手势自定义、TTS 朗读、无障碍支持等

8.4 工程侧持续建设建议

除了阅读器功能本身,还建议并行推进以下工程项:

  1. 控制器继续拆分 RDEPUBReaderController 与 demo 层控制器都偏大,继续叠加功能会增加维护成本
  2. Parser 与模块职责继续收敛 RDEPUBParser 仍承担较多 OPF metadata 解析细节,后续可继续按职责拆分
  3. 回归验收矩阵固化 建议把样书、渲染路径、主题、字号、翻页模式、标注恢复等整理为固定回归清单

8.5 推荐下一阶段实施顺序

如果以“最小投入换最大稳定性提升”为目标,建议按下面顺序推进:

  1. 样书基线验证