Skip to content

小票打印跨平台解决方案

一、背景

作为一家生鲜供应链平台,店铺在每个订单都有打印小票需求,这个是顾客的消费凭证,同时商家也会留档,因此对于我们平台来说,小票打印能力就必不可少。

  • 打印需求端

    以react-native为主体的iOS,安卓系统,手机端,平板端

  • 小票业务场景

    订单,码单,货单等

  • 小票打印机设备类型

image-20231129140515292

过去我们存在的痛点:

  1. 每种打印机类型的实现方案不一样,比如云打印是Java写了一套然后调易联云等接口,a4是前端vue,escpos等一体机是安卓开发
  2. 打印小票的业务场景比较多,每个业务都自己实现模板封装及打印逻辑,模板及触发逻辑不统一,维护成本大。
  3. 多种小票设备的适配,对于每种方案都要分别适配

其中最主要的痛点还是在于第一点,多种方案实现的不统一问题。由于不统一,导致开发和维护的成本成倍级增长。

针对以上痛点,小票打印技术方案需要解决的三个主要问题:

  1. iOS 、安卓需要提供小票样式设置和打印的能力,如何降低小票打印代码的维护和更新成本。
  2. 如何定制显示不同业务场景的小票内容:不同业务场景下的小票信息都不尽相同,比如订单,码单,对账单等不同小票样式不一样,并且支持商铺的自定义字段隐藏和展示,如何一种小票一套模板描述信息。
  3. 如何更灵活的适配多种多样的小票打印机,从连接方式上分为蓝牙连接、 WIFI 连接、云连接,从纸张样式分为 80mm 和 110mm 两种宽度。

打印方案流程图

image-20231229173238951

我们的业务主要是RN,跨平台方案中,我们把他拆成了

  1. 打印机的编译库,这个是纯js,可以用于任何可以执行js的地方 (主要介绍)
  2. 打印机连接库,这个主要是RN连接app

编译库的业务边界

正常的打印流程如下:

  1. 业务触发打印需求
  2. SDK 容器接收订单数据与模板数据
  3. 将订单数据与模板数据融合得到融合数据
  4. 融合数据翻译成对应打印机指令
  5. 客户端传送打印机指令给打印机
  6. 打印机接收指令完成打印操作

其中步骤3与步骤4的功能就是打印库所负责的功能。订单数据再抽象就是业务数据,从而可以得到以下公式:

  • 模板数据 + 业务数据 = 融合数据
  • 融合数据 + 打印机信息 = 指令数据

模板数据 = 包含占位符的模板

业务数据 = 需要填入模板里的数据

融合数据 = 占位符已被填充的模板

编译库的设计

根据业务边界,我们可以将打印库进行分层:

  • 模板渲染层:业务数据与模板的拼接融合
  • 翻译层:将融合数据解析为打印指令

image-20240101210220150

模板渲染层: 用Handlebars,用的HTML结合数据得到最终模板,因为是HTML语法,前端易维护,并且合成后仍然是HTML,可以方便预览最终效果

模板

<p><span style="width:100%;text-align:center;font-size:32px">{{origin.title}}</span></p>

数据

{
 title:'xxx店铺'
}

合成后模板

<p><span style="width:100%;text-align:center;font-size:32px">xxx店铺</span></p>

为了对应后续的排版,我们定义了几种字体大小

**
 * 模板字符大小
 */
export const TEMPLATE_FONT_SIZE = {
  /**
   * 16px
   */
  FONT_SIZE_SM: "16px",
  /**
   * 20px
   */
  FONT_SIZE_XS: "20px",
  /**
   * 24px
   */
  FONT_SIZE_MD: "24px",
  /**
   * 28px
   */
  FONT_SIZE_XM: "28px",
  /**
   * 32px
   */
  FONT_SIZE_LG: "32px"
};

翻译层:因为对应不同类型的打印机,指令不一样,比如escpos类型的是escpos,而a4这种事HTML调用系统打印,易联云这种是自己的标签,所以我们统一讲HTML转换成了抽象语法树,然后用js对象去描述布局,我们将js操作成不同的类型的指令

典型难题

  1. 多行多列显示问题

    在小票中存在货品,重量,单价以多行多列的形式展示,就涉及了换行,

    品名   单价    数量     金额
    商品名称(规格)¥5.00    2
    份  ¥10.00

    分析原因本质在于,品名这一列只占据了25%的空间,在商品名称过长的时候,挤压了后续的空间。所以针对这种情况,我们需要进行内容切割。 最终排版调整为:

    品名   单价    数量     金额
    商品名 ¥5.00  2份  ¥10.00
    称(规
    格)

​ 该纸张一排可以打多少个英文字符,一般中文占两个英文字符,然后每一列占据的宽度,算出该列应该占据的字符数,在结合字符,左右对齐,然后在前面或者后面补上空格,1中文宽度 = 2空格宽度 = 2英文宽度,如果某个单元格换行了,就要先变成多行,找到最大行,其他小于最大行数的,都要用空格补上,最后形成一个二维数据,按行列遍历出来即可。

如果遇到某个单元格字号大的,安装倍率进行换算

  1. 图片问题

    如果是浏览器或者node环境,用node-canvas绘制,RN环境,原生提供幕布,在上面绘制后,在转换

为什么图片需要进行灰度二值化处理?

因为对于票据打印机来说,图片像素点只有打与不打,所以不支持灰度与彩色图片。而我们的图片大多数都是灰度或者彩色图片,因此我们需要进行二值化处理。在 ESC/POS 协议中,打印图片的指令如下:

image-20240101215414934

其中d1~dk就是图片的数据块,并且值只有01,1表示打印该点,0为不打印该点。

上次更新于: