JS实现自定义打印与静默打印:从Iframe到Electron方案

作者:纸飞机时间:2026-10-31 09:59:42浏览:523

本文详细解析JavaScript中实现自定义打印与静默打印的完整方案。首先介绍基于iframe拼接HTML字符串的自定义打印方法,解决分页、页眉页脚等复杂排版需求。随后探讨纯前端无法实现静默打印的限制,提出基于Electron开发本地打印App并通过WebSocket与浏览器通信的解决方案,实现点击按钮直接调用打印机而无需预览弹窗。

在浏览器环境中执行打印是一项常见操作。最基础的方式是点击浏览器右上角的“打印”按钮,或者在代码中调用window.print()。执行该操作后,当前页面的所有内容将被完整打印出来。

一、自定义打印两个方法

然而,实际业务场景中的打印需求往往更为复杂。通常我们需要打印页面中特定的某段内容,或者进行高度自定义的排版,而非整个页面。这就引出了自定义打印的需求。

一、自定义打印两个方法

实现自定义打印的现有方案众多,包括各种JavaScript库。其中应用最为广泛的两种方法如下:

第一种是直接调用window.print()。在执行打印前,先通过display='none'属性隐藏不需要打印的元素,待打印任务完成后,再通过display='block'恢复这些元素的显示状态。

第二种方法是创建临时Iframe进行打印。在生成临时Iframe标签后,将需要打印的内容拼接为HTML字符串并渲染至Iframe内部,随后在Iframe中执行iframe.contentWindow.print()。

方法1操作简便,适用于结构简单的页面;但对于复杂页面,其局限性较大。方法2则能应对绝大多数复杂的打印需求,本文后续设计的自定义打印方案即基于此方法实现。

1.iframe 打印基本使用

Iframe打印的本质与直接页面打印一致,最终调用的依然是window.print()。区别在于,我们将打印内容渲染在Iframe内部,并在该Iframe环境中触发打印动作。

具体的打印方法实现代码如下:

/**
 * 打印方法实现
 */
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,该变量保存了打印时各页所需的数据及配置。我们为每一页定制特定的渲染数据与配置信息。

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模板负责UI与布局,传入分割好的数据,最终即可渲染出固定样式的HTML页面内容。后续无论打印内容如何变化,只需处理好这几部分之间的关系,即可得到对应的HTML页面,将其放入Iframe即可打印任意内容。

三、静默打印

前文所述均调用浏览器自带的打印能力,即通过window.print()方法触发浏览器预览打印。这种方式接入简单,操作便捷。

然而,它存在一个不容忽视的缺点(这也情有可原,毕竟浏览器并非专业打印设备,需兼顾安全性与通用性),即在打印触发前,必然会弹出一个“预览”大弹窗。

而在某些需求场景下,我们期望点击按钮即实现打印,直接向打印机发送指令,而不弹出打印“预览”弹窗。

经多方查证,纯“浏览器前端”通过浏览器端的JavaScript无法实现这一功能。

那就没有解决办法了吗?

当然有,那就是自行开发一个打印App。

浏览器本身可视为一种特殊的“打印App”。既然浏览器能调用打印机,自定义的打印App自然也能做到。

1、如何设计打印App的功能

打印App是一个PC端应用,使用Electron即可轻松构建。其需实现两个核心功能:

1.连接和管理电脑设备上的打印机 2.能够与浏览器进行通信。

连接和管理电脑设备上的打印机在此不作详细介绍,网上已有成熟方案,使用Electron自带的API即可实现。

至于如何与浏览器通信,也不复杂,此处简要介绍实现思路。其实很简单,即通过Socket通信。

我们只需在此应用上启用一个Socket Server服务,该服务监听一个端口,例如:18877。这个Socket服务与我们服务器上启动的服务类似,但直接部署在用户本地机器上,仅供当前用户使用。

之后,我们只需在浏览器端启动一个WebSocket本地客户端,然后建立与ws://127.0.0.1:18877的连接即可。

当需要打印时,只需发送Socket信息给打印App,将打印事件、打印文本及其他相关打印信息发送给打印控件服务。打印控件接收到请求后,调用电脑打印功能,驱动打印机执行打印。

至此,一套最基本的打印控件打印方案即告完成。

最终实现整体架构图

一、自定义打印两个方法

打印结果示例

一、自定义打印两个方法

以上就是JS分页打印及静默打印的方法实现的详细内容,更多关于JS分页打印及静默打印的资料请关注本站其它相关文章!

作者最新文章