后端刚生成了一份 PDF——发票、快递面单、取票凭证——现在需要它从一台实体打印机里出来,全自动,没有人点打印对话框。
window.print() 帮不上忙:它只能在浏览器里跑、需要有人在场,而且输出效果取决于用户的浏览器、驱动和操作系统。服务端打印现实中有三条路,下面逐一给出可运行的代码。
方案一:调用 lp(仅限同一台机器)
如果打印机就装在运行 Node.js 的那台机器上,操作系统的打印队列已经把最难的部分做完了。macOS 和 Linux 上是 CUPS,用 lp 驱动:
import { execFile } from 'node:child_process';
execFile('lp', ['-d', 'Office_Printer', 'invoice.pdf'], (err, stdout) => {
if (err) throw err;
console.log(stdout); // request id is Office_Printer-42 (1 file(s))
});对于自助终端或单点部署,这个方案完全够用,合适就该用它。但它的边界来得很快:
- 打印机必须装在服务器本机。云主机和容器里没有打印机。
- 你只能知道「队列收下了」。纸有没有真的出来——卡纸、离线、缺墨——不自己解析
lpstat就无从得知。 - Windows 没有
lp,要换 PowerShell 或第三方程序,代码失去可移植性。
方案二:直连 IPP 协议
多数网络打印机支持 IPP(互联网打印协议)。用 npm 的 ipp 包可以绕过系统队列,把 PDF 直接发给打印机:
import ipp from 'ipp';
import { readFile } from 'node:fs/promises';
const printer = ipp.Printer('http://192.168.1.50:631/ipp/print');
const data = await readFile('./invoice.pdf');
printer.execute(
'Print-Job',
{ 'operation-attributes-tag': { 'document-format': 'application/pdf' }, data },
(err, res) => console.log(err ?? res)
);问题就藏在那个 IP 地址里:你的服务器必须能连到打印机。应用部署在云上、打印机在办公室 NAT 后面,就得拉 VPN、打隧道,或者把打印机端口暴露到公网(千万别——公网打印机是经典安全漏洞)。而且现实中 IPP 的实现参差不齐:很多廉价热敏机、标签机只实现了「够让你抓狂」的那部分。
方案三:云打印 API
彻底绕开网络问题的模式是:在打印机旁边的任意一台电脑上跑一个轻量 Agent,由它向云端保持一条出站长连接。你的后端只管调用普通 HTTPS API,云端把任务从已建立的连接推下去,Agent 交给本地驱动执行。不需要 VPN、不暴露设备,打印机可以在世界任何角落。
以 PrintBase 为例,一次性准备工作只有两步:
- 注册账号(免费套餐每月 50 个任务,无需绑卡),创建 API Key。
- 在打印机旁的电脑上安装 Agent——Windows、macOS、Linux 都支持。已安装的打印机会自动注册,各分配一个数字
printer_code。
之后从 Node.js 打印一份 PDF 就是一次 HTTPS 调用,连 SDK 都不需要:
import { readFile } from 'node:fs/promises';
const pdf = await readFile('./invoice.pdf');
const res = await fetch('https://api.printbase.cloud/v1/print-jobs', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PRINTBASE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
printer_code: 1000000, // 来自 GET /v1/printers 或控制台
content_type: 'pdf',
content: pdf.toString('base64'),
copies: 1,
options: { duplex: false },
}),
});
const job = await res.json();
// { id: "job_abc", status: "queued" }如果 PDF 本身有可访问的 URL(S3 预签名链接、你的发票接口),可以省掉 base64,让 Agent 自己下载,大文件尤其推荐:
body: JSON.stringify({
printer_code: 1000000,
content_type: 'pdf',
content_url: 'https://example.com/invoices/inv-2041.pdf',
})怎么知道真的打出来了
这是 lp 永远给不了你的部分。任务沿 queued → dispatched → printing → completed 推进,随时可查:
const status = await fetch(
`https://api.printbase.cloud/v1/print-jobs/${job.id}`,
{ headers: { Authorization: `Bearer ${process.env.PRINTBASE_API_KEY}` } }
).then((r) => r.json());
console.log(status.status); // "completed"失败不会是沉默,而是带原因码的状态——AGENT_OFFLINE、PRINTER_OFFLINE、DOWNLOAD_FAILED、PRINT_ERROR、INVALID_CONTENT——你的代码可以据此重试、告警或切换备用打印机。生产环境建议用 Webhook 代替轮询:在控制台订阅后,任务完成或失败时 PrintBase 会回调你的接口,接口暂时不可用还会自动重试。
该选哪个
| 你的情况 | 用哪个 |
|---|---|
| Node.js 和打印机在同一台机器,「发出去就行」可以接受 | lp / 系统打印队列 |
| 打印机和服务器同网段、IPP 实现良好、网络归你管 | 直连 IPP |
| 应用在云上、打印机在门店/仓库/办公室,且需要状态回传 | 云打印 API |
对多数 SaaS 和电商后端来说,诚实的答案是第三个——前两个都默认了一种生产环境里并不存在的网络拓扑。
