ReadViewSDK/Doc/EPUB_MAINTENANCE.md

15 KiB
Raw Blame History

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

1. 文档目的

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

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


2. 关键文件速查

2.1 EPUBCore 层

文件 职责
RDEPUBParser.swift + 扩展 EPUB 解压、container.xml、OPF、manifest、spine、TOC/NCX 解析
RDEPUBPublication.swift 解析结果的唯一聚合入口,外部不直接访问 parser 字段
RDEPUBModels.swift metadata / manifest / spine / TOC 基础模型
RDEPUBReadingModels.swift RDEPUBLocation、RDEPUBViewport、RDEPUBHighlight、选区模型
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 + 扩展 承载 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

2.2 EPUBTextRendering 层

文件 职责
RDEPUBTextRenderer.swift 渲染引擎协议 + 错误类型 + 样式 / 内容模型
RDEPUBDTCoreTextRenderer.swift DTCoreText 实现HTML → NSAttributedString
RDEPUBTextRendererSupport.swift fragment 标记注入、fragment offset 提取、属性标准化
RDEPUBTextPaginationSupport.swift NSAttributedString 切页算法
RDEPUBTextBookBuilder.swift 驱动逐章节渲染和分页,生成 RDEPUBTextBook

2.3 EPUBUI 层

文件 职责
RDEPUBReaderController.swift 开箱即用读者控制器,整合 session / readerView / UI
RDEPUBReaderConfiguration.swift 外部配置项(字号、主题、翻页模式等)
RDEPUBReaderPersistence.swift 阅读位置和高亮的本地持久化
RDEPUBReaderSettings.swift 运行时设置状态
RDEPUBWebContentView.swift WKWebView 内容视图,供 RDReaderView 渲染 WebView 类 spine
RDEPUBTextContentView.swift 文本内容视图,供 RDReaderView 渲染文本类 spine

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. 拆分 demo 层 RDReaderController:约 1900+ 行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. 样书基线验证