浏览器打印功能的正确打开方式

3,727 阅读4分钟

最近业务中有需要用到浏览器打印功能,本来以为很简单,但实操下来并非一帆风顺,想要达到预期的效果有不少阻碍,因此记录一下。

基础简介

  • 核心方法 window.print()
  • 调用后会弹出打印预览页面,并使浏览器进程进入等待状态,点击打印或者取消按钮后才会继续运行后面的代码
  • 打印设置建议设置 无边距,方便控制打印效果,第一次设置后,浏览器会保存设置
  • 打印纸张尺寸其实是一个比例,入A4纸为 根号二比一,只要控制好最外层的比例打印基本就不会有问题

几种浏览器打印的方法

不想看废话的直接看最后一种方法,通过iframe在页面视觉上无任何变化的情况下触发打印

整页打印

  1. 通过 window.print() 直接整页打印
    • 会打印当前整个 window.document 对应的页面
    • 这种方法最简单,也最没用,基本无法实现各种业务的打印场景

打印自定义的组件

  • 自定义组件最好使用内联样式
  1. 通过 window.print() 打印自定义的组件

    • 先获取自定义组件内容元素
    • 保存当前页 body 下的整个 html
    • 将自定义组件元素替换 body 下的元素
    • 执行打印
    • 打印完成后还原原来的页面
    • 这种方法会将当前页面替换成打印页内容,视觉上会有变化,某些场景可能不适用
  2. 通过 window.open() 打印自定义的组件

    • 先获取自定义组件内容元素,该组件可以使用 z-index: -9999; opacity: 0; 等方式不在原页面上显示
    • 使用 let newWindow = window.open("",'newwindow', 'height=300, width=700, top=100, left=100, toolbar=no, menubar=no, scrollbars=no, resizable=no,location=n o, status=no'); 打开一个新窗口
    • 将自定义组件元素内容放入新窗口的 body
    • 执行打印
    • 打印后执行 newWindow.close() 关闭新窗口
    • 这种方法可以做到不改变原页面视觉效果,但是会弹出新窗口,某些场景可能不适用
  3. 通过 后端URL 打印自定义的组件

    • 从后端拿到自定义组件内容的 URL 请求路径
    • 使用 let newWindow = window.open(URL,'newwindow', 'height=300, width=700, top=100, left=100, toolbar=no, menubar=no, scrollbars=no, resizable=no,location=n o, status=no'); 打开一个新窗口
    • 执行打印
    • 打印后执行 newWindow.close() 关闭新窗口
    • 这种方法与上一种方法大同小异,只是获取自定义组件内容元素从前端移到了后端,缺点同样存在
  4. 通过 iframe 打印自定义组件

    • 先获取自定义组件内容元素,该组件可以使用 z-index: -9999; opacity: 0; 等方式不在原页面上显示
    • 添加 iframe 元素,该元素可以使用 display: none; 等方式不在原页面显示
    • 将自定义组件元素内容放入 iframebody
    • 执行打印
    • 这种方法应该是最接近预期的,在页面视觉上无任何变化的情况下触发打印,但是也有几个问题要注意一下

iframe打印要点说明

iframe默认样式覆盖

  • iframe 相当于一个新窗口,有内置自带的样式,而通常现在工程化的页面中会使用 reset.css 等手段拉齐各浏览器的样式,所以在原页面定义的组件放入 iframe 中可能表现不一致,因此需要将 iframe 问题样式覆盖掉
  • 比较常见的问题样式(比较影响打印效果的,以谷歌为例):
    • body 自带的 margin: 8px;,需要将外边距置为 0
    • 盒模型问题,需要在带 padding 的元素上加上 box-sizing: border-box;
    • 包括 line-height 等样式以及 h1、h2、p 等元素,注意覆盖样式或者避免使用
  • 也可以将原页面 <head> 中的内容全部放入 iframe<head> 中:iframe.document.head.innerHTML = document.head.innerHTML;,此法某些场景不适用

高度和宽度最好固定

  • 利用响应式布局写的组件,通常不用使用固定宽度,但是发现火狐浏览器(不排除其他浏览器)带了 18px 的边距(即使设置了无边距),这样会导致火狐样式错乱,此时需要设置固定宽度
  • A4 纸打印,可设置高度为 1122px,宽度为 793.37px(根据固定比例 根号二比一 计算得出)

打印分页

  • 就是用到了 css 中的一个属性,但是网上很多相关内容过于陈旧,找到正确的属性费了点时间。
  • 亲测有效,break-after: page; 打印时会在设置了该属性的元素之后进行分页

防止分页

  • 避免在元素内部插入分页符,比如图片被截断
  • page-break-inside: avoid; 加上该属性后,如果当前页放不下,内容会进入下一页。而不会从中间截断

参考代码示例(vue)

<div class="cover-preview" v-if="showPreviewCover">
    <div id="print-cover-box" style="width: 793.37px;color: #000;">
        <!-- 火狐带了18px的边距,即使设置为无边距 把宽度定死-->
        <div v-for="item in coverList" :key="item.objectId" style="break-after: page;width: 100%; height: 1122px; padding: 140px 80px;box-sizing: border-box;"> 
            <div style="width: 100%; height: 100%; border: 6px solid #000;padding: 6px;box-sizing: border-box;">
                <!-- ... -->
            </div>
        </div>
    </div>
</div>
<iframe id="print-cover-iframe" frameborder="0" style="display: none;"></iframe>
this.showPreviewCover = true;
this.$nextTick(() => {
    let iframe = document.getElementById('print-cover-iframe').contentWindow;
    var printHtml = document.getElementById("print-cover-box").outerHTML;
    // iframe.document.head.innerHTML = document.head.innerHTML;
    iframe.document.body.style.margin = '0px';
    iframe.document.body.innerHTML = printHtml;
    iframe.print();
    this.showPreviewCover = false;
})
.cover-preview{
    position: absolute;
    left: 50%;
    top: 0;
    overflow: auto;
    transform: translateX(-50%);
    background-color: white;
    border-left: 1px solid #333;
    border-right: 1px solid #333;
    z-index: -9999;
    opacity: 0;
}