react 前端打印,实现分页页码

3,029 阅读5分钟

本文将会介绍如何在前端打印时实现如下形式的页码,同时也会讨论该实现方式的缺陷、以及其他可能的解决方案。

image.png

这个需求的难点

在网页上显示的内容是连续的,而调用打印之后,连续的内容会被分割到多个纸张上。

但是前端无法得知浏览器是以何种方式(横向打印还是纵向打印、纸张大小、页边距等)对内容进行分割的,所以无法得知总共有多少页。进而无法设置页码。

相关知识

通常情况下,前端通过调用 window.print() 来唤起打印功能(浏览器弹出“打印”弹窗)。或者将指定 dom 元素复制到 iframe 里并调用 iframe 的 print 方法来完成对局部页码的打印。在 react 中则是使用封装好的 react-to-print 组件来完成打印指定元素的功能。

给指定元素添加 page-break-before: always; 会让本元素在打印时强制另起一页。 类似的还有 page-break-after: always;,即让下一个元素强制另起一页。

包含 position: absolute; 的元素将会基于其所在的纸张进行定位。包含 position: fixed; 的元素同理,但是会在每页纸张上都显示一遍。

一般情况下:100vh 和 100vw 是指整个浏览器可视窗口的宽高。而在打印中,这两个将变成每张纸可用面积(即整张纸刨去页边距)的宽高。

w3c 标准提供了 @media print 来设置打印时特有的页面样式,并且提供了 @page 来配置打印时具体纸张的样式(例如下文中就使用了 @page { margin: 2.5cm; } 来设置默认页边距。)。目前现有的页面设置方案就用到了这个规则。

@page 范围内,可以使用 @top-center@bottom-left 等设置边框样式(通俗讲就是页眉页脚),具体规则可以看这里:CSS Paged Media Module Level 3 (w3.org)

但是目前绝大多数浏览器都没有实现该标准。 can i use 甚至都没有收录这几个规则,网上有很多文章都介绍可以用这个方式设置页码,纯属浪费时间。

这里是一些比较有用的 stack overflow 讨论,感兴趣的可以看一下:

如何实现分页页码

讲完了预备知识后,现在讲一下怎么实现添加分页页码的思路:

  • 首先获取要打印元素的高度,然后除以 A4 纸的高度(1123px)来得出大致的分页总数。
  • 按照分页数量,往要打印的元素里插入新 dom,添加  position: absolute; 并等差设置其 bottom 值为 -0vh、-100vh、-200vh 等等。打印时会将其自动排列到每张纸的底部。
  • 将这几个分页页码元素设置为 display: none;,并在 @media print 中设置为 display: inherit!important;,从而实现只在打印时才看得到这几个页码。

下面是对应的 react-to-print 代码实现:

index.less

@media print {
    .printPagination {
        display: inherit !important;
        position: absolute;
        width100%;
        font-size12px;
        text-align: center;
    }
}

.printPagination {
    display: none;
}

PrinterModal.jsx

import React, { useRef } from 'react';
import { Button } from 'antd';
import ReactToPrint from 'react-to-print';
import styles from './index.less';

export const PrinterModal = (props) => {
    // 要打印的元素引用
    const printRef = useRef(null);

    // 要打印前生成页码
    const onBeforeGetContent = () => {
        if (!printRef.currentreturn;

        // 猜一下总页数
        const totalPages = Math.ceil(printRef.current.scrollHeight / 1123);

        // 移除可能存在的页码元素
        const paginations = document.querySelectorAll(styles.printPagination);
        for (const el of [...paginations]) {
            el.parentElement.removeChild(el);
        }

        // 添加页码元素
        for (let i = 0; i < totalPages; i++) {
            const pagination = document.createElement('div');
            pagination.className = styles.printPagination;
            pagination.innerHTML = `<span>${i + 1} / ${totalPages}</span>`;
            // 设置 bottom 高度
            pagination.style.bottom = `-${i * 100}vh`;
            printRef.current.appendChild(pagination);
        }
    }

    return (
        <div>
            <div ref={printRef}>
                {/* 要打印的内容 */}
            </div>
            <ReactToPrint
                pageStyle="@page { margin: 2.5cm; }"
                onBeforeGetContent={onBeforeGetContent}
                trigger={() =>  <Button type="primary">确认打印</Button>}
                content={() => printRef.current}
            />
        </div>
    )
};

这里是一个类似的实现方案,没有使用 react:CSS Number Pages - With Total - JSFiddle - Code Playground

该实现方案存在的问题

最大的问题在于,前端无法精准的获取到当前能分成多少页,下列几个问题均有可能导致分页数量和实际页数不一致:

  • 要打印的元素显示宽度和纸张宽度不一致。
  • 某些元素设置了 page-break-before: always;,因为这个 css 会导致另起一页,所以页码数量肯定比计算数量多。
  • 某些只会在打印时出现并且会占据高度的元素。
  • 用户修改了页边距。
  • 用户修改了纸张类型。
  • 用户修改了打印方向,例如纵向打印改为横向打印。

一旦出错导致实际页码和当前页码不一致,就会导致最后几页没有页码,或者多出来几张只有页码的空白页。

总结一下就是:在用户不修改打印配置的情况下,页码几乎不会出错,而在用户修改打印配置时,大概率会导致页码出错。

除此之外,当页面底部有内容时可能会导致一个更严重的问题:页码有可能会和正文内容重叠。 由于页码元素使用的是相对定位,这个问题几乎无解。

未经验证的解决方案

在搜索时发现了另一种解决思路,即将打印内容转移到后端实现,后端生成 word 并用使用页脚设置页码,最后转为 pdf 通过接口发送给前端进行打印。

使用 word 生成内容保证页码必定正确,并转换为 pdf 强制分页,从而避免用户修改打印配置导致的页码问题。

前端打印 pdf 见:vue接收后端传来的pdf文件流,前端调用预览PDF_iskr樂的博客-CSDN博客_vue接收pdf

参考