ReadViewSDK/Doc/ARCHITECTURE.md
2026-05-21 19:40:51 +08:00

416 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# RDReaderView 架构文档
## 1. 项目概览
RDReaderView 是一个 iOS 阅读器组件库CocoaPods提供开箱即用的 EPUB 阅读能力,并保留对纯文本翻页的支持。
- **最低 iOS 版本**15.0podspec 仍标注 9.0,实际 demo Podfile 要求 15.0
- **Swift 版本**5.0+
- **依赖**ZIPFoundationEPUB 解压、DTCoreText文本 EPUB 渲染)
- **Demo 额外依赖**SnapKit、SSAlertSwift
---
## 2. 总体分层
```
┌─────────────────────────────────────────────────────────┐
│ Demo / 宿主 App │
│ ViewController → RDReaderControllerdemo 级路由控制器) │
└───────────────────────────┬─────────────────────────────┘
┌───────────────────────────▼─────────────────────────────┐
│ EPUBUI 层library 级读者 UI
│ RDEPUBReaderController开箱即用入口
│ RDEPUBReaderConfiguration / Theme / Persistence │
│ TopToolView / BottomToolView / ChapterList / Settings │
│ RDEPUBTextContentView / RDEPUBWebContentView │
└───────────────────────────┬─────────────────────────────┘
┌───────────────────────────▼─────────────────────────────┐
│ 翻页容器层RDReaderView
│ RDReaderViewUIView统一翻页外壳
│ 4 种翻页模式pageCurl / horizontalScroll / │
│ verticalScroll / horizontalCoverScroll │
│ RDReaderFlowLayout / RDReaderContentCell │
│ RDReaderPageChildViewControllerpageCurl 页包装) │
└───────────────────────────┬─────────────────────────────┘
┌───────────────────────────▼─────────────────────────────┐
│ EPUBCore 层EPUB 引擎) │
│ │
│ Publication 层 │
│ RDEPUBParser+Archive / +Package / +TOC / …) │
│ RDEPUBPublication出版物聚合对象
│ RDEPUBModelsmetadata / manifest / spine 模型) │
│ RDEPUBReadingModelslocation / viewport / highlight
│ │
│ Services 层 │
│ RDEPUBResourceResolver资源 URL 统一入口) │
│ RDEPUBPreferences展示参数聚合
│ RDEPUBPaginator离屏分页服务
│ │
│ Navigator 层 │
│ RDEPUBReadingSession状态机 + 会话协调) │
│ RDEPUBNavigatorState状态枚举
│ RDEPUBNavigatorLayoutContext │
│ │
│ Resource View 层 │
│ RDEPUBWebView+Configuration / +Reflowable / │
│ +FixedLayout / +JavaScriptBridge
│ RDEPUBResourceURLSchemeHandlerss-reader:// 协议) │
│ RDEPUBStyleSheetBuilder / RDEPUBJavaScriptBridge │
│ RDEPUBFixedLayoutTemplate / RDEPUBAssetRepository │
│ RDEPUBRenderRequest │
└───────────────────────────┬─────────────────────────────┘
┌───────────────────────────▼─────────────────────────────┐
│ EPUBTextRendering 层(文本 EPUB 渲染) │
│ RDEPUBTextRenderer协议
│ RDEPUBDTCoreTextRendererDTCoreText 实现) │
│ RDEPUBTextRendererSupport / RDEPUBTextPaginationSupport │
│ RDEPUBTextBookBuilder │
└──────────────────────────────────────────────────────────┘
```
---
## 3. 翻页容器层RDReaderView
### 3.1 四种翻页模式
| 模式 | 实现方式 | 特点 |
|------|----------|------|
| `pageCurl` | UIPageViewController | 原生翻书效果,手势由系统提供 |
| `horizontalScroll` | UICollectionView + RDReaderFlowLayout | 每屏显示 2 项,水平分页滚动 |
| `verticalScroll` | UICollectionView + RDReaderFlowLayout | 全宽项目,垂直连续滚动 |
| `horizontalCoverScroll` | UICollectionView + RDReaderFlowLayout | 覆盖滚动效果Z 轴动画 |
### 3.2 数据源协议
```swift
public protocol RDReaderDataSource: NSObjectProtocol {
func pageCountOfReaderView(readerView: RDReaderView) -> Int
func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView
func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String?
@objc optional func topToolView(readerView: RDReaderView) -> UIView?
@objc optional func bottomToolView(readerView: RDReaderView) -> UIView?
}
```
### 3.3 手势分区scroll 模式)
屏幕水平三等分:
- **左 1/3**:上一页
- **中 1/3**:显示 / 隐藏工具栏
- **右 1/3**:下一页
### 3.4 翻页模式切换
`switchReaderDisplayType(_ type:)` 会完全销毁并重建底层 viewpageViewController 或 collectionView然后重新加载数据。
---
## 4. EPUB 引擎层EPUBCore
### 4.1 Publication 层:解析与聚合
**主链路:**
```
epubURL
→ RDEPUBParser.parse(epubURL:)
→ extractArchive # ZIP 解压到沙盒临时目录
→ parseContainerXML # 定位 OPF 路径
→ parseOPF # 解析 metadata / manifest / spine
→ parseTOC # 解析 NCX 或 Nav 目录
→ RDEPUBPublication(parser:) # 聚合解析结果,挂载 resourceResolver
```
**RDEPUBPublication 暴露的能力:**
| 属性 / 方法 | 说明 |
|------------|------|
| `metadata` | 书名、作者、语言、layout 等 |
| `manifest` | id → RDEPUBManifestItem 映射 |
| `spine` | 有序 spine 列表(包含 href |
| `tableOfContents` | 树形目录 |
| `layout` | `.reflowable``.fixed` |
| `readingProfile` | `.webInteractive` / `.webFixedLayout` / `.textReflowable` |
| `resourceResolver` | 统一资源 URL 解析入口 |
| `fixedLayoutSpreadEnabled(for:viewportSize:)` | 判断是否启用双页 spread |
| `makeFixedSpreads(preferences:viewportSize:)` | 生成 fixed spread 页模型 |
**readingProfile 判定逻辑:**
```
layout == .fixed → webFixedLayout
layout == .reflowable + 含交互脚本 → webInteractive
layout == .reflowable + 无交互脚本 → textReflowable
```
### 4.2 Services 层
#### RDEPUBResourceResolver
统一处理所有资源路径转换,是 WebView 和 Paginator 访问资源的唯一入口:
| 方法 | 说明 |
|------|------|
| `fileURL(forHref:)` | href → 本地文件 URL |
| `schemeURL(forHref:)` | href → `ss-reader://` 协议 URL |
| `href(forSpineIndex:)` | spineIndex → href |
| `normalizedHref(_:)` | 相对路径标准化(统一相对 OPF |
#### RDEPUBPaginator
使用隐藏的 `WKWebView` 离屏加载每个 spine 资源,通过 JS 注入分页 CSS回调每个资源的页数
```swift
paginator.calculate(publication: publication, preferences: preferences, viewportSize: size) { pageCounts in
// pageCounts[i] = spine[i] 的页数
}
```
#### RDEPUBPreferences
聚合 WebView 和 Paginator 共用的展示参数字号、行距、主题色、边距、布局适配模式、spread 模式。
### 4.3 Navigator 层
#### RDEPUBNavigatorState 状态机
```
initializing → loading → idle ←→ jumping
←→ moving
←→ repaginating
```
| 状态 | 含义 | 允许动作 |
|------|------|----------|
| `initializing` | 刚打开书籍 | 接收初始恢复位置 |
| `loading` | 生成首轮 page model | 缓存 pending navigation |
| `idle` | 显示稳定 | 应用 staged snapshot、处理跳转、保存位置 |
| `jumping` | 目录 / 内部链接跳转中 | 重放 pending location |
| `moving` | 用户翻页中 | 更新当前 pageNum |
| `repaginating` | 重新分页中(字号/横竖屏变化) | 缓存当前位置,等待完成 |
#### RDEPUBReadingSession
会话级协调者,持有:
- `publication`:出版物只读视图
- `activePages / activeChapters`:当前展示的页模型和章节信息
- `stagedPages / stagedChapters`:后台分页完成后暂存,等待 idle 状态时应用
- `pendingNavigationLocation / pendingNavigationPageNum`:等待当前加载完成后再执行的跳转请求
- `currentViewport / currentReadingContext`:当前可见区域的位置信息
关键操作:
```swift
session.stageSnapshot(snapshot, restoreLocation:) // 后台分页完成,暂存结果
session.consumeStagedSnapshotIfAllowed() // idle 时消费暂存(线程安全切换)
session.transition(to: .idle) // 状态跃迁
session.clearPendingNavigation() // 取消待执行跳转
```
### 4.4 Resource View 层
#### RDEPUBWebView
承载单个 spine 资源的 WKWebView按职责拆分为 4 个扩展文件:
| 扩展 | 职责 |
|------|------|
| `+Configuration` | WKWebViewConfiguration、schemeHandler 注册、user scripts |
| `+Reflowable` | 注入分页 CSS、滚动到指定 progression、接收 JS 事件 |
| `+FixedLayout` | fixed-layout HTML wrapper 生成和加载 |
| `+JavaScriptBridge` | JS ↔ Swift 消息路由、选区、高亮、进度上报 |
#### ss-reader:// 协议
所有 spine 资源XHTML、CSS、图片、字体统一通过 `ss-reader://book/<relative-path>` 访问,由 `RDEPUBResourceURLSchemeHandler` 从本地解压目录读取并返回。
优点:
- 相对资源路径在 WebView 中自然解析
- fixed-layout iframe 不再白屏
- 无需 `allowingReadAccessTo` 路径权限
---
## 5. 文本 EPUB 渲染层EPUBTextRendering
适用于 `readingProfile == .textReflowable` 的书籍(纯文本小说类 EPUB2
### 5.1 渲染链路
```
章节 HTML 文件
→ RDEPUBTextRendererSupport.injectFragmentMarkers(into:) # 注入 fragment 标记
→ RDEPUBDTCoreTextRenderer.renderChapter(html:baseURL:style:)
→ DTHTMLAttributedStringBuilder # HTML → NSAttributedString
→ extractFragmentOffsets # fragment → 字符偏移量映射
→ normalizeReadingAttributes # 统一字体/行距
→ RDEPUBRenderedChapterContent
.attributedString # 渲染后的富文本
.fragmentOffsets # fragment id → 字符偏移
```
### 5.2 分页
```
RDEPUBTextBookBuilder.buildBook(publication:style:pageSize:renderer:)
→ 逐章节调用 renderer.renderChapter(...)
→ RDEPUBTextPaginationSupport.paginate(attributedString:pageSize:)
→ RDEPUBTextBook章节 + 页模型 + fragment 索引)
```
### 5.3 内容显示
`RDEPUBTextContentView` 基于 `NSAttributedString` + `UITextView`(或 DTCoreText 自定义绘制),实现:
- 每页显示对应字符范围的内容
- 高亮重叠渲染
- 支持按 fragment 偏移量跳转
---
## 6. EPUBUI 层(开箱即用读者 UI
`RDEPUBReaderController` 是 library 层提供的完整读者入口,调用方只需传入 EPUB 文件 URL
```swift
let controller = RDEPUBReaderController(
epubURL: url,
configuration: .default,
persistence: RDEPUBReaderPersistence(storageKey: "my-book")
)
controller.delegate = self
present(controller, animated: true)
```
### 6.1 内置能力
| 能力 | 对应文件 |
|------|----------|
| 顶部工具栏(书名、返回、目录、高亮入口) | RDEPUBReaderTopToolView |
| 底部工具栏(进度条、页码) | RDEPUBReaderBottomToolView |
| 目录面板 | RDEPUBReaderChapterListController |
| 高亮管理 | RDEPUBReaderHighlightsViewController |
| 设置面板(字号、行距、主题、翻页模式) | RDEPUBReaderSettingsViewController |
| 阅读位置持久化 | RDEPUBReaderPersistence |
### 6.2 配置项RDEPUBReaderConfiguration
| 属性 | 默认值 | 说明 |
|------|--------|------|
| `fontSize` | 15 | 字号pt |
| `lineHeightMultiple` | 1.6 | 行距倍数 |
| `displayType` | `.pageCurl` | 翻页模式 |
| `landscapeDualPageEnabled` | `true` | 横屏双页 |
| `showsTableOfContents` | `true` | 是否显示目录入口 |
| `allowsHighlights` | `true` | 是否显示高亮入口 |
| `showsSettingsPanel` | `true` | 是否显示设置入口 |
| `theme` | `.light` | 主题 |
| `fixedLayoutSpreadMode` | `.automatic` | fixed-layout spread 模式 |
| `textRenderingEngine` | `.dtCoreText` | 文本 EPUB 渲染引擎 |
---
## 7. 关键数据流
### 7.1 打开 EPUB 书籍
```
RDEPUBReaderController.init(epubURL:)
→ viewDidLoad
→ RDEPUBParser.parse(epubURL:)
→ RDEPUBPublication(parser:)
→ RDEPUBReadingSession(publication:)
→ readingProfile 判断渲染路径
textReflowable:
RDEPUBTextBookBuilder.buildBook(...) # 后台分页
→ session.stageSnapshot(...)
→ session.consumeStagedSnapshotIfAllowed() # idle 时应用
→ readerView.reloadData()
webInteractive / webFixedLayout:
RDEPUBPaginator.calculate(...) # 离屏 WebView 分页
→ session.stageSnapshot(...)
→ readerView.reloadData()
→ persistence.restoreLocation() # 恢复上次阅读位置
```
### 7.2 翻页
```
用户手势tap / swipe
→ RDReaderView 检测手势分区 / 翻页方向
→ delegate.pageNum(readerView:pageNum:)
→ RDEPUBReaderController 根据 pageNum 找 EPUBPage
→ RDEPUBReadingSession.transition(to: .moving)
→ readerView.pageContentView(pageNum:) 回调
→ 创建 RDEPUBWebContentView 或 RDEPUBTextContentView
→ loadPage(spineIndex: pageIndexInChapter: preferences:)
→ JS bridge 上报 progression → session.currentViewport 更新
```
### 7.3 定位模型RDEPUBLocation
EPUB 是可重排内容,字号 / 横竖屏变化会使页号失效。定位模型使用 `href + progression`
```swift
struct RDEPUBLocation: Codable, Equatable {
var bookIdentifier: String? // 隔离不同书的进度
var href: String // OPF 相对路径(对应 spine 资源)
var progression: Double // 视口起始位置 [0, 1]
var lastProgression: Double? // 视口末尾位置 [0, 1]
var fragment: String? // 锚点
}
```
**恢复流程:**
1. 标准化 `href`(相对 OPF
2. 找到对应 spineIndex
3.`navigationProgression` 估算扁平页号
4. 若有 `fragment`,加载后在 WebView 中锚点滚动
---
## 8. 已知限制与后续待办
| 问题 | 说明 |
|------|------|
| Demo 层 RDReaderController 过大 | 约 1900+ 行,仍混有 EPUB 加载、UI 管理、数据源逻辑,待进一步拆分 |
| RDEPUBParser.swift 仍偏大 | 约 900 行虽已拆出多个扩展文件OPF metadata 细节仍集中 |
| 横竖屏切换已接入一阶段支持 | `RDEPUBReaderController.viewWillTransition()` 统一接管正文重分页,`RDReaderView` 保留容器级双页布局刷新;仍需补齐固定回归矩阵 |
| 固定手势分区比例 | 三等分固定写死,不支持自定义 |
| 自动化测试为首批接入状态 | 已有 `ReadViewSDKDemoTests` / `ReadViewSDKDemoUITests` 与 parser / resolver / persistence / smoke 用例,分页、恢复链路和更多 UI 闭环仍需继续补齐 |
| 基线验证待跑 | 四本样书(凡人修仙传、爱忘事熊爷爷、宝山辽墓、张学良传)的分页耗时、目录命中率、末页事件等数据尚未收集 |
---
## 9. 构建与依赖
### 9.1 安装
```bash
cd RDReaderDemo
pod install
# 打开 RDReaderDemo.xcworkspace选 RDReaderDemo scheme构建
```
### 9.2 主要依赖
| 依赖 | 用途 | 使用层 |
|------|------|--------|
| ZIPFoundation | EPUB ZIP 解压 | RDEPUBParser+Archive |
| DTCoreText | HTML → NSAttributedString | RDEPUBDTCoreTextRenderer |
| SnapKit | Demo 布局 | Demo 层 |
| SSAlertSwift | Demo 弹窗 | Demo 层 |
### 9.3 podspec 关键配置
- `s.source_files = 'Sources/RDReaderView/**/*.{swift}'`(递归包含所有子目录)
- `s.resource_bundles`:包含 JS、CSS 等资源文件
- Library 本身无需 demo 层依赖