LOGO 首页 OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 技术文档 其他文档  
 
网站管理员

我开源了一个HTML 分页导出工具,解决了复杂页面的内容截断、跨页错乱和长文档白屏问题

zhenglin
2026年11月4日 8:54 本文热度 147

项目地址

最近重新改造了三年前写的一个 PDF 打印工具 Web Printer,现在更新到了 3.0。

这个项目最早用于财务系统的报表打印,主要解决表格跨页截断的问题。后来发现,真正困难的不是将 HTML 转成 PDF,而是如何在保留网页原有布局的前提下,找到正确的分页位置,并稳定导出长文档。

目前最常见的浏览器端方案之一是 html2pdf.js。它基于 html2canvas 和 jsPDF,接入简单,但在复杂长文档中存在一些已知限制。

GitHub Issues 里也能找到对应的真实反馈:

  • #692:长文本在分页位置被截断。


  • #393:表格分页出现逐页累积的位置偏差。


  • #19:长文档超过 Canvas 尺寸限制,导出空白。


  • #512:使用 avoid-all 后,分页产生不合理的空白区域。


针对这些问题,我重新设计了分页和渲染流程。


一、内容感知分页:不再直接按像素切割

html2pdf.js 的基本流程是:

HTML → DOM 克隆 → html2canvas 整页截图 → 按高度切割 → jsPDF

它也支持 CSS 分页和 avoid 规则,通过在元素前后插入占位节点调整分页位置。

但最终仍然是在 Canvas 上按照固定像素高度切图。当分页边界落在文字行中间时,就可能把一行文字切成两半。

Web Printer 采用另一种思路:先理解内容边界,再决定在哪里分页。

1. 元素分类

对 DOM 进行遍历时,将需要处理的内容分为三类:

分类典型内容分页规则
Atomic图片、SVG、代码块、显式保护区域整体保留,不跨页
Splittable段落容器、表格、一般布局容器递归分析子节点,寻找安全断点
Text文字节点按实际渲染的文字行测量

其中,表格行采用“整块优先”的策略:一行放得下就整体保留,超过单页高度则继续分析内部内容。严格 Atomic 元素如果超过单页容量,则明确报错。

分类规则独立维护于 print-rules.ts。


2. 文字行测量

这是分页中比较关键的一步。

DOM 元素的高度容易获取,但一个段落可能包含很多行文字。如果只知道整个段落的边界,就无法实现按行分页。

Web Printer 使用浏览器原生 Range.getClientRects() 获取文本在实际布局中的几何位置:

const range = document.createRange();


range.selectNodeContents(textNode);


const rects = Array.from(

  range.getClientRects()

);

​

随后结合 line-height 和可见裁剪区域,将这些矩形转换成受保护的纵向区间:

type Interval = {

  top: number;

  bottom: number;

};

这些区间表示:分页线不应该穿过这段可见内容。

实际实现见 text-range.ts。这里的行高补偿是基于 CSS 行高的近似计算,并不是完整重建浏览器的 line box。

3. 安全断点计算

完成测量后,分页算法不再依赖 DOM。

输入只有三个核心参数:


  • 文档总高度 height

  • 单页可用高度 capacity

  • 需要保护的区间 protectedRanges

采用贪心分页策略,先尝试当前页面能够容纳的最大高度。如果断点落入受保护区间,就向前回退到该区间起点。

核心逻辑可以简化为:

let end = Math.min(start + capacity, height);


// 如果分页线穿过受保护区域

if (range.top < end && range.bottom > end) {

  end = range.top;

}

实际实现会先排序、合并重叠区间,再通过游标逐页推进,避免每一页重复扫描全部区间。

完整算法见 paginator.ts。

这套设计的关键是:分页计算只产生坐标,不通过插入空白节点来改变原有 DOM 的布局。

二、视觉保真:分页不能反过来改变页面

解决了截断问题,接下来又遇到另一个问题:分页正确,不代表导出后的页面看起来正确。

真实业务页面可能有 Flex、Grid、自动宽度、嵌套容器、滚动区域,以及 fixed、sticky 等定位方式。

如果直接把 DOM 移动到新的打印容器,很容易触发重新布局(Reflow),造成元素宽度、文字换行和位置变化。

Web Printer 3.0 的解决方案是建立一个离屏打印快照(Print Snapshot)。

主要处理包括:

  1. 等待字体、图片等资源就绪,避免在临时布局状态下测量。

  2. 克隆需要打印的 DOM,复制计算后的 CSS 样式。

  3. 锁定打印根元素宽度,尽可能保留原有换行与布局。

  4. 复制表单当前值、Canvas 内容和内部滚动状态。


将 fixed、sticky 等定位状态冻结为快照中的当前几何位置。

之后,测量、分页和截图都基于同一份快照进行。

这样可以避免分页计算和实际渲染使用不同布局,也不会直接修改业务页面。

相关代码位于 snapshot.ts。


3.0 还移除了旧版分页规则中的 ignore 分类,避免分页引擎默认删除某些元素而改变布局。需要打印什么内容,优先由调用方选择打印根节点来确定。

这里的“所见即所得”是一个尽量保持打印视图几何与样式的设计目标,而不是保证能够还原任意网站的所有 CSS 特性。


三、长文档:从一次性超长截图改为逐页渲染

传统整页截图方案还有一个限制:Canvas 的尺寸并不是无限的。

当 HTML 文档很长时,一次性生成超大 Canvas,可能触及浏览器的最大尺寸或内存限制,最终导致空白 PDF。

Web Printer 把测量分页与 Canvas 渲染分离。

整个流程变成:

DOM 快照 → 布局测量 → 分页计划 → 逐页 Canvas → PDF / PNG

先生成所有分页区间,再逐页执行截图:

for (const page of pages) {

  const canvas = await capture(

    root,

    page.start,

    page.end - page.start

  );


  try {

    await writer.addPageSlice(

      index,

      canvas,

      pageHeight

    );

  } finally {

    release(canvas);

  }

}

这里不是为每页重新排列一份 DOM,而是从同一份冻结快照中,按照计算好的纵向区间截图。

每张截图都有独立的像素预算:最多 800 万像素、单边最大 8192 像素,必要时降低缩放比例。截图写入 PDF 后立即释放当前 Canvas。

相关实现:

这种方案降低了单张超大 Canvas 导致导出失败的风险,同时可以复用分页结果输出 PDF 或逐页 PNG。

但它并不意味着内存占用固定,也不意味着长文档导出一定更快:当前每页仍需调用 html2canvas,整体耗时会随页数增长。

四、Benchmark:与 html2pdf.js 实测对比

为了验证这些改造,我使用 Playwright 搭建了自动化 Benchmark,对三个版本进行比较:

  • html2pdf.js 0.14.0

  • Web Printer 2.0.0

  • Web Printer 3.0 开发版本

测试环境为 macOS、Chromium 141、A4 纵向、DPR 1,每个场景运行 3 次,耗时取中位数。

1. 导出性能

场景html2pdf.jsWeb Printer 3.0结果
长文本(2 页)1,761 ms759 ms快约 2.3 倍
复杂表格(3 页)3,117 ms1,237 ms快约 2.5 倍
长文档(5 页)4,382 ms2,167 ms快约 2.0 倍
Flex / Grid 布局校验失败630 ms通过校验
长文档(20 页)校验失败9.96 s通过校验
长文档(50 页)校验失败34.98 s通过校验

在本机这批测试样本中,5 页以内的成功导出场景,Web Printer 的耗时约为 html2pdf.js 的 40%–50%。


2. 长文档正确性

Benchmark 不只检查导出是否成功,还使用 PDF.js 重新读取实际生成的 PDF,逐页验证内容。

20 页和 50 页样本包含预设的顺序颜色标记,用于判断内容是否丢失、重复或生成空白页。

测试html2pdf.jsWeb Printer 3.0
20 页内容标记重复,校验失败3/3 通过
50 页50 页全部空白3/3 通过

50 页样本中,html2pdf.js 的输出为全白 PDF,未检出预期的 500 个标记;Web Printer 则通过全部内容校验。

需要说明的是,这不意味着 html2pdf.js 在所有 20 页或 50 页文档上都会失败,结果与文档实际像素尺寸和浏览器环境有关。


3. 视觉还原

另外,对原页面与离屏快照进行了几何和像素对比。

在固定样本中,Web Printer 3.0 的文本、表格、Flex/Grid 布局,测量坐标最大差值均为 0,文字行数量没有变化。

滚动容器样本中,2.0 版本曾出现最大 160px 的坐标偏差,3.0 修复后降为 0。

因此,3.0 相比 2.0 的主要改进是布局正确性和快照一致性,而不是进一步提高导出速度。

完整测试参数、8 组结果以及复现方法都在仓库中:

Web Printer Benchmark

五、使用 Web Printer

安装:

npm install @darialan/web-printer

导出已经渲染好的 DOM:

import { outputDocument } from '@darialan/web-printer';


await outputDocument({

  element: document.getElementById('report')!,

  filename: 'report',

  format: 'pdf',

});

对于不希望跨页拆开的签名区、图片组合等内容,可以显式标记:

<div data-print-atomic="true">

  <!-- 需要整体保留的内容 -->

</div>

目前 Web Printer 的目标不是兼容任意交互式网页,而是为报表、单据、合同和长文章等已经排版好的打印视图,提供更可靠的分页导出。

输出的 PDF 仍然是位图页面,不支持直接选择和搜索文本;部分 CSS 特性及跨域资源也会受到 html2canvas 的限制。

项目已经开源,欢迎体验和反馈实际业务场景。


阅读原文:点击这里


该文章在 2026/11/4 8:54:43 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-9  粤公网安备44030602007207号