- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
22 KiB
EPUBCore 模块代码级参考文档
最后更新:2026-06-18
1. 模块概述
EPUBCore 是 ReadViewSDK 的基础层,负责 EPUB 文件解析、资源管理、WebView 渲染、JavaScript 桥接、CFI 定位、搜索和分页计算。它是上层 EPUBTextRendering 和 EPUBUI 的数据提供者。
文件清单(~50 个 Swift 文件):
| 子系统 | 核心文件 | 职责 |
|---|---|---|
| 解析器 | RDEPUBParser*.swift |
EPUB 解压、OPF 解析、TOC 解析、资源提取 |
| 数据模型 | RDEPUBModels.swift |
Metadata、ManifestItem、SpineItem、TOC 等基础模型 |
| Publication | RDEPUBPublication.swift |
出版物抽象,聚合 parser 和 resourceResolver |
| 阅读会话 | RDEPUBReadingSession.swift |
管理分页快照、阅读位置、待导航状态 |
| WebView | RDEPUBWebView*.swift |
WKWebView 封装,支持 Reflowable 和 Fixed Layout |
| JS 桥接 | RDEPUBJavaScriptBridge.swift |
JavaScript 消息协议和脚本注入 |
| 资源解析 | RDEPUBResourceResolver.swift |
href 归一化、文件路径解析 |
| URL 方案 | RDEPUBResourceURLSchemeHandler.swift |
自定义 ss-reader:// 协议处理 |
| 分页器 | RDEPUBPaginator.swift |
WebView 分页计算(按章测量页数) |
| CFI | CFI/RDEPUBCFI*.swift |
EPUB CFI 解析、生成、序列化、恢复 |
| 搜索 | RDEPUBSearchEngine.swift |
HTML 全文搜索 |
| 样式 | RDEPUBStyleSheetBuilder.swift |
CSS 注入与分页样式生成 |
| 偏好 | RDEPUBPreferences.swift |
阅读偏好设置模型 |
| 资源文件 | RDEPUBAssetRepository.swift |
JS/CSS/HTML 资源文件加载 |
| 笔记 | Notes/RDEPUBNote*.swift |
脚注/尾注检测与解析 |
2. 解析器子系统
2.1 RDEPUBParser
文件: RDEPUBParser.swift + 扩展文件
EPUB 文件解析的核心类,负责从 .epub 文件中提取结构化数据。
public final class RDEPUBParser {
public internal(set) var metadata: RDEPUBMetadata // 书籍元数据
public internal(set) var manifest: [String: RDEPUBManifestItem] // 资源清单
public internal(set) var spine: [RDEPUBSpineItem] // 阅读顺序
public internal(set) var tableOfContents: [EPUBTableOfContentsItem] // 目录
public internal(set) var extractionRootURL: URL? // 解压根目录
public internal(set) var opfURL: URL? // OPF 文件路径
public var opfDirectoryURL: URL? // OPF 所在目录
/// 解析 EPUB 文件(解压 + 解析 container.xml + 解析 OPF)
public func parse(epubURL: URL) throws
/// 直接解析 OPF 文件
public func parseOPF(at opfURL: URL) throws
/// 解析 Navigation Document 或 NCX
public func parseTOC() -> [EPUBTableOfContentsItem]
/// 从 parser 创建 Publication
public func makePublication() -> RDEPUBPublication
}
扩展文件职责:
| 文件 | 职责 |
|---|---|
RDEPUBParser+Archive.swift |
ZIP 解压(ZIPFoundation),提取到 Caches 目录 |
RDEPUBParser+Package.swift |
OPF XML SAX 解析(metadata/manifest/spine) |
RDEPUBParser+TOC.swift |
Navigation Document 和 NCX 解析 |
RDEPUBParser+Resources.swift |
资源文件读取(HTML 字符串、文件 URL) |
RDEPUBParser+ReadingProfile.swift |
阅读配置文件检测 |
2.2 RDEPUBParserError
public enum RDEPUBParserError: LocalizedError {
case archiveOpenFailed(URL)
case missingContainerXML
case missingRootFile
case invalidRootFilePath(String)
case invalidXML(URL)
case missingManifestItem(idref: String)
case emptySpine
case invalidArchiveEntryPath(String)
}
3. 数据模型
3.1 RDEPUBMetadata
文件: RDEPUBModels.swift
public struct RDEPUBMetadata: Codable, Equatable {
public var identifier: String? // ISBN/UUID
public var title: String // 书名
public var author: String? // 作者
public var language: String? // 语言
public var version: String? // EPUB 版本
public var layout: RDEPUBLayout // reflowable / fixed
public var spread: String? // none / auto
public var readingProgression: RDEPUBReadingProgression // ltr / rtl / auto
}
3.2 RDEPUBManifestItem
public struct RDEPUBManifestItem: Codable, Equatable {
public var id: String // 资源 ID
public var href: String // 相对路径
public var mediaType: String // MIME 类型
public var properties: [String] // 属性(nav, mathml, svg 等)
public var fallback: String? // 回退资源 ID
public var mediaOverlay: String? // 媒体叠加 ID
public var title: String? // 标题
public var isNavigationDocument: Bool // 是否为 Navigation Document
public var isNCX: Bool // 是否为 NCX 文件
}
3.3 RDEPUBSpineItem
public struct RDEPUBSpineItem: Codable, Equatable {
public var idref: String // 引用 manifest ID
public var href: String // 解析后的相对路径
public var mediaType: String // MIME 类型
public var title: String // 章节标题
public var linear: Bool // 是否为线性内容
public var properties: [String] // 属性(page-spread-left/right)
public var pageSpread: RDEPUBPageSpread? // 页面位置
public var layout: RDEPUBLayout? // 章节级布局覆盖
}
3.4 EPUBTableOfContentsItem
public struct EPUBTableOfContentsItem: Codable, Equatable {
public var title: String // 目录标题
public var href: String // 链接地址
public var children: [EPUBTableOfContentsItem] // 子目录(支持多级)
}
3.5 枚举类型
public enum RDEPUBLayout: String, Codable {
case reflowable // 文本重排
case fixed // 固定布局
}
public enum RDEPUBReadingProfile: String, Codable {
case webInteractive // WebView 交互模式(原 EPUB 渲染)
case webFixedLayout // WebView 固定布局
case textReflowable // 原生文本渲染(DTCoreText)
}
public enum RDEPUBReadingProgression: String, Codable {
case ltr, rtl, auto
}
public enum RDEPUBPageSpread: String, Codable {
case left, right, center
}
4. Publication
文件: RDEPUBPublication.swift
聚合 RDEPUBParser 和 RDEPUBResourceResolver,提供统一的出版物访问接口。
public final class RDEPUBPublication {
public let parser: RDEPUBParser
public let resourceResolver: RDEPUBResourceResolver
public var metadata: RDEPUBMetadata
public var manifest: [String: RDEPUBManifestItem]
public var spine: [RDEPUBSpineItem]
public var tableOfContents: [EPUBTableOfContentsItem]
public var layout: RDEPUBLayout
public var readingProfile: RDEPUBReadingProfile
public var readingProgression: RDEPUBReadingProgression
public var bookIdentifier: String?
/// 判断 Fixed Layout 是否启用双页展开
public func fixedLayoutSpreadEnabled(for preferences: RDEPUBPreferences, viewportSize: CGSize) -> Bool
/// 生成 Fixed Layout 的 Spread 列表
public func makeFixedSpreads(preferences: RDEPUBPreferences, viewportSize: CGSize) -> [EPUBFixedSpread]
}
5. 阅读会话
文件: RDEPUBReadingSession.swift
管理阅读过程中的分页快照、阅读位置和待导航状态。
public final class RDEPUBReadingSession {
public typealias PaginationSnapshot = (pages: [EPUBPage], chapters: [EPUBChapterInfo])
public let publication: RDEPUBPublication
public private(set) var navigatorState: RDEPUBNavigatorState
public private(set) var activePages: [EPUBPage] // 当前活跃页列表
public private(set) var activeChapters: [EPUBChapterInfo] // 当前活跃章节列表
public private(set) var stagedPages: [EPUBPage]? // 暂存页列表(待应用)
public private(set) var stagedChapters: [EPUBChapterInfo]?
public private(set) var currentViewport: RDEPUBViewport?
public private(set) var currentReadingContext: RDEPUBReadingContext?
// 状态管理
public func transition(to state: RDEPUBNavigatorState)
public func resetRuntimeState()
// 快照管理
public func setActiveSnapshot(_ snapshot: PaginationSnapshot)
public func stageSnapshot(_ snapshot: PaginationSnapshot, restoreLocation: RDEPUBLocation?)
public func consumeStagedSnapshotIfAllowed() -> (snapshot: PaginationSnapshot, restoreLocation: RDEPUBLocation?)?
// 导航
public func queueNavigation(to location: RDEPUBLocation, ...) -> Int?
public func updateReadingContext(pageNumber:location:spineIndex:chapterIndex:bookIdentifier:)
public func currentReadingLocation(bookIdentifier: String?) -> RDEPUBLocation?
// 分页快照生成
public func makePaginationSnapshot(pageCounts:preferences:layoutContext:) -> PaginationSnapshot
}
5.1 RDEPUBNavigatorState
public enum RDEPUBNavigatorState: String, Codable {
case initializing // 初始化中
case loading // 加载中
case idle // 空闲(可应用快照)
case jumping // 跳转中
case moving // 翻页中
case repaginating // 重新分页中
public var isStableForSnapshotApplication: Bool // 仅 idle 状态为 true
}
6. WebView 子系统
6.1 RDEPUBWebView
文件: RDEPUBWebView.swift + 扩展文件
UIView 子类,内部封装 WKWebView,支持 Reflowable 和 Fixed Layout 两种渲染模式。
public final class RDEPUBWebView: UIView {
public weak var delegate: RDEPUBWebViewDelegate?
public var onRendered: (() -> Void)?
var publication: RDEPUBPublication?
var currentRenderRequest: RDEPUBRenderRequest?
var webView: WKWebView?
var schemeHandler: RDEPUBResourceURLSchemeHandler?
var currentSpineIndex: Int
var currentHref: String
var currentPageIndex: Int
var currentTotalPagesInChapter: Int
var viewportSize: CGSize
var currentFontSize: CGFloat
var currentLineHeightMultiple: CGFloat
var targetLocation: RDEPUBLocation?
var pendingHighlights: [RDEPUBHighlight]
}
扩展文件:
| 文件 | 职责 |
|---|---|
RDEPUBWebView+Configuration.swift |
WKWebView 配置(scheme handler、user script) |
RDEPUBWebView+Reflowable.swift |
Reflowable 模式渲染逻辑 |
RDEPUBWebView+FixedLayout.swift |
Fixed Layout 模式渲染逻辑 |
RDEPUBWebView+JavaScriptBridge.swift |
JS 消息接收与处理 |
RDEPUBWebView+Search.swift |
搜索高亮渲染 |
6.2 RDEPUBWebViewDelegate
public protocol RDEPUBWebViewDelegate: AnyObject {
func epubWebView(_ webView: RDEPUBWebView, didUpdateLocation location: RDEPUBLocation, spineIndex: Int)
func epubWebView(_ webView: RDEPUBWebView, didChangeSelection selection: RDEPUBSelection?, spineIndex: Int)
func epubWebView(_ webView: RDEPUBWebView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction)
func epubWebView(_ webView: RDEPUBWebView, didActivateInternalLink location: RDEPUBLocation, fromSpineIndex: Int)
func epubWebView(_ webView: RDEPUBWebView, didActivateExternalLink url: URL)
func epubWebView(_ webView: RDEPUBWebView, didLogJavaScriptError message: String)
func epubWebViewDidFinishRendering(_ webView: RDEPUBWebView)
}
7. JavaScript 桥接
文件: RDEPUBJavaScriptBridge.swift
定义 Native ↔ WebView 的消息协议和脚本注入。
7.1 消息类型
enum RDEPUBJavaScriptBridgeMessage: String, CaseIterable {
case progressionChanged = "ssReaderProgressionChanged" // 阅读进度变化
case selectionChanged = "ssReaderSelectionChanged" // 文本选择变化
case internalLink = "ssReaderInternalLink" // 内部链接点击
case externalLink = "ssReaderExternalLink" // 外部链接点击
case javaScriptError = "ssReaderJSError" // JS 错误
case fixedLayoutReady = "ssReaderFixedLayoutReady" // Fixed Layout 就绪
}
7.2 注入的 JS 资源
rangy-core.js— 范围与选择 APIrangy-serializer.js— 选择序列化cssInjector.js— CSS 注入WeReadApi.js— 阅读器 API(分页、跳转、高亮)epub-bridge.js— 消息桥接
7.3 关键方法
enum RDEPUBJavaScriptBridge {
static var messageNames: [String] // 所有消息名称
static var userScript: String // 注入的 User Script
/// 生成 Reflowable 渲染脚本
static func applyPresentationScript(for request: RDEPUBReflowableRenderRequest) -> String
/// 生成 Fixed Layout 渲染脚本
static func applyFixedPresentationScript(for request: RDEPUBFixedRenderRequest) -> String
}
8. 资源解析
文件: RDEPUBResourceResolver.swift
负责 href 归一化、文件路径解析和 spine 索引查找。
public final class RDEPUBResourceResolver {
public var opfDirectoryURL: URL?
/// 将相对路径转为文件 URL
public func fileURL(forRelativePath relativePath: String) -> URL?
/// 将相对路径转为 ss-reader:// 资源 URL
public func resourceURL(forRelativePath relativePath: String) -> URL?
/// href 归一化(去除 fragment,解析相对路径)
public func normalizedHref(_ href: String, relativeToSpineIndex spineIndex: Int? = nil) -> String?
public func normalizedHref(_ href: String, relativeToHref baseHref: String) -> String?
/// 位置归一化(合并 href + fragment + progression)
public func normalizedLocation(_ location: RDEPUBLocation, relativeToSpineIndex spineIndex: Int?, bookIdentifier: String?) -> RDEPUBLocation?
/// 根据 href 查找 spine 索引
public func spineIndex(forNormalizedHref normalizedHref: String) -> Int?
public func spineIndex(for location: RDEPUBLocation) -> Int?
/// 获取 spine 项的 href 和 title
public func href(forSpineIndex spineIndex: Int) -> String?
public func title(forSpineIndex spineIndex: Int) -> String?
}
9. URL 方案处理器
文件: RDEPUBResourceURLSchemeHandler.swift
实现 WKURLSchemeHandler,拦截 ss-reader://book/ 请求,从 EPUB 解压目录流式读取资源文件。
public final class RDEPUBResourceURLSchemeHandler: NSObject, WKURLSchemeHandler {
public static let scheme = "ss-reader"
public static let host = "book"
public init(parser: RDEPUBParser)
// WKURLSchemeHandler
public func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask)
public func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask)
}
10. 分页器
文件: RDEPUBPaginator.swift
使用隐藏的 WKWebView 逐章加载 HTML,通过 JS 测量每章的页数。
public final class RDEPUBPaginator: NSObject {
/// 计算所有章节的页数
public func calculate(
parser: RDEPUBParser,
hostingView: UIView,
presentation: RDEPUBPresentationStyle,
completion: @escaping ([Int]) -> Void
)
/// 计算单个章节的页数
public func calculateSingleChapter(
parser: RDEPUBParser,
spineIndex: Int,
hostingView: UIView,
presentation: RDEPUBPresentationStyle,
completion: @escaping (Int) -> Void
)
}
11. 搜索引擎
文件: RDEPUBSearchEngine.swift
protocol RDEPUBSearchEngine {
func search(keyword: String) -> [RDEPUBSearchMatch]
}
RDEPUBHTMLSearchEngine
基于 HTML 的搜索引擎,逐章加载 HTML → 转为纯文本 → 关键词匹配。
final class RDEPUBHTMLSearchEngine: RDEPUBSearchEngine {
init(parser: RDEPUBParser, publication: RDEPUBPublication)
func search(keyword: String) -> [RDEPUBSearchMatch]
}
RDEPUBTextSearchEngine(EPUBTextRendering 模块)
基于原生文本的搜索引擎,在 NSAttributedString 上直接搜索,性能更好。
final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
init(textBook: RDEPUBTextBook, publication: RDEPUBPublication)
func search(keyword: String) -> [RDEPUBSearchMatch]
}
12. CFI 子系统
文件: CFI/RDEPUBCFI*.swift(13 个文件)
实现 EPUB CFI(Canonical Fragment Identifier)标准,用于精确定位 EPUB 内容。
| 文件 | 职责 |
|---|---|
RDEPUBCFI.swift |
CFI 值类型,核心解析入口 |
RDEPUBCFIParser.swift |
CFI 字符串解析器 |
RDEPUBCFISerializer.swift |
CFI 序列化为字符串 |
RDEPUBCFIPath.swift |
CFI 路径节点 |
RDEPUBCFIRange.swift |
CFI 范围(起止点) |
RDEPUBCFIResolver.swift |
CFI → DOM 位置解析 |
RDEPUBCFIGenerator.swift |
DOM 位置 → CFI 生成 |
RDEPUBCFIMap.swift |
章节级 CFI 映射表 |
RDEPUBCFIDOMPathBuilder.swift |
DOM 路径构建 |
RDEPUBCFITextAssertion.swift |
文本断言(偏移校验) |
RDEPUBCFIRecoveryEngine.swift |
CFI 恢复引擎(节点变化后重新定位) |
RDEPUBCFICompatibility.swift |
CFI 兼容性处理 |
RDEPUBCFIError.swift |
错误类型 |
13. 样式构建
文件: RDEPUBStyleSheetBuilder.swift
public enum RDEPUBStyleSheetBuilder {
/// 向 HTML 注入分页 CSS(用于 Paginator 测量)
public static func injectPaginationCSS(into html: String, presentation: RDEPUBPresentationStyle) -> String
/// 生成渲染用 CSS(viewport 尺寸、字体、行高、主题色)
public static func renderCSS(for presentation: RDEPUBPresentationStyle) -> String
}
14. 偏好设置
文件: RDEPUBPreferences.swift
public struct RDEPUBPreferences: Equatable {
public var fontSize: CGFloat // 字体大小
public var lineHeightMultiple: CGFloat // 行高倍数
public var reflowableContentInsets: UIEdgeInsets // Reflowable 内容边距
public var fixedContentInset: UIEdgeInsets // Fixed Layout 内容边距
public var themeBackgroundColor: String? // 主题背景色(CSS)
public var themeTextColor: String? // 主题文字色(CSS)
public var fixedBackgroundColor: String? // Fixed 背景色
public var fixedLayoutFit: RDEPUBFixedLayoutFit // Fixed 适配模式
public var fixedLayoutSpreadMode: RDEPUBFixedLayoutSpreadMode // 双页模式
public var numberOfColumns: Int // 列数
public var columnGap: CGFloat // 列间距
/// 生成 PresentationStyle
public func presentationStyle(viewportSize: CGSize) -> RDEPUBPresentationStyle
/// 生成渲染请求
public func renderRequest(for page: EPUBPage, publication: RDEPUBPublication, viewportSize: CGSize, ...) -> RDEPUBRenderRequest?
}
15. 渲染请求模型
文件: RDEPUBRenderRequest.swift
public enum RDEPUBRenderRequest: Equatable {
case reflowable(RDEPUBReflowableRenderRequest)
case fixed(RDEPUBFixedRenderRequest)
public var isFixedLayout: Bool
public var primarySpineIndex: Int
public var primaryHref: String
}
public struct RDEPUBReflowableRenderRequest: Equatable {
public var spineIndex: Int
public var href: String
public var pageIndex: Int
public var totalPagesInChapter: Int
public var presentation: RDEPUBPresentationStyle
public var targetLocation: RDEPUBLocation?
public var highlights: [RDEPUBHighlight]
public var searchPresentation: RDEPUBSearchPresentation?
}
public struct RDEPUBFixedRenderRequest: Equatable {
public var spread: EPUBFixedSpread
public var viewportSize: CGSize
public var contentInset: UIEdgeInsets
public var fit: RDEPUBFixedLayoutFit
}
RDEPUBPresentationStyle
public struct RDEPUBPresentationStyle: Equatable {
public var viewportSize: CGSize
public var contentInsets: UIEdgeInsets
public var fontSize: CGFloat
public var lineHeightMultiple: CGFloat
public var numberOfColumns: Int
public var columnGap: CGFloat
public var themeBackgroundColor: String?
public var themeTextColor: String?
}
16. 资源仓库
文件: RDEPUBAssetRepository.swift
管理 SDK 内置的 JS/CSS/HTML 资源文件加载。
enum RDEPUBAsset: String {
case rangyCoreScript, rangySerializerScript, cssInjectorScript
case weReadAPIScript, bridgeScript, fixedLayoutTemplate
case wxReadDefaultCSS, wxReadReplaceCSS, wxReadDarkCSS, wxReadLatinReplaceCSS
}
enum RDEPUBAssetRepository {
/// 加载资源文件内容,支持模板替换
static func string(for asset: RDEPUBAsset, replacements: [String: String] = [:]) -> String
}
17. 笔记子系统
文件: Notes/RDEPUBNote*.swift
| 文件 | 职责 |
|---|---|
RDEPUBNoteModels.swift |
脚注/尾注数据模型 |
RDEPUBNoteDetector.swift |
从 HTML 中检测脚注标记 |
RDEPUBNoteResolver.swift |
解析脚注内容 |
18. 设计模式总结
| 模式 | 应用 |
|---|---|
| Builder 模式 | RDEPUBParser → RDEPUBPublication 构建链 |
| Strategy 模式 | RDEPUBSearchEngine 协议,HTML 和 Text 两种实现 |
| URL Scheme 拦截 | RDEPUBResourceURLSchemeHandler 自定义协议 |
| 消息桥接 | RDEPUBJavaScriptBridge Native ↔ JS 双向通信 |
| 快照管理 | RDEPUBReadingSession staged/active 双快照 |
| 状态机 | RDEPUBNavigatorState 管理导航状态 |