本文首发于公众号:符合预期的CoyPan
写在前面
最近的工作中,接触了网页打印的相关内容。本文总结一下。
一行代码实现网页打印
想要实现网页打印,一行代码就够了。
window.print();
浏览器会自动拉起打印预览页面。可将网页保存为pdf 或直接选择打印机进行打印。
如何打印页面中部分元素
网络上有不少的库可以完成此功能。其大致逻辑为:
- 获取对应的DOM元素的html字符串。
- 新建一个不可见的
iframe
。 - 在
iframe
中,使用document.write
,传入html字符串。 - 在
iframe
中,调用window.print
拉起打印预览。
打印的时候,想要实现所见即所得,还需要传入对应的样式。大部分的第三方库,都是希望使用者自己传入一个特定的css文件,第三方库会把这个css文件导入到iframe中,使样式生效。当然,有简单的方法,就是将样式内联,直接写在html元素的style属性内。在代码开发的过程中,我们可以使用@page
规则,来设置页面特定的打印样式。如:
// 设置打印时的大小为A4纸大小。
@page {
size: A4; // 注意,这里并不能改变打印预览中的选项。
margin-left: 1in; // 页面左边距1英寸。
}
// 设置首页的上边距为10厘米
@page :first {
margin-top: 10cm
}
如何实现完美的打印效果
前面我们介绍了基本的关于打印的内容,事实上 W3C 已经制定了打印相关的标准。
但是由于各个浏览器的支持度不同,想要实现复杂的打印是比较困难的。例如,我们想要打印一个页面,内容是一篇文章,其中包含了图片以及我们使用HTML实现的特殊样式。
首先需要了解的一个知识点是:网页打印时默认会自动分页,当然也可以通过设置css,在指定的地方进行分页。
.selector {
page-break-after : auto | always | avoid | left | right;
page-break-before : auto | always | avoid | left | right;
page-break-inside : auto | avoid;
}
例如,设置page-break-before: always
,要求在当前元素前强制进行分页:
在页面中的元素被分页意外截断时,可以设置 page-break-inside: void
,要求在元素内禁止截断。如果再加一个需求呢:给每一页添加特定的页眉、页脚。
基于以上的内容,想要实现完美的打印效果,我首先考虑到有以下几个方案:
1、使用pdf生成工具,先将网页相关内容生成pdf,然后直接将pdf打印出来即可。
2、我们可以计算页面中的元素高度,再计算其在打印页面中的高度,在每一页合适的地方手动插入页眉、页脚,同时设置页眉、页脚的 page-break 属性,手动分页。
第一种方案对于内容较少的网页比较方便。对于内容较长的网页成本不小。因为内容较长的网页在打印时,也需要考虑分页,并且需要考虑如何添加页眉和页脚。第二种方案,整个过程比较可控,但是对高度计算准确性的要求比较高。
网页打印比较不错的解法
由于业务特点,需要更加通用的方案,因此没有采用生成pdf的方案。而是采用的标准打印方案。
W3C虽然制定了标准,但是浏览器实现没有跟上。于是,我们需要一个 polyfill 。正好,有这样一个polyfill:paged.js
W3C的标准,将打印页面分为了几个部分,如上图所示。页面的内容展示在 page area 区域,周围的部分,可以设置不同的内容。
让打印页面自动排版,在不想被分页截断的地方,设置 page-break-inside: void
,保证页面内容的完整性。设置 top 和 bottom 区域,展示特定的页眉页脚。
以下是一个完整的例子:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
p {
font-size: 48px;
}
@page {
size: A4;
margin-left: 0.3in;
margin-right: 0.3in;
/* 设置中间的页脚为页码 */
@bottom-center {
content: counter(page) ' / ' counter(pages);
}
/* 左上方的页眉设置为特定汉字内容 */
@top-left {
content: 'hello world';
text-align: left;
font-size: 12px;
}
/* 右上方的页眉,指定为 header-right 元素,这里的 header-right, 要和下面 running 中的取值 对应上 */
@top-right {
content: element(header-right);
text-align: right;
}
}
/* position running,设置为 header-right */
.header-right {
position: running(header-right);
}
</style>
<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"></script>
</head>
<body>
/* 使用自定义元素的方式,设定右上角页眉。*/
<div class='header-right'><img style="width: 60px" src="https://coypan.info/public/gzh_qrcode.jpg" /></div>
<p>
Hello World。Hello World。Hello World。Hello World。
Hello World。Hello World。Hello World。Hello World。
Hello World。Hello World。Hello World。Hello World。
Hello World。Hello World。Hello World。Hello World。
Hello World。Hello World。Hello World。Hello World。
</p>
/* 不让该元素中的内容被分页截断 */
<p style="page-break-inside: avoid">
这里的内容不能从中间截断。这里的内容不能从中间截断。
这里的内容不能从中间截断。这里的内容不能从中间截断。
这里的内容不能从中间截断。这里的内容不能从中间截断。
这里的内容不能从中间截断。这里的内容不能从中间截断。
这里的内容不能从中间截断。这里的内容不能从中间截断。
这里的内容不能从中间截断。这里的内容不能从中间截断。
</p>
</body>
</html>
最终的打印预览效果如下:
这里需要注意的是:不要在 iframe 中使用paged.js,我试过,会出现莫名其妙的问题(也可能是没弄对,有经验的大佬欢迎指教)。也就是说,当我们想对网页进行局部打印时,不要使用第三方库,可以考虑将对应内容单独拎出来,做一个新页面,在新页面中,就可以愉快的使用 paged.js 来实现较为完美的打印效果了。
写在后面
由于网络上关于网页打印的内容并不多,因此完成整个过程,还是踩了不少坑的。最后贴一个 GPT-4 的回答: