统信 UOS、银河麒麟如何实现网页静默打印?——技术架构深解

统信 UOS银河麒麟 等国产 Linux 桌面上交付 B/S 系统时,「网页静默打印」往往被低估为「前端加个按钮」------直到联调阶段才发现:window.print() 绕不过去、打印机名对不上、任务进了队列却不出纸。

本文从浏览器安全模型本地客户端架构WebSocket 协议HTML→PDF→纸流水线并发与排错 几个层面展开,说明统信 / 麒麟环境下静默打印的完整技术链路,以及前端 npm 包 web-print-pdf 在其中的位置。

npm 包https://www.npmjs.com/package/web-print-pdf

源码https://github.com/weixiaoyi/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 + 驱动队列                             │
└─────────────────────────────────────────────────────────────────┘

设计要点

  1. 业务后端不参与出纸 ------ 打印任务不经过机房,符合政务 / 金融数据不出终端的要求。
  2. Web 代码跨平台一致 ------ 差异收敛在 Layer 2,前端始终 import webPrintPdf from "web-print-pdf"
  3. 本地环回通信 ------ 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:printHtmlprintPdfByUrlbatchPrint
/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 含 datatype,并可单独覆盖 pdfOptions / printOptions / extraOptions;全局选项会与 task 级选项浅合并,task 优先。

约束 :合并后所有 task 的 extraOptions.action 必须一致(要么全 print,要么全 preview),否则服务端抛错------避免一批任务里一半预览一半出纸的歧义状态。


4. 打印流水线:以 printHtml 为例

统信 / 麒麟与 Windows 在 Layer 2 共用同一套产品逻辑,差异仅在最后「把 PDF 交给谁」。

4.1 阶段 A --- HTML 规范化(generateHtml)

  1. 入参 content 若不是完整 HTML 文档,会套入标准模板(standard.html),注入 {``{%body%}}
  2. 写入客户端工作目录,生成本地可访问的 htmlUrl(供 PDF 引擎加载)。
  3. 记录 html 日志:耗时、路径、成功 / 失败。

实践建议 :业务侧尽量传带内联样式的片段,减少对外部 CSS 的依赖;统信桌面若缺字体,此阶段不会报错,但下一阶段 PDF 会出现「豆腐块」。

4.2 阶段 B --- HTML → PDF(generatePdf)

使用 Playwright + Chromium headless (或系统已安装的 Chrome / Edge channel)打开上一步的 htmlUrl,调用 page.pdf() 等价能力,支持:

  • paperFormat / 自定义 width·height
  • marginprintBackgroundlandscape
  • pageRanges --- [{from:1,to:5}, ...]
  • watermarkpageNumber 等后处理

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 文件路径,改为调用 CUPSlp -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 阶段:

  1. 客户端 API 拉取打印机列表
  2. 下拉框 只展示 API 返回值,不让用户手输
  3. 打印时 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、银河麒麟上的网页静默打印,在技术上是一条清晰的分层链路:

  1. 浏览器因沙箱不能直连 CUPS;
  2. web-print-pdf 负责把业务参数变成本地 WebSocket JSON;
  3. Web打印专家 负责 HTML→PDF→队列,在 Linux 上走 CUPS ,在 Windows 上走 Spooler
  4. 生产可用性取决于 队列名、字体、CUPS 状态、并发队列 等工程细节,而非某一版前端框架。

对架构师而言,应把「静默打印」单列为终端能力子系统 (含客户端分发、环境自检、日志、远程推送),而不是塞进某个 Vue 组件里试 print()

对开发者而言,从 npm 安装 web-print-pdf 开始,在统信 / 麒麟样机上跑通一条 printHtml + 一条 printPdfByUrl,再谈批量与远程,是最省时间的 PoC 路径。

文中行为描述基于 Web打印专家与 web-print-pdf 公开接口;具体 OS 版本、驱动与 npm 版本以项目实测为准。