作者:刘锦泉
一、前言
在供应链业务系统中,打印是一项难以被完全数字化替代的基础能力。尽管系统形态不断向 Web 化、平台化演进,但在仓储、物流、配送等线下环节,大量核心流程依然依赖纸质单据(如面单、送货单、运输单)完成流转与交接。
打印的稳定性与一致性,往往直接影响现场作业效率。一旦打印出现异常,问题会迅速放大,甚至直接阻断线下业务流程。
基于真实的供应链项目实践,本文将围绕浏览器打印这一常被低估的能力,系统性地梳理主流方案的技术取舍,并重点展开基于 window.print 的实现思路与工程实践。
二、供应链系统中的主流打印方案调研
围绕“如何将单据稳定、可靠地打印出来”,在实际项目中常见的 Web 打印方案大致可以分为三类:
- 基于 DOM 的浏览器原生打印方案
- 基于可视化模板的前端打印方案
- 基于本地打印控件的软件方案
下面结合实际项目经验,对这三类方案进行简要分析。
1.基于DOM浏览器原生打印方案
这是目前最常见、也是前端介入成本最低的一种打印实现方式。
其核心思路是:
直接使用浏览器原生打印能力,将页面或指定 DOM 节点渲染为打印内容。
打印效果如下图:
实现方式
- 原生 window.print()
// window.print
window.print
- 使用 print-js 等开源库进行增强
官网地址:传送门(printjs.crabbly.com/)
// printJs
printJS({
printable: 'print-area',
type: 'html',
style: '@page { size: A4; margin: 10mm; }',
targetStyles: ['*']
});
优点
- 零额外依赖,完全基于浏览器原生能力
- 浏览器兼容性优秀:Chrome、Firefox、Safari、Edge 均支持
- 开发与维护成本低,对现代前端技术栈(React / Vue)非常友好
- 页面即模板,业务变更成本低,适合供应链中高频调整的单据场景
缺点
- 样式控制高度依赖 CSS,对分页、跨页控制要求较高
- 批量打印需自行实现调度与队列逻辑
- 打印体验受浏览器限制:
- 页眉页脚难以完全控制
- 无法绕开系统打印弹窗
2.基于可视化模板的前端打印方案(HiPrint)
以 HiPrint 为代表的方案,通常基于 jQuery,通过拖拽方式设计打印模板,再将业务数据动态渲染到模板中进行打印。HiPrint 官网地址:传送门(hiprint.io/)
配置示例效果如下图:
实现方式
// 基于jQuery的打印设计器
$('#element').hiPrint({
designer: true, // 开启可视化设计
templates: [...] // 预设模板
});
优点
- 所见即所得的可视化模板设计
- 拖拽式配置
- 非开发人员也可参与模板调整
- 布局组件丰富
- 表格、文本、条码、图片等组件齐全
- 支持动态数据绑定
- 模板管理能力完善
- 模板导入 / 导出
- 支持模板版本管理
缺点
- 技术栈依赖性强:强依赖 jQuery,与 React / Vue 整合成本高
- 性能问题明显:
- 模板复杂时渲染速度下降
- 大数据量场景容易卡顿
- 样式隔离难度大:
- 容易与现有系统样式冲突
- 需要额外处理样式覆盖与作用域
3.基于本地打印控件的软件方案(LODOP)
LODOP 是一类 依赖本地打印控件或客户端程序 的打印方案,通过浏览器与本地程序通信完成打印。在早期政务、财务、制造业系统中使用较多。官网地址:传送门(www.lodop.net/)
实现方式
- 先去Lodop官网下载相应的安装包
- 解压安装后将LodopFuncs.js放在项目中
- 具体页面引入 Lodop
import { getLodop } from "@/plugins/LodopFuncs";
let LODOP = getLodop()
- 执行打印指令
// 需要先安装LODOP软件
LODOP.PRINT_INIT("打印任务");
LODOP.ADD_PRINT_TEXT(50, 100, 200, 30, "供应链单据");
LODOP.PRINT();
优点
- 打印控制能力极强
- 支持毫米级定位
- 可直接控制打印机指令
- 对专业打印设备支持完善
缺点
- 浏览器兼容性极差
- 依赖 ActiveX
- 现代浏览器与移动端基本不支持
- 只支持windows 系统
- 部署与运维成本高
- 每台客户端需安装软件
- 需要管理员权限
- 安全性与可持续性问题
- ActiveX 存在安全隐患
- 已逐步被浏览器厂商淘汰
4.方案对比一览表
| 维度 | window.print | jQuery HiPrint | LODOP |
|---|---|---|---|
| 开发成本 | 极低 | 中等 | 高 |
| 打印精度 | 一般(受限于浏览器) | 良好 | 极高 |
| 跨浏览器 | 优秀 | 良好 | 极差 |
| 部署成本 | 零部署 | 低 | 高(需安装客户端) |
| 维护成本 | 低 | 中等 | 高 |
5.方案选型结论
结合当前业务特点:
- 需要快速适配不同客户与环境
- 系统主体基于 React 技术栈
- 单据模板相对稳定,支持页面预览
最终选择了 基于 DOM 的浏览器打印方案,并以 window.print 作为核心实现方式。
为什么直接使用 PrintJs ?
可能会有人疑惑,既然是 DOM 打印,为什么不使用 print-js 进行封装?主要原因如下:
- print-js 需要能够直接获取到打印节点(dom 的id 或者原始的html)
- 必须将预览节点在打印前挂载到页面上。
- iframe 隔离成本高
- 需要单独引入工程化依赖包CSS 资源。如: Antd等
- 样式维护成本高
- 额外样式需要么通过字符串方式注入,要么将工程的less 转换成css 引入,不利于后续维护。
总的来说,print-js 在工程化配置项目依旧不是那么方便还用。
三、window.print 的能力边界
window.print() 本质上是触发浏览器打印对话框。当打印动作触发时,浏览器会将当前页面最终渲染的结果输出为打印内容,或者导出为 PDF 文件。
也正因为这一点,window.print 天生存在一个非常重要的限制:
它无法直接指定“只打印某一个 DOM 区域”。
浏览器并不知道你只想打印某一个 div,它只会按照当前页面的渲染结果,完整地执行一次打印流程。
这意味着,如果页面上存在导航栏、按钮、筛选条件等元素,它们同样会被一并打印出来。
四、指定区域打印的实现思路
既然 window.print 无法直接指定打印区域,那么解决问题的思路就非常清晰了:
在触发打印之前,主动控制页面最终的渲染结果,让真正参与渲染的内容只剩下需要打印的部分。
顺着这个思路,第一个最直观的方案自然就出现了。
1.innerHTML 直接替换页面内容
既然浏览器只会打印当前页面的渲染结果,那么我们是否可以在打印前,直接用需要打印的内容替换整个页面?
实现方式也很简单:
- 缓存当前页面内容
- 将
document.body.innerHTML替换为打印区域内容 - 执行
window.print() - 打印完成后再恢复页面
代码示例如下:
// 获取预览组件的html
const getPreviewContainerHtml = (component) => {
const div = document.createElement('div');
flushSync(() => {
ReactDOM.render(component, div);
})
return div.innerHtml
}
const prinit = () => {
const oldHtml = document.body.innerHtml;
const printHtml = getPreviewContainerHtml(<PreviewContainer />);
document.body.innerHtml = printHtml;
window.print()
document.body.innerHtml = oldHtml;
}
这种方案在早期项目中非常常见。但随着实践的深入,很快就会发现一个问题:
直接替换 innerHTML 会彻底打破原有页面的渲染结构。
在 React / Vue 等现代前端框架中,这种方式会导致:
- 组件状态丢失
- 事件绑定失效
- 页面需要强制刷新才能恢复
因此,这种方案虽然思路直接,但在复杂系统中并不安全。
2.新开标签页打印
既然直接替换当前页面会破坏原有渲染结构,那么很自然就会想到:
如果不动当前页面,而是新开一个标签页,专门用来承载打印内容,是否就可以解决这个问题?
新开标签页后,我们可以:
- 在新页面中渲染完整的打印内容
- 调用新页面的
window.print() - 当前业务页面完全不受影响
从技术角度看,这个方案确实可以稳定地打印出我们想要的内容。但在真实业务场景中,很快又会暴露出新的问题:
- 浏览器可能拦截弹窗
- 用户需要多一步切换窗口的操作
- 打印流程被打断,体验不够流畅
对于仓库、物流等高频打印场景来说,这种额外的交互成本是不可忽略的。
于是问题进一步演化为:
能否在不新开标签页、不破坏当前页面的前提下,在当前页签内完成指定内容的打印?
3.iframe 当前页签打印
顺着这个问题继续往下推,自然而然就会引出 iframe 打印方案。
其核心思路是:
- 在当前页面中动态创建一个隐藏的
iframe - 将需要打印的内容完整渲染到 iframe 中
- 调用 iframe 内部的
print()方法完成打印
示例代码如下:
const print = () => {
const iframe = document.createElement('iframe');
iframe.src = `预览页地址`;
iframe.setAttribute(
'style',
'visibility: hidden; height: 0; width: 0; position: absolute; border: 0'
);
window.addEventListener('onafterprint', () => {
console.log('打印完成');
iframe.remove();
});
document.body.appendChild(iframe);
iframe.onload = () => {
setTimeout(function () {
iframe.contentWindow?.print();
}, 1000);
};
};
从浏览器的角度来看,iframe 本身就是一个完整的页面环境,因此可以独立完成打印流程,而不会影响主页面的渲染状态。
这一方案在实践中具备明显优势:
- 不破坏当前页面状态
- 不依赖新窗口或弹窗
- 对 React / Vue 等现代框架友好
- 非常适合供应链系统中的高频打印场景
4.批量打印
到目前为止,我们已经可以稳定地完成 单个单据的打印。但在供应链场景中,一个非常常见的需求是:
一次性打印多张面单、送货单或运输单。
在浏览器环境下,这一需求首先会受到一个客观限制:
- 浏览器在同一时间只能处理一个打印任务
- 调用
window.print()时,浏览器会弹出系统打印对话框 - 在用户完成打印设置并关闭对话框之前,页面线程会被阻塞
这意味着,通过循环多次调用 window.print() 来实现批量打印并不可行。
批量渲染内容 + CSS 分页
基于上述限制,一个更合理的思路是:在一次打印动作中,承载所有需要打印的单据内容。具体做法是:
- 遍历需要打印的单据数据
- 将每一张单据渲染为独立的打印容器
- 通过 CSS 控制分页规则
- 最终只触发一次 window.print()
示例代码如下:
// 批量创建打印容器
printPageList.map(() => {
return <PrintContent className="single-order-page" />
})
// css 配置
/* 确保每个订单在新页开始 */
.single-order-page {
page-break-before: always;
page-break-inside: avoid;
}
/* 第一个订单不需要分页 */
.single-order-page:first-child {
page-break-before: avoid;
}
通过这种方式,浏览器会将每个单据视为一页内容,顺序稳定、实现简单,非常适合版式统一的批量单据打印场景。
大批量场景下的性能考量
需要注意的是,当单据数量非常大(例如超过 50 张)时,一次性渲染所有打印内容,可能会带来内存占用和页面卡顿的问题。在这种情况下,可以考虑引入串行队列式的打印方案。
该方案的核心思路是:
- 构建一个打印任务队列
- 每次只渲染并打印一张单据
- 当前打印结束后,再进入下一张单据的处理
但需要特别注意的是,浏览器无法准确感知打印完成的时机,因此该方案通常需要额外的控制手段,例如:
- 由用户交互驱动(如点击“下一张”)
- 通过时间间隔进行节流控制
这种方式更适合作为大批量或特殊场景下的补充方案,而不是默认选择。
五、核心 CSS 打印配置:从“能打”到“好用”
在基于浏览器的打印方案中,CSS 决定了最终的输出效果。合理的打印样式配置,往往可以显著减少版式错乱、内容截断等问题,也是浏览器打印能否真正落地的重要前提。
下面结合实际项目,整理几类最常用、也最容易被忽视的打印样式配置。
1.使用 @media print 定义专用打印样式
浏览器提供了 @media print 媒体查询,用于在打印场景下覆盖页面的默认样式。
通过该方式,可以做到:
- 页面正常浏览时样式不受影响
- 打印时只应用与单据相关的样式规则
@media print {
/* 打印场景下隐藏页面中的非单据内容 */
.no-print {
display: none !important;
}
}
2.页面尺寸与边距控制
如果不对打印页面的尺寸和边距进行控制,常见问题包括:
- 内容被裁切
- 页面留白过多
- 不同浏览器下表现不一致
通过 @page 可以明确指定打印纸张规格与边距:
@media print {
@page {
size: A4;
margin: 10mm;
}
}
3.避免表格内容被强制拆页
表格是供应链单据中最常见的结构,同时也是打印问题的高发区域。
如果不加控制,浏览器可能会在任意位置拆分页,导致一行数据被拆成两页。
@media print {
table {
border-collapse: collapse;
/* 避免整个表格在打印时被拆分 */
page-break-inside: avoid;
}
tr {
/* 避免单行数据跨页 */
page-break-inside: avoid;
}
}
4.主动控制分页位置
在部分业务场景中,需要对分页位置进行明确控制,例如:
- 一张送货单单独占一页
- 合计信息必须在新页展示
此时可以通过自定义分页样式来实现:
@media print {
.page-break {
/* 在该元素前强制分页 */
page-break-before: always;
}
}
5.打印颜色与背景处理
浏览器在打印时,默认会忽略背景颜色与部分样式,这在以下场景中尤为明显:
- 面单背景色
- 标签类单据
- 状态标识区域
@media print {
body {
/* 强制按页面样式输出颜色 */
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
注:Window 电脑如果不设置 -webkit-print-color-adjust: exact 打印输出水印内容, 会出现空白现象。
六、总结
浏览器打印在供应链系统中看似只是一个基础能力,但在真实项目中,往往涉及方案选型、技术栈适配、用户体验以及长期维护成本等多方面的权衡。
本文结合具体的业务背景,对主流 Web 打印方案进行了梳理,并重点分析了基于 window.print 的实现思路及其工程边界。
可以看到,window.print 本身并不复杂,真正的挑战更多来自页面渲染控制、样式管理以及打印流程的工程化处理。这也使得浏览器打印更像是一项系统能力的组合,而非单一 API 的简单调用。
在当前阶段,window.print 配合 iframe 依然是 Web 打印方案中性价比最高、可控性最强的一种实现方式。随着业务复杂度的提升,打印方案仍有进一步演进与细化的空间。