使用 paged.js 高效构建可打印HTML的动态目录

本教程旨在指导开发者如何利用 paged.js 库为可打印的 HTML 文档动态生成一个结构清晰的目录。文章将详细介绍如何通过 J*aScript 遍历文档中的标题元素、生成锚点链接,并将其集成到 paged.js 的处理流程中,从而在打印输出时实现一个功能完善、自动更新的目录,无需直接通过 J*aScript 获取页面号。
在构建可打印的 HTML 文档时,尤其对于长篇内容,一个带有章节页码的目录是必不可少的。传统的做法可能涉及在 J*aScript 中尝试获取特定元素所在的页码,但由于页码是打印布局的产物,并非 DOM 元素的固有属性,这种方法往往难以实现。paged.js 提供了一种优雅的解决方案,允许我们在其渲染流程中动态构建目录,从而间接实现页码关联的效果。
核心原理
paged.js 的核心在于将 HTML 内容流式地分页,并应用 CSS 打印样式。为了生成动态目录,我们不需要直接在 J*aScript 中计算页码。相反,我们可以在 paged.js 开始分页之前,通过 J*aScript 遍历文档中的所有章节标题,为它们创建唯一的 ID,并在目录区域生成指向这些 ID 的锚点链接。当 paged.js 完成分页并生成打印输出时,这些锚点链接将自然地指向其对应的章节起始位置,而打印机或 PDF 阅读器则会显示这些章节所在的实际页码。
paged.js 提供了处理程序(Handlers)机制,允许我们在其处理生命周期的不同阶段注入自定义逻辑。对于目录生成,beforeParsed 钩子是理想的选择,因为它在 HTML 内容被解析但尚未进行分页布局之前执行。
准备您的 HTML 结构
首先,确保您的 HTML 文档包含清晰的章节标题和目录的占位符。
-
章节标题: 使用语义化的标题标签(如
,
,
等)来标识您的章节。为确保锚点链接的正确性,这些标题需要有唯一的 id 属性。如果标题本身没有 id,我们将通过 J*aScript 动态添加。
- 目录占位符: 在您希望目录出现的位置创建一个空的容器元素,例如一个 div 或 ul,并为其指定一个唯一的 id。
示例 HTML 结构:
<!DOCTYPE html>
<html>
<head>
<title>可打印文档</title>
<!-- 引入 paged.js -->
<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"></script>
<!-- 您的自定义脚本和样式将在此处 -->
</head>
<body>
<!-- 目录占位符 -->
<div id="my-toc-content">
<h2>目录</h2>
<ul id="list-toc-generated">
<!-- 目录项将在此处动态生成 -->
</ul>
</div>
<!-- 文档内容 -->
<h1 id="pre-digital_era" class="title-element" data-title-level="h1">
数字时代前夕
</h1>
<p>这是数字时代前夕的相关内容...</p>
<h2 id="early_computers">早期计算机</h2>
<p>早期计算机的发展历史...</p>
<h1 id="digital_era" class="title-element" data-title-level="h1">
数字时代
</h1>
<p>这是数字时代的相关内容...</p>
<div class="aritcle_card">
<a class="aritcle_card_img" href="/ai/2108">
<img src="https://img.php.cn/upload/ai_manual/001/246/273/68b6ca42843ea121.png" alt="AppStruct">
</a>
<div class="aritcle_card_info">
<a href="/ai/2108">AppStruct</a>
<p>无代码应用开发平台</p>
<div class="">
<img src="/static/images/card_xiazai.png" alt="AppStruct">
<span>132</span>
</div>
</div>
<a href="/ai/2108" class="aritcle_card_btn">
<span>查看详情</span>
<img src="/static/images/cardxiayige-3.png" alt="AppStruct">
</a>
</div>
<h2 id="internet_revolut
ion">互联网革命</h2>
<p>互联网如何改变世界...</p>
</body>
</html>动态生成目录的 J*aScript 逻辑
接下来,我们将编写一个 J*aScript 函数来遍历文档中的标题,并根据它们生成目录项。
/**
* 动态生成目录
* @param {object} config - 配置对象
* @param {HTMLElement} config.content - 文档内容根元素
* @param {string} config.tocElement - 目录容器的选择器
* @param {string[]} config.titleElements - 标题元素的选择器数组,按层级顺序
*/
function createToc(config) {
const content = config.content; // 文档内容根元素
const tocElementSelector = config.tocElement; // 目录容器选择器
const titleElementsSelectors = config.titleElements; // 标题元素选择器数组
// 查找目录容器,并创建或获取 ul 元素
let tocContainer = content.querySelector(tocElementSelector);
if (!tocContainer) {
console.warn(`目录容器 ${tocElementSelector} 未找到.`);
return;
}
let tocUl = tocContainer.querySelector("#list-toc-generated");
if (!tocUl) {
tocUl = document.createElement("ul");
tocUl.id = "list-toc-generated";
tocContainer.appendChild(tocUl);
} else {
// 清空现有目录,防止重复生成
tocUl.innerHTML = '';
}
let tocElementNbr = 0; // 用于生成唯一 ID 的计数器
// 1. 遍历所有标题元素,添加必要属性
for (let i = 0; i < titleElementsSelectors.length; i++) {
let titleHierarchy = i + 1; // 标题层级 (1, 2, 3...)
let currentLevelTitles = content.querySelectorAll(titleElementsSelectors[i]);
currentLevelTitles.forEach(function (element) {
// 添加通用类名和层级数据属性
element.classList.add("title-element");
element.setAttribute("data-title-level", titleHierarchy);
// 如果没有 ID,则生成一个唯一 ID
if (element.id === "") {
tocElementNbr++;
element.id = `title-element-${tocElementNbr}`;
}
});
}
// 2. 遍历所有标记为 "title-element" 的元素,创建目录列表项
let tocElements = content.querySelectorAll(".title-element");
for (let i = 0; i < tocElements.length; i++) {
let tocElement = tocElements[i];
let tocNewLi = document.createElement("li");
// 添加目录项的层级类名
tocNewLi.classList.add("toc-element");
tocNewLi.classList.add(`toc-element-level-${tocElement.dataset.titleLevel}`);
// 复制标题元素的其他类名到目录项(可选,用于样式继承)
let classTocElement = tocElement.classList;
for (let n = 0; n < classTocElement.length; n++) {
if (classTocElement[n] !== "title-element") { // 避免复制内部使用的类名
tocNewLi.classList.add(classTocElement[n]);
}
}
// 创建锚点链接
tocNewLi.innerHTML = `<a href="#${tocElement.id}">${tocElement.innerHTML}</a>`;
tocUl.appendChild(tocNewLi);
}
}代码解释:
- createToc 函数接收一个配置对象,包含文档内容根元素、目录容器选择器和标题元素选择器数组。
- 它首先查找目录容器,并确保其中有一个 ID 为 list-toc-generated 的
- 元素用于存放目录项。
- 接着,它分两步处理:
- 标记标题: 遍历 titleElementsSelectors 中定义的每个标题选择器。对于匹配到的每个标题元素,它会添加 title-element 类和 data-title-level 数据属性来指示其层级。如果标题没有 id,它会为其生成一个唯一的 ID。
- 构建目录: 再次遍历所有带有 title-element 类的元素。为每个元素创建一个
- 标签,并生成一个 锚点链接,其 href 指向标题的 id,文本内容为标题的 innerHTML。最后,将
- 添加到目录
- 中。
集成到 paged.js 工作流
为了让 createToc 函数在 paged.js 处理文档时自动执行,我们需要创建一个 Paged.Handler 并将其注册到 paged.js。
将以下代码添加到您的 HTML 文档的
部分,紧随 paged.js 脚本之后:<script>
// 确保 createToc 函数已定义
// (您可以将 createToc 函数放在这里,或者确保它在全局作用域中可用)
class CustomTocHandler extends Paged.Handler {
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
}
// beforeParsed 钩子在 HTML 内容被解析但尚未进行分页布局之前执行
beforeParsed(content) {
// 调用 createToc 函数,传入配置
createToc({
content: content, // paged.js 提供的文档内容根元素
tocElement: "#my-toc-content", // 您的目录容器 ID
titleElements: ["h1", "h2", "h3"], // 您希望作为目录项的标题选择器数组
// 例如: [".mw-content-ltr h2", "h3"]
});
}
}
// 注册您的自定义处理程序
Paged.registerHandlers(CustomTocHandler);
</script>代码解释:
- 我们定义了一个名为 CustomTocHandler 的类,它继承自 Paged.Handler。
- 在 CustomTocHandler 中,我们重写了 beforeParsed 方法。这个方法会在 paged.js 解析完所有 HTML 内容,但尚未开始计算页面布局之前被调用。
- beforeParsed 方法接收一个 content 参数,它是当前文档内容的根元素,我们可以用它来查询和操作 DOM。
- 在 beforeParsed 中,我们调用之前定义的 createToc 函数,并传入必要的配置。
- content: 直接使用 paged.js 提供的 content 元素。
- tocElement: 指定您的目录容器的选择器。
- titleElements: 指定您希望包含在目录中的标题元素选择器数组。可以根据实际需求调整,例如 ["h1", "h2", "h3"] 或者更具体的 [".chapter-title", "article h2"]。
- 最后,通过 Paged.registerHandlers(CustomTocHandler) 将您的处理程序注册到 paged.js。
注意事项与最佳实践
-
CSS 样式: 生成的目录只包含基本的 HTML 结构。您需要编写 CSS 样式来美化目录,例如添加缩进以体现层级关系、调整字体大小和颜色等。
#my-toc-content ul { list-style: none; padding-left: 0; } .toc-element-level-1 { margin-left: 0; font-weight: bold; } .toc-element-level-2 { margin-left: 20px; } .toc-element-level-3 { margin-left: 40px; font-style: italic; } /* 您也可以使用 CSS counters 来添加页码,paged.js 会自动处理 */ /* 例如,在 @page 规则中设置页码 */ @page { @bottom-right { content: counter(page); } } -
选择器精度: titleElements 数组中的选择器应尽可能精确,以避免将不希望出现在目录中的元素包含进来。例如,如果您只想将文章主体内的
包含进来,可以使用 article h2 而不是简单的 h2。
- ID 管理: createToc 函数已经包含了为没有 id 的标题自动生成 id 的逻辑,这增强了其健壮性。但如果您的标题本身就有稳定的 id,最好保持它们。
- 性能: 对于非常大的文档,querySelectorAll 操作可能会有性能开销。在大多数情况下,这种开销可以忽略不计,但如果遇到性能问题,可以考虑优化 DOM 查询。
-
可访问性: 确保生成的目录在语义上是正确的,并考虑到可访问性。例如,使用
- 和
- 标签来表示列表结构。
- 页码显示: 再次强调,paged.js 会通过 CSS 的 @page 规则和 counter(page) 功能在页眉或页脚自动添加页码。我们这里构建的 J*aScript 目录是基于锚点链接,它确保了目录项能够正确地导航到对应的章节,而实际的页码会在打印时由 paged.js 和浏览器/打印机引擎负责呈现。
总结
通过 paged.js 的处理程序机制,我们可以优雅地解决为可打印 HTML 文档动态生成目录的问题。这种方法避免了在 J*aScript 中复杂地计算页码,而是利用了 paged.js 在分页前对 DOM 进行操作的能力,通过锚点链接实现了目录与章节的关联。结合适当的 CSS 样式,您可以为您的打印输出提供一个专业、功能完善且易于导航的动态目录。
以上就是使用 paged.js 高效构建可打印HTML的动态目录的详细内容,更多请关注其它相关文章!

ion">互联网革命</h2>
<p>互联网如何改变世界...</p>
</body>
</html>