在 统信 UOS 、银河麒麟 等国产 Linux 桌面上交付 B/S 系统时,「网页静默打印」往往被低估为「前端加个按钮」------直到联调阶段才发现:window.print() 绕不过去、打印机名对不上、任务进了队列却不出纸。
本文从浏览器安全模型 、本地客户端架构 、WebSocket 协议 、HTML→PDF→纸流水线 、并发与排错 几个层面展开,说明统信 / 麒麟环境下静默打印的完整技术链路,以及前端 npm 包 web-print-pdf 在其中的位置。
1. 为什么浏览器本身做不到静默打印
1.1 安全边界
现代浏览器(含统信 UOS 浏览器、360 安全浏览器等 Chromium 系)把「访问本机打印机」视为高权限操作。页面 JavaScript 可以:
- 打开
window.print()→ 必定弹出系统打印对话框 - 通过受限的 Web API 获取有限的设备信息
页面 不能:
- 直接向 CUPS 提交
lp任务 - 指定队列、份数、纸盒并跳过用户确认
- 读写 spooler 作业状态(除非走已废弃的 NPAPI / ActiveX 路线)
这不是国产系统特有的限制,而是 W3C / Chromium 安全模型 的通用设计:任意 HTTPS 页面若可静默打纸,即可被恶意站点用于骚扰打印或信息泄露。
1.2 常见「伪静默」方案及其问题
| 方案 | 表面现象 | 实质问题 |
|---|---|---|
隐藏 iframe + print() |
少点一次预览 | 仍弹系统对话框;无法指定打印机 |
| 浏览器 Kiosk / 策略 | 个别环境可压对话框 | 依赖域策略,统信 / 麒麟桌面难统一推送 |
| 导出 PDF 让用户打开 | 无浏览器打印框 | 多步人工操作,柜面 / 窗口场景不可接受 |
| 服务端打印 | 机房出纸 | 数据离岗、物理位置不对 |
| 纯前端 jsPDF | 生成文件 | 不驱动真实打印机 |
结论 :要在统信 UOS、银河麒麟上实现生产级静默打印,必须在用户本机有一个受信任的本地代理进程 ,浏览器只与之通信用协议,由代理调用 CUPS(Windows 侧则为 Print Spooler)。
2. 总体架构:三层分离
┌─────────────────────────────────────────────────────────────────┐
│ Layer 1 --- 业务 Web(统信 / 麒麟 / Windows 浏览器内) │
│ Vue / React + web-print-pdf(npm) │
└────────────────────────────┬────────────────────────────────────┘
│ WebSocket JSON(127.0.0.1)
▼
┌─────────────────────────────────────────────────────────────────┐
│ Layer 2 --- Web打印专家(Electron 本地客户端) │
│ · Express + express-ws 本地服务 │
│ · HTML 落盘 / URL 拉取 / PDF 生成 / 任务队列 │
└────────────────────────────┬────────────────────────────────────┘
│ lp / CUPS(Linux)| Spooler(Windows)
▼
┌─────────────────────────────────────────────────────────────────┐
│ Layer 3 --- 操作系统打印栈 │
│ 统信 UOS / 银河麒麟:CUPS + 驱动队列 │
└─────────────────────────────────────────────────────────────────┘
设计要点:
- 业务后端不参与出纸 ------ 打印任务不经过机房,符合政务 / 金融数据不出终端的要求。
- Web 代码跨平台一致 ------ 差异收敛在 Layer 2,前端始终
import webPrintPdf from "web-print-pdf"。 - 本地环回通信 ------ WebSocket 走
127.0.0.1,不暴露到局域网(远程打印另议,见 §6)。
3. 本地服务:端口、路由与消息契约
3.1 端口选择
客户端启动内嵌 HTTP/WebSocket 服务时,在候选端口列表中选取首个可用端口:
16794 → 16805 → 19235
web-print-pdf 默认尝试连接 ws://127.0.0.1:16794;若该端口被占用,客户端会落到备选端口,npm 包会按约定探测。集成时若硬编码端口,需与客户端实际监听一致,或依赖库内自动发现逻辑(以 npm README 为准)。
3.2 WebSocket 路由
| 路径 | 用途 |
|---|---|
/websocket/standard |
标准打印 API:printHtml、printPdfByUrl、batchPrint 等 |
/websocket/remotePrintTest |
远程打印联调 |
每条入站消息必须是 JSON,且服务端校验三个必填字段:
json
{
"id": "uuid",
"timestamp": 1710000000000,
"type": "printHtml"
}
缺少任一字段会直接拒绝。这是为了:
- 幂等与对账 :前端可用
id关联业务单号 - 乱序检测 :
timestamp辅助远程场景 - 路由分发 :
type映射到具体 handler 模块
成功响应示例:
json
{
"id": "...",
"timestamp": 1710000000001,
"type": "printHtml",
"success": true,
"data": { },
"extraOptions": { }
}
失败时 success: false,并带 message 文本,前端应展示给用户或写日志,而不是静默吞掉。
3.3 支持的 type 一览
| type | 输入 | 典型场景 |
|---|---|---|
printHtml |
HTML 字符串 content |
柜面小票、表单片段 |
printHtmlByUrl |
可访问 URL | OA 整页报表 |
printHtmlByBase64 |
Base64 HTML | 服务端下发模板 |
printPdfByUrl |
PDF URL | 后端已生成 PDF |
printPdfByBase64 |
Base64 PDF | 内网 API 返回二进制 |
printImageByUrl / printImageByBase64 |
图片 | 拍照件、扫描件 |
batchPrint |
printTaskList[] |
连续多张凭证 |
batchPrint 内每个 task 含 data、type,并可单独覆盖 pdfOptions / printOptions / extraOptions;全局选项会与 task 级选项浅合并,task 优先。
约束 :合并后所有 task 的 extraOptions.action 必须一致(要么全 print,要么全 preview),否则服务端抛错------避免一批任务里一半预览一半出纸的歧义状态。
4. 打印流水线:以 printHtml 为例
统信 / 麒麟与 Windows 在 Layer 2 共用同一套产品逻辑,差异仅在最后「把 PDF 交给谁」。
4.1 阶段 A --- HTML 规范化(generateHtml)
- 入参
content若不是完整 HTML 文档,会套入标准模板(standard.html),注入{``{%body%}}。 - 写入客户端工作目录,生成本地可访问的
htmlUrl(供 PDF 引擎加载)。 - 记录 html 日志:耗时、路径、成功 / 失败。
实践建议 :业务侧尽量传带内联样式的片段,减少对外部 CSS 的依赖;统信桌面若缺字体,此阶段不会报错,但下一阶段 PDF 会出现「豆腐块」。
4.2 阶段 B --- HTML → PDF(generatePdf)
使用 Playwright + Chromium headless (或系统已安装的 Chrome / Edge channel)打开上一步的 htmlUrl,调用 page.pdf() 等价能力,支持:
paperFormat/ 自定义width·heightmargin、printBackground、landscapepageRanges---[{from:1,to:5}, ...]watermark、pageNumber等后处理
Headless 启动参数含 --no-sandbox 等,适配 Linux 容器化 / 国产桌面权限模型。
extraOptions 中的网络上下文 (对 printHtmlByUrl / 远程 PDF 尤其重要):
| 字段 | 作用 |
|---|---|
requestTimeout |
拉取 URL 超时(秒),默认 15 |
cookies / localStorages / sessionStorages |
携带登录态访问内网报表 |
httpHeaders |
如 Authorization |
action |
print(默认)或 preview |
客户端会为打印模式注入默认 localStorage._printMode_ = true,便于报表页 CSS 做打印态布局。
PDF 生成同样走 TaskQueue,与打印队列独立计数,便于监控瓶颈在「转 PDF」还是「出纸」。
4.3 阶段 C --- PDF → 纸(printPdf)
Windows :调用 SumatraPDF 命令行(printPDF64.exe 等),典型参数:
-print-to <printerName> -silent -print-settings paper=A4,1x,duplex ...
<file.pdf>
下发前会 resolveCanonicalPrinterName :把 WMI / PowerShell 因 GBK 乱码的打印机名校准为 Electron getPrintersAsync() 可见的 canonical 名,减少「界面选了打印机、命令行打错机」的问题。
统信 UOS / 银河麒麟 :同一 PDF 文件路径,改为调用 CUPS (lp -d 队列名 ...),队列名须与 lpstat -a、系统「打印管理」一致。
printOptions 主要字段:
| 字段 | 含义 |
|---|---|
printerName |
目标队列;省略则用默认机 |
paperFormat |
A4 或驱动支持的自定义表单名 |
copies |
份数 |
duplexMode |
simplex / duplex / duplexshort / duplexlong |
colorful |
彩色 / 黑白 |
scaleMode |
noscale / shrink / fit |
pageRanges |
仅打指定页 |
bin |
纸盒 |
**extraOptions.action = preview** 时,不 spawn 打印进程,而返回 printPreviewUrl`,由前端决定如何打开预览(适合联调版式)。
5. 并发、队列与资源控制
5.1 双队列模型
- generatePdf.taskQueue --- 控制同时运行的 headless Chromium 数量
- printPdf.taskQueue --- 控制同时 spawn 的打印子进程数量
默认基准并发约 5 ,并根据 CPU 核数 + 内存 动态放大(例如内存 > 60GB 时 PDF 队列可到 20)。这样在统信运维机(8 核 16G)与麒麟瘦客户端(4 核 8G)上不会无脑开 20 个 Chromium 把机器拖死。
5.2 子进程生命周期
打印子进程由 ChildProcessPromise 统一管理,带:
- 启动 / 自动关闭 / 强制 kill 计数(可通过 SSE
/api/common/getAnalysisData每秒推送,供运维面板看积压) - 50ms 延迟入队,避免瞬时统计偏差
柜面连续点击「打印」时,任务先入 tasks 排队,再进入 executeTasks;若队列长期不消化,应查:杀毒是否拦截无窗口进程(Windows)、CUPS 是否卡住作业(Linux)。
5.3 日志分层
客户端对三阶段分别记日志,可通过 API 拉取:
- HTML 生成日志
- PDF 生成日志
- 打印下发日志
统信 / 麒麟现场排错时,先看 PDF 日志是否成功------若 PDF 已生成但无纸,问题在 CUPS / 驱动;若 PDF 失败,回头查字体与 headless 组件。
6. 远程打印:WebSocket 第二条链路
除本机 web-print-pdf → 127.0.0.1 外,常见政务场景是:中心服务推送任务 → 柜面浏览器 → 本机客户端。
柜面页维护一条外向 WebSocket(连业务服务器),收到 JSON 后:
javascript
import webPrintPdf from "web-print-pdf";
// 消息必须含 id、timestamp、type
await webPrintPdf._printByRawMessage(data);
_printByRawMessage 会把 payload 原样转发 给本地客户端的标准 handler,等价于在本机执行一次 printHtml 等。
HTTP 轮询变体同理:定时 fetch 取任务后调用同一 API。
安全注意:
- 远程通道应做鉴权、TLS、任务签名;否则任意 WS 推送都可驱动柜面出纸。
- 柜面浏览器与本地客户端必须在同一台机器,远程链路只替代「谁触发」,不替代 Layer 2。
7. 统信 UOS / 银河麒麟专项:CUPS 与环境
7.1 CUPS 前置条件
| 检查 | 命令 / 现象 |
|---|---|
| 调度器运行 | lpstat -r → scheduler is running |
| 队列列表 | lpstat -a |
| 试打 | lp -d 队列名 /tmp/test.pdf |
客户端环境说明(应用内文档)通常覆盖:CUPS 未安装、服务未启、无权限、lp 不在 PATH 等------实施阶段应对照勾选,而不是只验 Web 页面。
7.2 打印机名一致性
Linux 队列表述与业务库字段必须 字节级一致(含空格、中文)。建议在 PoC 阶段:
- 客户端 API 拉取打印机列表
- 下拉框 只展示 API 返回值,不让用户手输
- 打印时
printerName传选中项原串
Windows 侧有额外的乱码修复;Linux 侧主要问题是配置漂移(重装驱动后队列改名)。
7.3 字体与 HTML 转 PDF
统信 / 麒麟若未装常见中文字体,headless 渲染 PDF 时缺字。应对:
- 在 OS 层安装
fonts-noto-cjk等(视发行版包名而定) - 报表 CSS 指定已安装 font-family
- PoC 打印一页含生僻字的样张
7.4 与 Windows 混部时的唯一差异点
前端 npm 依赖、WebSocket 协议、JSON 字段 完全一致 。
集成测试矩阵:
同一 branch、同一 web-print-pdf 版本
× Windows 10/11 样机
× 统信 UOS 某版本
× 银河麒麟某版本
→ 各跑:printHtml、printPdfByUrl、batchPrint(3)、指定非默认机
8. web-print-pdf 集成参考
8.1 安装
bash
npm install web-print-pdf
8.2 最小静默打印
javascript
import webPrintPdf from "web-print-pdf";
await webPrintPdf.printHtml(
document.getElementById("print-root").innerHTML,
{
paperFormat: "A4",
margin: { top: "10mm", bottom: "10mm", left: "10mm", right: "10mm" },
printBackground: true,
},
{
printerName: "办公室-A4", // 来自客户端枚举,勿手写臆测
paperFormat: "A4",
copies: 1,
},
{ action: "print" }
);
8.3 内网 PDF
javascript
await webPrintPdf.printPdfByUrl(
"https://intranet/api/voucher/202406/pdf",
{ paperFormat: "A4" },
{ paperFormat: "A4", duplexMode: "duplexlong" },
{
action: "print",
requestTimeout: 30,
httpHeaders: { Authorization: "Bearer ..." },
}
);
8.4 批量
javascript
await webPrintPdf.batchPrint(
[
{ data: "<div>页1</div>", type: "printHtml" },
{ data: "https://intranet/a.pdf", type: "printPdfByUrl" },
{
data: "https://intranet/report/view",
type: "printHtmlByUrl",
extraOptions: { requestTimeout: 45 },
},
],
{ paperFormat: "A4" },
{ paperFormat: "A4" },
{ action: "print" }
);
8.5 错误处理建议
javascript
try {
await webPrintPdf.printHtml(/* ... */);
} catch (err) {
if (err?.type === "close" && err?.code) {
// 本地客户端未启动或 WebSocket 断开
}
// 展示 err.message,并引导:启动客户端 / 检查 CUPS / 检查 printerName
}
客户端未运行时,WebSocket 连接失败是预期行为,前端应明确提示,而不是无限 loading。
9. 参数速查:pdfOptions vs printOptions
很多集成 bug 来自混淆两层选项:
| 维度 | pdfOptions(Layer 2 转 PDF) | printOptions(Layer 3 进队列) |
|---|---|---|
| 纸张 | A4、自定义宽高、margin | 驱动支持的表单名、纸盒 |
| 颜色 | printBackground(CSS 背景) | colorful 黑白 / 彩色 |
| 方向 | landscape | landscape(内容方向) |
| 页码 | pageRanges | pageRanges |
| 水印 / 页眉页脚 | watermark、headerTemplate | --- |
预览联调 :设 extraOptions.action = "preview",在浏览器打开返回的 printPreviewUrl,确认版式后再改回 print。
完整字段说明见 npm 包 README:https://www.npmjs.com/package/web-print-pdf
10. 故障树(统信 / 麒麟)
用户点击打印
├─ 浏览器报错 WebSocket failed
│ └─ 客户端未安装 / 未启动 / 端口被占
├─ WS 成功但 success:false
│ ├─ message 含 pdf / chrome → 查 PDF 引擎与字体
│ ├─ message 含 printer → 队列名错误或不存在
│ └─ message 含 lp / CUPS → 服务未启、无权限
├─ success:true 但无纸
│ ├─ lpstat -o 无作业 → 其实未下发,查客户端打印日志
│ ├─ 作业 stuck → 驱动、USB、网络打印机离线
│ └─ 作业完成但空白 → 字体 / PDF 内容为空
└─ 批量只出第一张
└─ batchPrint 中 action 不一致或队列并发满
11. 与替代方案的技术对比
| web-print-pdf + 本地客户端 | 浏览器 print() | 纯后端 PDF | |
|---|---|---|---|
| 统信 / 麒麟静默 | ✓(经 CUPS) | ✗ | ✗(不出终端) |
| 指定打印机 | ✓ | 用户手选 | --- |
| 与业务单关联 | ✓(id / 日志) | 弱 | 中 |
| 前端改动量 | npm 一行 import | 小 | 大(另做下载) |
| 运维 | 装客户端 + CUPS | 无 | 无 |
12. 小结
统信 UOS、银河麒麟上的网页静默打印,在技术上是一条清晰的分层链路:
- 浏览器因沙箱不能直连 CUPS;
web-print-pdf负责把业务参数变成本地 WebSocket JSON;- Web打印专家 负责 HTML→PDF→队列,在 Linux 上走 CUPS ,在 Windows 上走 Spooler;
- 生产可用性取决于 队列名、字体、CUPS 状态、并发队列 等工程细节,而非某一版前端框架。
对架构师而言,应把「静默打印」单列为终端能力子系统 (含客户端分发、环境自检、日志、远程推送),而不是塞进某个 Vue 组件里试 print()。
对开发者而言,从 npm 安装 web-print-pdf 开始,在统信 / 麒麟样机上跑通一条 printHtml + 一条 printPdfByUrl,再谈批量与远程,是最省时间的 PoC 路径。
文中行为描述基于 Web打印专家与 web-print-pdf 公开接口;具体 OS 版本、驱动与 npm 版本以项目实测为准。