聊一聊前端开发中的网页打印

75 阅读5分钟

本文首发于公众号:符合预期的CoyPan

写在前面

最近的工作中,接触了网页打印的相关内容。本文总结一下。

一行代码实现网页打印

想要实现网页打印,一行代码就够了。

window.print();

image.png

浏览器会自动拉起打印预览页面。可将网页保存为pdf 或直接选择打印机进行打印。

如何打印页面中部分元素

网络上有不少的库可以完成此功能。其大致逻辑为:

  1. 获取对应的DOM元素的html字符串。
  2. 新建一个不可见的iframe
  3. iframe中,使用document.write,传入html字符串。
  4. iframe中,调用 window.print 拉起打印预览。

打印的时候,想要实现所见即所得,还需要传入对应的样式。大部分的第三方库,都是希望使用者自己传入一个特定的css文件,第三方库会把这个css文件导入到iframe中,使样式生效。当然,有简单的方法,就是将样式内联,直接写在html元素的style属性内。在代码开发的过程中,我们可以使用@page规则,来设置页面特定的打印样式。如:

// 设置打印时的大小为A4纸大小。
@page {
  size: A4; // 注意,这里并不能改变打印预览中的选项。
  margin-left: 1in; // 页面左边距1英寸。
}

// 设置首页的上边距为10厘米
@page :first {
margin-top: 10cm 
}

如何实现完美的打印效果

前面我们介绍了基本的关于打印的内容,事实上 W3C 已经制定了打印相关的标准。

www.w3.org/TR/css-page…

但是由于各个浏览器的支持度不同,想要实现复杂的打印是比较困难的。例如,我们想要打印一个页面,内容是一篇文章,其中包含了图片以及我们使用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,要求在当前元素前强制进行分页:

image.png

在页面中的元素被分页意外截断时,可以设置 page-break-inside: void,要求在元素内禁止截断。如果再加一个需求呢:给每一页添加特定的页眉、页脚

基于以上的内容,想要实现完美的打印效果,我首先考虑到有以下几个方案:

1、使用pdf生成工具,先将网页相关内容生成pdf,然后直接将pdf打印出来即可。

2、我们可以计算页面中的元素高度,再计算其在打印页面中的高度,在每一页合适的地方手动插入页眉、页脚,同时设置页眉、页脚的 page-break 属性,手动分页。

第一种方案对于内容较少的网页比较方便。对于内容较长的网页成本不小。因为内容较长的网页在打印时,也需要考虑分页,并且需要考虑如何添加页眉和页脚。第二种方案,整个过程比较可控,但是对高度计算准确性的要求比较高。

网页打印比较不错的解法

由于业务特点,需要更加通用的方案,因此没有采用生成pdf的方案。而是采用的标准打印方案。

W3C虽然制定了标准,但是浏览器实现没有跟上。于是,我们需要一个 polyfill 。正好,有这样一个polyfill:paged.js

pagedjs.org/about/

image.png

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>

最终的打印预览效果如下:

image.png

这里需要注意的是:不要在 iframe 中使用paged.js,我试过,会出现莫名其妙的问题(也可能是没弄对,有经验的大佬欢迎指教)。也就是说,当我们想对网页进行局部打印时,不要使用第三方库,可以考虑将对应内容单独拎出来,做一个新页面,在新页面中,就可以愉快的使用 paged.js 来实现较为完美的打印效果了。

写在后面

由于网络上关于网页打印的内容并不多,因此完成整个过程,还是踩了不少坑的。最后贴一个 GPT-4 的回答:

image.png