导读:本期聚焦于大卫创作的《如何使用 paged.js 构建可打印 HTML 的动态目录?》,敬请观看详情。网页直接打印时目录页码几乎必然错乱,因为浏览器分页与内容排版互相独立,手动维护页码又极其低效。paged.js 提供了另一条路:它在浏览器里重新实现了一套分页引擎,把 HTML 内容按 CSS 分页媒体规范切分到真实页面中,再通过官方插件在渲染完成后扫描标题元素、生成目录并自动回填页码。本文围绕这套方案展开,先介绍 paged.js 的核心原理与浏览器端渲染流程,再给出动态目录插件的完整实现代码,最后针对重复标题过滤、多层目录缩进、双次渲染对齐等常见问题给出可直接复用的解决办法,帮助你把长文档打印页码控制得和正式出版物一样精确。

把一份几十页的 HTML 报告导出成 PDF 时,最头疼的问题往往不是样式,而是目录。屏幕上看起来一切正常的锚点链接,一旦进入打印分页,页码就全乱了:某个章节实际落在第 12 页,目录里却还写着第 10 页。手动核对当然不现实,而 paged.js 恰好为这类需求提供了完整的解决方案——它在浏览器里模拟真实分页,并允许我们在分页完成后动态生成目录、回填真实页码。

如何使用 paged.js 构建可打印 HTML 的动态目录?

一、paged.js 的分页原理:为什么它能拿到真实页码

要理解动态目录的实现,得先弄清楚 paged.js 的工作方式。浏览器原生的 @page 规则只做很有限的支持,Chrome、Firefox 都不会真正把内容“切”到一个个页面盒子里,而是在打印预览时才临时分页。paged.js 的做法不同:它在 DOM 加载后拦截整个文档内容,创建一系列 <div class="pagedjs_page"> 作为页面容器,然后把原始内容逐块搬进这些页面,遇到放不下的元素就自动断页,并处理 break-beforebreak-after 等分页控制属性。

这个“先分页、后渲染”的过程带来一个关键优势:每个元素最终落在哪个页面里,在 DOM 结构中是可查询的。比如想知道某个 h2 标题的真实页码,只需要向上遍历它的祖先节点,找到最近的页面容器,读取该容器上的 data-page-number 属性即可。这是任何静态目录方案都做不到的。

另一个重要机制是生命周期钩子。paged.js 暴露了 Paged.Handler 类,允许注册 afterPageLayoutafterRendered 等回调。动态目录的核心思路就是:在所有页面渲染完成之后,扫描全文标题、收集页码、生成目录 DOM、插入文档,再触发一次重新渲染让目录本身也参与分页。

二、基础接入:让 HTML 先正确分页

接入 paged.js 非常简单,引入脚本后它会自动处理 @page 样式。需要注意分页媒体样式要写在普通样式之外,避免影响屏幕预览。

<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"></script>
<style>
  @page {
    size: A4;
    margin: 20mm 18mm;
    @bottom-center { content: counter(page); }
  }
  /* 只在打印分页时为标题强制起新页 */
  @media print {
    h1.chapter { break-before: page; }
  }
</style>
<script>
  window.PagedConfig = {
    auto: true,
    before: () => console.log('分页即将开始')
  };
</script>

上面这段代码定义了 A4 页面、页脚页码计数器,并让每个 h1.chapter 从新页开始。此时打开页面,就能看到内容已经被切分到一个个可视化的页面容器中,每个容器带有 data-page-number 属性。可以先在控制台执行 document.querySelectorAll('.pagedjs_page').length 验证页面数量是否符合预期。

有一个容易踩的坑:如果文档里使用了 position: fixed 的浮动元素或异步加载的图片,分页时高度计算会出错,导致页码偏移。解决办法是在 PagedConfig.before 回调里等待所有图片加载完成,或者给图片写死宽高,确保 paged.js 拿到稳定的布局信息。

三、核心实现:动态目录插件

下面是完整可用的目录处理器。它的逻辑分三步:渲染完成后提取所有标题及页码,生成带锚点的目录结构插入文档头部,然后清空已渲染结果重新分页一次,让目录占据自己的页面,并保证锚点跳转正确。

class TocHandler extends Paged.Handler {
  constructor(chunker, polisher, caller) {
    super(chunker, polisher, caller);
  }

  // 所有页面渲染完成后触发
  afterRendered(pages) {
    // 防止第二次渲染时重复生成目录
    if (document.querySelector('.generated-toc')) return;

    const items = [];
    document.querySelectorAll('h1[data-toc], h2[data-toc]').forEach(el => {
      const page = el.closest('.pagedjs_page');
      items.push({
        level: el.tagName === 'H1' ? 1 : 2,
        text: el.textContent.trim(),
        page: page ? page.dataset.pageNumber : '?'
      });
    });

    // 构造目录 DOM 并插入文档最前面
    const toc = document.createElement('nav');
    toc.className = 'generated-toc';
    const title = document.createElement('h1');
    title.textContent = '目录';
    toc.appendChild(title);

    items.forEach(item => {
      const line = document.createElement('div');
      line.className = 'toc-line toc-level-' + item.level;
      line.innerHTML = '<span class="toc-text">' + item.text +
        '</span><span class="toc-page">' + item.page + '</span>';
      toc.appendChild(line);
    });

    // 目录本身从新的一页开始
    toc.style.breakBefore = 'page';
    document.body.prepend(toc);

    // 关键:清空 paged.js 渲染结果,重新分页,让目录参与排版
    document.querySelectorAll('.pagedjs_pages')[0].remove();
    window.PagedPolyfill.preview();
  }
}

Paged.registerHandlers(TocHandler);

注意 document.querySelectorAll('h1[data-toc], h2[data-toc]') 这个选择器:通过 data-toc 属性显式标记哪些标题进入目录,是过滤页眉、封面上重复标题最可靠的方式。比起按标签名全量抓取再排除,这种方式让文档作者自己控制目录粒度,代码也不用维护黑名单。

目录行的排版建议用 flex 布局,文字与页码两端对齐,中间留出引导点或空白:

.toc-line {
  display: flex;
  align-items: baseline;
  gap: 8px;
}
.toc-text::after {
  content: ' ';
  flex: 1;
  border-bottom: 1px dotted #999;
  /* 也可以用 leaders 特性,兼容性一般时用此方案 */
}
.toc-level-2 { padding-left: 2em; font-size: 0.9em; }
.toc-page { font-variant-numeric: tabular-nums; }

四、进阶问题与调优

页码在第二次渲染后偏移怎么办。插入目录后文档会多出一到两页,所有正文的真实页码都会顺移。这正是代码里“重新分页”的意义:第一次渲染只为收集页码,第二次渲染后目录中的数字才与正文一致。但如果目录条目极多(比如超过两页),第二轮插入的目录又会改变后续页码,形成循环。解决办法是先估算目录占用的页数,在第一次渲染前就预留出相同数量的空白页,这样两轮渲染的总页数一致,页码自然稳定。

封面与前言不计入页码。正式文档通常封面无页码、目录从 1 或罗马数字开始。可以在 CSS 中对特定页面重置计数器:

@page cover { @bottom-center { content: none; } }
.cover { page: cover; }

@page :first {
  counter-reset: page 0; /* 封面记为 0,下一页从 1 开始 */
}

点击目录跳转到对应页。打印场景下跳转意义不大,但屏幕预览时很有用。paged.js 会为带 id 的元素维护内部锚点,给每个标题加上唯一 id(可在 handler 中用 el.id = el.id || 'toc-' + i 补齐),再把目录行包一层 a 标签指向该 id,浏览器内点击即可定位。

最后是性能问题。几百页的长文档,双次渲染的耗时可能达到十几秒。如果导出是高频操作,建议把渲染放到 Web Worker 或独立的无头浏览器(如 Puppeteer)中执行,渲染完成后再触发 window.print() 输出 PDF,用户侧几乎无感知等待。这套方案上线后,目录页码与实际内容完全由程序保证一致性,人工校对环节可以直接省掉。

paged.js可打印HTML动态目录修改时间:2026-09-07 19:16:44

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260907/52391.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。