• js如何实现自定义打印功能?
  • 发布于 2个月前
  • 262 热度
    0 评论

在浏览器上打印应该一个比较常见的操作。最简单的打印方式就是直接点击浏览器右上角,找到“打印”按钮或者调用window.print()。通过此方式,就能将当前页面整个打印出来了。然而,实际情况下大多数需求都不会如此简单。更多的可能是需要打印页面中的某一段“特定”内容或者自定义内容。这就需要用到自定义打印了。


一、自定义打印两个方法
实现自定义打印的方法网上能找到很多不同的实现方案和 js 库。
其中有两个用的最多的方法:
1)直接调用window.print()。
在调用此方法之前将不需要被打印的元素先通过display='none'隐藏掉,打印执行完毕后再通过display='block'还原页面显示。
2)创建临时 Iframe 进行打印。

临时的 Iframe 标签创建完成后将需要打印的内容拼接成 html 字符串渲染到 Iframe 里面,再执行iframe.contentWindow.print()。


方法 1 操作起来方便快捷,适合简单的页面,对于稍微复杂一点的页面就很不方便了。
方法 2 适合复杂的打印需求,几乎可以满足所有的打印需求,本人在后文中设计的自定义打印方案就是基于此方法实现。
1.iframe 打印基本使用
Iframe 打印和直接页面打印其实是一样的,最终也是调用 window.print()。只不过我们是将打印的内容渲染在 iframe 内部,后在 iframe 内部调用而已。
打印方法实现如下:
/**
 * 堆代码 duidaima.com
 * 打印方法实现
 */
const handlePrintByLocalIframe = ({ printHtml }) => {
  // 判断是否已经存在该iframe
  let iframe: any = document.getElementById('J_printIframe');
  if (!iframe) {
    // 新建一个隐藏起来的iframe,并将其添加到当前页面的dom里面
    iframe = document.createElement('IFRAME');
    iframe.setAttribute('id', 'J_printIframe');
    iframe.setAttribute('style', 'position: absolute; width: 0px; height: 0px;left:-5000px;top:-5000px;');
    document.body.appendChild(iframe);
  }
  const doc = iframe.contentWindow.document;
  // 将需要打印的html字符串写入iframe
  doc.write(printHtml);
  doc.close();
  iframe.contentWindow.focus();
  setTimeout(function () {
    // 对iframe执行打印操作
    //延迟50ms是为了解决第一次样式不生效的问题
    iframe.contentWindow.print();
  }, 50);

  // 网上有人加了这一段代码,应该是为了兼容ie,这个看个人需求添加上。
  if (navigator.userAgent.indexOf('MSIE') > 0) {
    document.body.removeChild(iframe);
  }
};
html 字符串拼接方法实现如下:
/**
 * 生成 Iframe 内嵌页面字符串并执行打印
 * 为了将业务和打印功能分开,这里将打印的 html 页面做成了一个 html 模板,并上传至 cdn。
 * 后分别拉取 html 模板、接口数据、然后通过第三方库 mustache 来组装生成 html 字符串。
 * 最后将其传入前面的打印方法进行打印
 */
// 从cdn上获取html字符串
const htmlStr = await fetchRemoteData('这里填写html模板字符串的cdn地址');
// 从服务端获取数据
const data = await fetchRemoteData('这里获取接口数据,用于打印文件的数据');
// 使用mustache模板语法进行渲染(需要和html模板字符串模板一致,可以使用其他模板如 handlebars)
const printHtml = mustache.render(htmlStr, data);
// 执行打印
handlePrintByLocalIframe(printHtml);
至此,一个基本的打印功能就完成了,针对单页打印、普通文本的打印场景已经足够了。只是,这就结束了吗?当然不会,实际需求中还有很多复杂的打印场景,比如报表打印。打印报表的时候往往会涉及到分页、页头、页眉、页脚等比较复杂的场景。
比如:
<!-- 文末有示例 -->
首页页需要页头其它也不需要
首页都需要表头,末页需要签名其它也不需要
...

很显然,面对这些“有理”要求,仅靠上面这个方案还做不到。


二、定制化的自定义打印

上文实现的打印,其实现原理就是拼接 html 字符串,然后将字符串传入 iframe,然后进行打印。而作为一名前端开发,操作 html 就像呼吸一样简单,想要在网页上画出来分页、表头、页眉、页脚这些根本没什么难度可言。因此,理论上只需要在原方案基础上做“亿点优化”就可以解决了。下面介绍一下本人的设计实现方案,其核心在于自定义分页。


具体打印方案
首先从接口拿到数据并将其转换成下面的数据结构。
其核心在于 pageList,这个 pageList 保存的就是打印的时候各个打印页需要用到的数据和配置。
我们为每一页定制当页渲染所需要的特定的数据和特定配置。
1)约定的数据格式示例
const data = {
  pageTitle: '多页模板的数据',
  pageList: [
    {
      // 只有第一页有head,后面的页没有
      pageHead: true,
      pageNum: 1, // 当前页属于第1页
      list: [
        {
          dataId: 1,
          dataName: 'dataName1',
          dataNum: 8,
        },
        //...第一页的其他数据 28 条
      ],
    },
    {
      pageHead: false, // 除了第1页其他页面都不需要标题信息。
      pageNum: 2, // 当前页属于第2页
      list: [
        {
          dataId: 2,
          dataName: 'dataName2',
          dataNum: 6,
        },
        //...第2页的其他数据 28 + 2 条,多了pageHead 的空间所以多两条
      ],
    },
  ],
};
这份数据属于是定制化数据,具体数据格式与需要打印的 html 模板文件有关。
每一页的数据都是通过手动计算出来的,计算方法示例如下:
/**
 * serverDataList 为接口返回的原始数组数据
 * 此方法将原始的数据转换成每一页单独需要的特定数据格式
 * 这里仅是一个示例,具体复杂度跟其打印的业务和模板文件有关
 * 理论上可以实现任何打印
 */
const calculatePageNum = (serverDataList) => {
  // 这里的数值需要手动测量,毕竟每一行的高度都不一样,需要根据实际情况测试出来
  const firstPageMaxNum = 36;
  const otherPageMaxNum = 40;

  const pageList = [];
  let currentPage = 0; // 当前遍历到第几页
  serverDataList.forEach((item, index) => {
    const { dataId, dataName, dataNum } = item;
    currentPage = index < firstPageMaxNum ? 1 : 1 + Math.ceil((index + 1 - firstPageMaxNum) / otherPageMaxNum);

    if (!pageList[currentPage - 1]) {
      pageList[currentPage - 1] = {
        pageHead: currentPage === 1,
        pageNum: currentPage,
        list: [item],
      };
    } else {
      pageList[currentPage - 1].list.push(item);
    }
  });
  return pageList;
};
上述方法最终输出的是一个大的 pageList, 内部有一个小的 list。pageList 包含的是各个页面的数据,而 list 包含的是某一页的列表数据。除此之外,还有当前页面的页码,是否应该包含头部信息等。可以看出,这份数据就是为分页服务的,有了这份数据,我们只需要同步设计出相应的 html 模板.然后将对应的数据传入模板进行渲染就能得到相应的分页 html 字符串了。
2)对应的 html 模板
html模板可以是任何模板语法,这里我们采用的最简单的mustache语法
<body class="a4-body">
  <!-- pageList的数组长度就是当前页数,这里是一个遍历循环 -->
  {{#pageList}}
  <section class="a4-page">
    {{#pageHead}}
    <header class="head">
      <h2>{{pageTitle}}</h2>
    </header>
    {{/pageHead}}
    <table class="a4-table">
      <tr>
        <th>数据ID</th>
        <th>数据名称</th>
        <th>数据数量</th>
      </tr>
      <!-- 这里list就是当前页面的数据,每一页的长度可以不一样,如果有header这里就少几行 -->
      {{#list}}
      <tr>
        <td>{{dataId}}</td>
        <td>{{dataName}}</td>
        <td>{{dataNum}}</td>
      </tr>
      {{/list}}
    </table>
  </section>
  <ul class="a4-footer">
    <li>第{{pageNum}}页 总{{pageList.length}}页</li>
  </ul>
  {{/pageList}}
</body>

不难看出,当我们将前面格式化出来的 pageList 数据渲染到如上模板就能得到多个 pageList。每个 pageList 又包含多个数据行list,最终输出的就是一个完整的分页 html 字符串结构了。当然,仅仅有对应的结构是不够的,作为 html 页面,还需要配合对应的 css 样式。所以,我们还需要用 css 来做一些布局来保证 pageList 里面的一个 item 的总高度为 A4 的高度。


只要保证这个高度,其内部样式如何变化都没关系,多一个 header、或者某个特殊页面多一个特殊元素都无所谓。无非是在计算 pageList 的时候对数据进行增减即可。因此,根据上文的 html 模板,对模板里面的元素设置其 a4-body 容器和 a4-page 容器的高度,使其每一页高度固定。这样我们打印出来的内容就是我们最终期望的分页数据了。(这里主要介绍A4纸张,其它的原理类似)。
CSS 核心代码如下:
/* css全部使用mm作为单位 */
.a4-body {
  width: 208mm; /** 这里的宽度就是A4纸的宽度 */
  margin: 0 auto;
  text-align: center;
}

.a4-page {
  width: 100%;
  padding: 6mm;
  /** 这里高度 + a4-footer 的高度就是整张A4纸的高度(297mm) */
  height: 288mm;
  margin: 0 auto;
  box-sizing: border-box;
}
.a4-footer {
  line-height: 9mm;
}
至此,有了 html 模板和 css 负责处理 ui 和布局,传入分割好的数据,最终就能渲染出固定样式的 html 页面内容了。后面不论需要打印的内容如何变化,只需要处理好这几部分之间的关系,我们就能得到对应的 html 页面,将其塞入 iframe 就能打印任意内容。

三、静默打印

前面我们都是调用的浏览器自带的打印能力,即 window.print()方法触发的浏览器预览打印。这种方式非常简单,接入也不麻烦。然而,它有一个不容疏忽的缺点(也不算缺点,毕竟浏览器并不是专业打印设备,需要考虑到安全性和通用性),那就是打印触发之前它一定会弹出一个“预览”大弹窗。而有时候我们的需求是点击按钮就实现打印,直接给打印机发出打印指令,不要弹出打印“预览”弹窗。通过各种途径了解到,这是无法实现的,至少纯“浏览器前端”,通过浏览器端的 js 无法实现。


那就没有办法了吗?当然有,那就是自己开发一个打印App。浏览器本身其实也可以看做是一个特殊的“打印App”。浏览器能调用打印机,自定义打印App当然可以。
如何设计打印App的功能
打印App就一个PC端的应用,用 Electron 就能很轻松的做出来。
其需要实现两个核心功能:
1.连接和管理电脑设备上的打印机

2.能够与浏览器进行通信。


连接和管理电脑设备上的打印机这个这里不做详细介绍,网上有成熟实现方案,使用 electron 自带的 API 即可实现。至于如何与浏览器进行通信,也不是麻烦,这里简单介绍下实现思路。其实也很简单,无非就是一个 Socket 通信。我们只需要在此应用上启用一个 Socket Server 服务,此服务监听一个端口,比如:18877。


这个 Socket 服务和我们服务器上启动的服务是一样的,只不过此服务是直接部署到我们用户的本地机器上的,只给当前用户使用的。之后我们只需要在浏览器端启动一个 Websocket 本地客户端,然后建立与 ws://127.0.0.1:18877 的连接即可。当我们需要打印的时候,只需发送 Socket 信息给打印 App,将打印事件、打印文本及其他相关打印信息发送给打印控件服务。打印控件接收到请求之后再调用电脑的打印功能,调用打印机即可。
至此,一套最基本的打印控件打印方案就算完成了。
最终实现整体架构图

打印结果示例

用户评论