动态分页(基于 DOM 测量,随布局重排)
上级:key-designs
它基于 DOM 测量,而不是字符预算。 阅读器把每一个候选页渲染进一个离屏节点,读取真实的 scrollHeight 来判断是否溢出——于是页面边界是当前字体加视口下实际渲染出来的文本的函数,只要这两者变化,边界就跟着变。这正是"动态"二字的来源。
探针(application-services/streamingProbe/)。一个 StreamingProbe 接口:appendAndCheck(sentence) 把一句话压入候选页并渲染,溢出时返回 true;另有 reset(seed)/clear()。背后有两套实现(由一个 facade 按书籍大小/设备路由):
domStreamingProbe(网页端,主线程):把已分类的段落flushSync进一个.reader-probe容器;判定是否合适的准则是scrollHeight <= clientHeight - PROBE_PADDING_PX(64)。每页刻意采用 O(N) 次测量(取代了更早的二分查找),并配合一个 50 ms 时间片的macrotaskYield,使整本书的分页不会卡死阅读器。pretextStreamingProbe(Web Worker,主线程之外):通过@chenglou/pretext测量,用于移动端/大书场景,那里逐句flushSync会造成卡顿。
分配 pageNumber。 usecases/paginate.ts 是一个异步生成器,逐句驱动探针;一旦溢出,就把恰好放得下的范围提交,并用溢出的那一句重新起页,给每个 CachedSentence 打上 sentenceIdx 与 pageNumber。有三种断页情形:章节标题硬断、普通溢出断页,以及"单句就比一页还高"时的进度保证。
章节断页不经过测量、强制断开:paginationChapterBreaks.collectChapterBreaks 合并 TOC 与 NCX/PDF 大纲的偏移量(把间距小于 200 字符/5 句的簇合并),页中出现章节标题句时,本页在它之前结束。
动态循环(重排)。 usePaginationCache 监视 {fontSize, fontFamily, pageWidth, pageHeight, contentHash}(视口来自一个 ResizeObserver),做防抖(首次 50 ms,之后 300 ms),并由 pickComputeFn 分派:
- 缓存就绪且
LayoutKey匹配 → 空操作; - 缓存就绪但布局不同 →
repaginate——复用句子文本/sanitize/LCS 的结果,只重跑探针以重新分配pageNumber(这是字体/窗口大小变化时的重排路径); - 无缓存/版本失效 → 全量计算(sanitize + raw↔display LCS + 探针)。
持久化的
LayoutKey {fontSize, pageWidth, pageHeight, fontFamily}会在每次打开时被比对。
版本号管理。 PAGINATION_VERSION(当前为 7,有一段递增历史)被刻意与 SANITIZER_VERSION 分开,这样一次布局逻辑的改动只会强制重新分页,不会连带重建代价高昂的内容缓存。
Page 是瞬时的(entities/Page.ts):按需从一段 pageNumber 相同的连续切片构建;内存中只保留当前页加上下各 10 页的窗口;偏移量的换算依靠每个句子上的 displayToRaw(raw-display-cursor)。