2026-09-23·by Sijie Wang#lucerna#software#architecture

dynamic-pagination

动态分页(基于 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 打上 sentenceIdxpageNumber。有三种断页情形:章节标题硬断、普通溢出断页,以及"单句就比一页还高"时的进度保证。 章节断页不经过测量、强制断开:paginationChapterBreaks.collectChapterBreaks 合并 TOC 与 NCX/PDF 大纲的偏移量(把间距小于 200 字符/5 句的簇合并),页中出现章节标题句时,本页在它之前结束。

动态循环(重排)。 usePaginationCache 监视 {fontSize, fontFamily, pageWidth, pageHeight, contentHash}(视口来自一个 ResizeObserver),做防抖(首次 50 ms,之后 300 ms),并由 pickComputeFn 分派:

  1. 缓存就绪且 LayoutKey 匹配 → 空操作
  2. 缓存就绪但布局不同 → repaginate——复用句子文本/sanitize/LCS 的结果,重跑探针以重新分配 pageNumber(这是字体/窗口大小变化时的重排路径);
  3. 无缓存/版本失效 → 全量计算(sanitize + raw↔display LCS + 探针)。 持久化的 LayoutKey {fontSize, pageWidth, pageHeight, fontFamily} 会在每次打开时被比对。

版本号管理。 PAGINATION_VERSION(当前为 7,有一段递增历史)被刻意与 SANITIZER_VERSION 分开,这样一次布局逻辑的改动只会强制重新分页,不会连带重建代价高昂的内容缓存。

Page 是瞬时的entities/Page.ts):按需从一段 pageNumber 相同的连续切片构建;内存中只保留当前页加上下各 10 页的窗口;偏移量的换算依靠每个句子上的 displayToRawraw-display-cursor)。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →