几乎每一台热敏小票打印机——收银台那台、外卖出单那台、自助点单机里那台——说的都是同一种语言:ESC/POS。它是爱普生定义的指令集,后来成了行业事实标准,佳博、芯烨、Star、Citizen 等主流品牌全都兼容。
理解它之后你会发现:小票打印不是「把一张图片打出来」,而是往打印机写一段字节流。这篇讲清楚这段字节流怎么构造、中文怎么编码、以及三种把它送进打印机的方式。
ESC/POS 是什么
一段 ESC/POS 数据就是普通文本混着控制指令。指令以 ESC(0x1B)或 GS(0x1D)开头,后跟操作码。最常用的几个:
| 指令 | 字节 | 作用 |
|---|---|---|
ESC @ | 1B 40 | 初始化打印机(每张小票开头都该发) |
ESC a n | 1B 61 n | 对齐:0 左 / 1 中 / 2 右 |
ESC E n | 1B 45 n | 加粗:1 开 / 0 关 |
GS ! n | 1D 21 n | 字号放大(宽/高倍数) |
GS V m | 1D 56 m | 切纸(42 00 为留边半切) |
ESC p | 1B 70 ... | 弹开钱箱 |
文本直接写、\n 走纸一行——就这么直白。
用 Node.js 构造一张小票
不需要任何打印机专用 SDK,Buffer 拼接就够了。唯一要注意的是中文编码:小票机的中文字库几乎都是 GBK/GB18030,直接发 UTF-8 会打出乱码,用 iconv-lite 转一下:
import iconv from 'iconv-lite';
const ESC = 0x1b, GS = 0x1d;
const receipt = Buffer.concat([
Buffer.from([ESC, 0x40]), // 初始化
Buffer.from([ESC, 0x61, 0x01]), // 居中
Buffer.from([GS, 0x21, 0x11]), // 双倍字号
iconv.encode('PrintBase 咖啡\n', 'GBK'),
Buffer.from([GS, 0x21, 0x00]), // 恢复字号
iconv.encode('2026-08-12 14:30 单号 #1042\n', 'GBK'),
Buffer.from([ESC, 0x61, 0x00]), // 左对齐
iconv.encode('--------------------------------\n', 'GBK'),
iconv.encode('拿铁 x1 28.00\n', 'GBK'),
iconv.encode('冰美式 x2 36.00\n', 'GBK'),
iconv.encode('--------------------------------\n', 'GBK'),
Buffer.from([ESC, 0x45, 0x01]), // 加粗
iconv.encode('合计 64.00\n', 'GBK'),
Buffer.from([ESC, 0x45, 0x00]),
iconv.encode('\n谢谢惠顾\n\n\n', 'GBK'),
Buffer.from([GS, 0x56, 0x42, 0x00]), // 切纸
]);手写字节适合理解原理;正式项目里可以用 esc-pos-encoder 这类库生成同样的 Buffer,逻辑更可读。对齐金额时记住:58mm 纸宽一行 32 个半角字符,80mm 是 48 个,一个汉字占 2 个半角位。
三种把字节流送进打印机的方式
1. 直连(USB / 网口 / 蓝牙)。 escpos 这类 npm 包可以直接写 USB 或 TCP 9100 端口。前提和所有直连方案一样:你的 Node 进程必须和打印机在同一台机器或同一个局域网。程序跑在云上、打印机在门店,这条路走不通。
2. 装驱动,当普通打印机用。 Windows 驱动会把内容当文档渲染成点阵图再发。能出纸,但你失去了指令层控制——切纸、钱箱、字号都得靠驱动设置碰运气,而且渲染出的小票经常又慢又糊。
3. 云端 RAW 直通。 打印机旁的电脑跑一个 Agent 保持到云端的出站连接;你的后端把 ESC/POS 字节流 base64 后调 API,云端推给 Agent,Agent 原样写进打印机——指令一个字节都不动。这就是 PrintBase 的 content_type: "raw":
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, // 小票机的设备码
content_type: 'raw',
content: receipt.toString('base64'),
}),
});
const job = await res.json();
// { id: "job_abc", status: "queued" }订单落库后调这一下,门店的小票机几秒内出纸——后端在哪个云、门店在哪个城市都无所谓。任务状态沿 queued → dispatched → printing → completed 推进,失败会带原因码(PRINTER_OFFLINE、PRINT_ERROR 等)回传,也可以用 Webhook 让 PrintBase 主动回调你的接口,出单失败立刻知道、立刻补打。
常见坑清单
- 乱码:九成是编码问题。确认转了 GBK,并且打印机的代码页设置和你发的编码一致。
- 不切纸:切纸指令前先发两三个
\n,把内容走出切刀位置再切。 - 金额对不齐:用等宽思路手动补空格,别指望制表符;汉字按 2 个半角位算。
- 打一半停住:base64 后的内容体积会膨胀约 1/3,超长小票注意请求体大小,或者拆单。
