本文将会介绍如何在前端打印时实现如下形式的页码,同时也会讨论该实现方式的缺陷、以及其他可能的解决方案。
这个需求的难点
在网页上显示的内容是连续的,而调用打印之后,连续的内容会被分割到多个纸张上。
但是前端无法得知浏览器是以何种方式(横向打印还是纵向打印、纸张大小、页边距等)对内容进行分割的,所以无法得知总共有多少页。进而无法设置页码。
相关知识
通常情况下,前端通过调用 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;
width: 100%;
font-size: 12px;
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.current) return;
// 猜一下总页数
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