找回密码
 会员注册
查看: 12|回复: 0

有赞零售跨平台打印库方案

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
71625
发表于 2024-10-8 12:48:47 | 显示全部楼层 |阅读模式
点击关注“有赞coder”获取更多技术干货哦~作者:鱼干团队:零售前端之前我们介绍了有赞零售小票打印跨平台解决方案,详情请见有赞零售小票打印跨平台解决方案。其中涉及到打印库只是做了简单的介绍。从上次文章至今,打印库也经历了从1.0到2.0的变迁,本文将对打印库的设计与变更有更详细的讲述。一、背景打印是商家在日常经营中不可缺失的行为。打印从实际业务中划分可以分为:小票打印、标签打印、电子面单打印等细分业务。小票打印在实际场景中又可以扩展出:购物小票、退货小票、换货小票、拣货小票、发货小票、交班小票、核销小票、取件小票、存件小票等等;这些小票对应着商家交易履约中的各个环节。在 JS 打印库出来之前,有赞零售已经实现了小票的原生打印库,但在实践遇到了不少痛点。引用之前说的三大痛点:每个端各自实现一套打印流程,方案不统一。导致每次修改都会三端修改,而且 iOS 和 Android 必须依赖发版才可上线,不具有动态性,而且研发效率比较低。打印小票的业务场景比较多,每个业务都自己实现模板封装及打印逻辑,模板及逻辑不统一,维护成本大。多种小票设备的适配,对于每个端来说都要适配一遍。因此原生的打印库不能满足快速发展的打印需求,急需一套能跨平台通用的打印库。二、挑战打印库能够跨端运行一套能够描绘小票的模板不同小票打印机的指令解析三、跨端语言选择经过调研,iOS、Android、Java 都有 JavaScript 运行环境库。iOS 使用 JavaScriptCore 框架,Android 使用 J2V8 框架,Java 中 JDK8 自带 Nashorn 引擎。后续有赞零售 PC 收银采用的是 Electron 框架,自带 V8 执行环境。综上所述,JavaScript 这门语言成了跨平台的首选项。四、打印库的业务边界正常的打印流程如下:业务触发打印需求SDK 容器接收订单数据与模板数据将订单数据与模板数据融合得到融合数据融合数据翻译成对应打印机指令客户端传送打印机指令给打印机打印机接收指令完成打印操作其中步骤 3 与步骤 4 的功能就是打印库所负责的功能。订单数据再抽象就是业务数据,从而可以得到以下公式:模板数据 + 业务数据 = 融合数据融合数据 + 打印机信息 = 指令数据模板数据 = 包含占位符的模板业务数据 = 需要填入模板里的数据融合数据 = 占位符已被填充的模板五、打印库的设计根据业务边界,我们可以将打印库进行分层:模板渲染层:业务数据与模板的拼接融合翻译层:将融合数据解析为打印指令六、模板设计6.1 模板元素要设计一套模板语言,首先要确认模板元素有几种。我们从实际的一张全功能的小票入手进行拆解。以下为常见的一张小票示例:分析以上小票我们可以整理出一张完整小票包含以下内容:元素文本图片二维码条形码换行布局单行单列一行多列排版居左局中居右6.2 模板语言的设计打印库的模板语言在 V1 版本的是 JSON ,而在 V2 版本的里替换成了 HTML 。以下是 V1 模板语言与 V2 模板语言的对比:一行右对齐的中等字号的有赞V1 模板:[ { "content": "有赞", "contentType": "text", "textAlign": "right", "fontSize": "middle", "pagerWeight": 1 }]V2 模板:有赞V1 模板采取 JSON 的背景考虑在于模板直接写成 JSON ,对后续的翻译层的代码逻辑友好,能够直接一对一的进行翻译。在初期小票业务不复杂的情况下,JSON 能够较好地承载这块业务。后续随着小票业务的发展,小票内容复杂度提高,JSON 作为模板语言的缺陷也暴露了出来,举个例子:以下是发货小票商品详情的效果图:V1 模板(JSON)的写法是这样的:[{{#each itemList}}[ { "content": "{{titleSkuDesc}}", "contentType": "text", "textAlign": "left", "fontSize": "default", "pagerWeight": 4 }, { "content": "", "contentType": "text", "textAlign": "left", "fontSize": "default", "pagerWeight": 1 }],[ { "content": "{{toFixed (divide unitPrice 100) 2}}", "contentType": "text", "textAlign": "left", "fontSize": "default", "pagerWeight": 1 }, { "content": "{{quantityDesc}}", "contentType": "text", "textAlign": "center", "fontSize": "default", "pagerWeight": 1 }, { "content": "{{toFixed (divide itemAmount 100) 2}}", "contentType": "text", "textAlign": "right", "fontSize": "default", "pagerWeight": 1 }]{{#each priceDiffInfoList}}{{#if @first}},[ { "content": "", "contentType": "text", "textAlign": "left", "fontSize": "default", "pagerWeight": 1 }, { "content": "实发重量", "contentType": "text", "textAlign": "center", "fontSize": "default", "pagerWeight": 1 }, { "content": "应退差价", "contentType": "text", "textAlign": "right", "fontSize": "default", "pagerWeight": 1 }][ { "content": "", "contentType": "text", "textAlign": "left", "fontSize": "default", "pagerWeight": 1 }, { "content": "实发重量", "contentType": "text", "textAlign": "center", "fontSize": "default", "pagerWeight": 1 }, { "content": "应退差价", "contentType": "text", "textAlign": "right", "fontSize": "default", "pagerWeight": 1 }]{{/if}},[ { "content": "", "contentType": "text", "textAlign": "left", "fontSize": "default", "pagerWeight": 1 }, { "content": "{{divide realWeight 1000}}", "contentType": "text", "textAlign": "center", "fontSize": "default", "pagerWeight": 1 }, { "content": "-{{toFixed (divide diffAmount 100) 2}}", "contentType": "text", "textAlign": "right", "fontSize": "default", "pagerWeight": 1 }]{{/each}}{{#if @last}}{{else}},{ "content": "", "contentType": "newline"},{{/if}}{{/each}}]从上面的例子可以看出 JSON + Handlebars语法 作为模板语言:可读性低维护成本高所以在打印库 V2 版本设计的时候决定将模板语言进行替换,经过多方面调研,最终选定了 HTML 作为 V2 版本的模板语言。HTML 作为模板语言不仅解决了 JSON 模板语言两个缺陷,同时提供了后续的可扩展性。还是以上面发货小票的为例:V2 模板(HTML)写法:{{each itemInfoList}} {{ $value.titleSkuDesc }} {{$value.unitPrice}} {{$value.quantityDesc}} {{$value.itemAmount}} 实发重量 应退差价 {{each $value.priceDiffInfoList}} {{$value.realWeight}} {{$value.diffAmount}} {{/each}}{{if $index!=itemInfoList.length-1}} {{/if}}{{/each}另一大原因是原有的 JSON 模板只能描绘小票这种自上而下的一维信息,而标签打印,杯贴打印等其它打印都是基于坐标的二维打印,原有的模板无法支撑相关的业务,而采用 HTML 之后,借助 CSS 的能力,我们能够轻松地描绘小票、杯贴、价签、条码的打印需求。6.3 模板引擎在实际小票打印中,一套小票模板样式是固定的,但是里面的实际内容是可变的,所以我们需要使用模板引擎来实现相关的替换工作。打印库 V1 版本中模板引擎为 Handlebars,而在打印库 V2 版本里我们替换成 art-template 这款模板引擎。对比 V1 的模板引擎,它拥有以下特性:拥有接近 JavaScript 渲染极限的的性能?在线渲染速度测试?调试友好:语法、运行时错误日志精确到模板所在行支持原生语法和标准语法,更强的表达能力。在 V1 的模板引擎中,要实现判断值是否存在,需要注册一个 Helper 方法,才能使用相关能力,而在 V2 的模板引擎中天然支持。{{if user}} {{user.name}} {{/if}}配合模板引擎,我们可以得到第一个公式:模板数据 + 业务数据 = 融合数据模板数据 储值编号:{{orderNo}}业务数据{ orderNo: "E1278909900990"}融合数据 储值编号:E1278909900990完整的 HTML 模板 储值编号:{{orderNo}} 储值时间:{{createTime | formatDate}} 操作人员:{{operator}} 活动名称 {{ruleName}} 支付方式: {{payDesc}} 储值金额: {{rechargeAmount}} 账户余额: {{balance}} 会员:{{buyerName}} 电话:{{mobile}}www.youzan.com 扫码关注店铺公众号 本次储值赠送白金卡,5张10元优惠券至此一套描绘打印模板的模板语言已经设计完成。七、翻译层模板渲染层帮助我们实现了对打印业务的描绘,打印模板语言与打印机型号协议无关,只与打印业务的类型(小票、标签)有关。而到了翻译层,这一层负责将模板翻译成打印机指令。而要实现相关能力,我们需要对打印机协议有进一步了解。打印机协议从业务形态上分可以分为两大类:票据(小票)打印机与标签打印机。7.1 票据打印机协议目前市面上票据(小票)打印机协议可以分为以下二种。ESC/POS?协议基于?ESC/POS?封装的上层协议目前市面上的 99% 的票据打印机都支持 ESC/POS 协议,是票据打印机的事实标准。而第二种基本都是为了方便开发者使用的二次包装,多存在于云打印机厂商。故我们如果能够实现 模板到 ESC/POS 指令的功能,我们可以做到快速对接大部分票据打印机。而针对第二种情况,打印库提供单独的适配,ESC/POS 协议该打印控制命令(WPSON StandardCode for Printer)是 EPSON 公司自己制定的针式打印机的标准化指令集,现在已成为针式打印机控制语言事实上的工业标准。ESC/POS ?打印命令集是 ESC 打印控制命令的简化版本,现在大多数票据打印都采用 ESC/POS 指令集。7.2 标签打印机协议目前市面上标签打印机协议没有类似 ESC/POS?的通用协议,根据打印库对接的几款标签打印机来看,打印机厂商的提供的协议文档都是对底层协议进行了封装。该协议的特点在于,每一个元素都需要提供 x, y 的坐标以进行定位。这边打印库则提供了 Point 坐标打印协议进行映射标签打印机协议。7.3 HTML 到 ESC/POS 协议指令示范HTML: 有赞等于一行右对齐的中等字号的有赞等于右对齐指令 + 中等字号指令 + 文本16进制编码 + 打印指令打印机指令:1B6102 + 1D2111 + D3D0 + D4DE + 0A右对齐 + 加宽加粗两倍 + 有 + 赞 + 打印并换行以上为 HTML 到 ESC/POS 指令的解析过程。不同于 v1 的 JSON 模板能够方便实现1对1的映射。v2 的 HTML 模板转化到指令中间,需要解析成 AST 以方便我们进行翻译,因为我们需要一个解析 HTML 的库。7.4 HTML 解析库要完成 HTML 模板到打印机指令的过程,我们需要类似于 Babylon 的处理工具。经过调研与比对,这里选择了 unified 这个库。unified 是一个用于处理带有语法树的文本并在它们之间进行转换。选择这个库的原因在于它的生态比较丰富,提供的插件也能较好的满足我们打印库的需求。最终我们的处理流程图如下:rehype.js?是针对 HTML 语言的处理库,通过它我们能够实现对模板的压缩,格式化处理。我们利用它的 Parser 进行 AST 的构建,而 Compiler 则需要我们自己去编写。7.5 编译器在 Compiler 编译器中,我们实现 抽象语法树到 打印机指令。大体上流程如下:编写一个 Compiler ,首先需要对语法树进行解析,语法树的数据结构标准可以从 HTML 语法树格式这里查询。通过解析语法树,我们解析出模板里对应的文本、图片、条形码、二维码等元素。然后我们在代码中实现对应元素到打印机指令的翻译,最终生成完整的打印指令输出。在打印库中,针对不同打印机协议编写对应的 Compiler 实现 AST 到不同打印指令的输出。这样完成了输入同一份模板与打印机信息,输出相对应的打印机指令。7.6 实例模板: 有你有赞输出?ESC/POS?协议1C43001B61001B21001D2100D2BBB6FEC8FDCBC4CEE5C1F9C6DFB0CBBEC5CAAE2020202020202020202020200A1D564200某 A 云打印机协议有你有赞某 B 云打印机协议 有你有赞 八、典型难点在开发打印库过程中,实际会遇上不少难题。接下来我会介绍两个典型难题:图片与小票排版问题8.1 图片问题图片是小票中的重要元素,在之前文章中介绍过打印库本身不处理图片,交于外部处理。原因是打印库运行在模拟的 JS 运行环境库中,没有能力处理图片。下面是一张图片模板示例:我们要翻译一张图片要经过以下步骤:下载图片图片灰度二值化处理翻译打印机指令步骤一,依赖网络连接进行下载图片。步骤二,JavaScript 需要依赖 Canvas 这个对象进行处理。而在 iOS、 Android、Java 的 JavaScript 运行环境库中没有提供这两个能力,这也必然导致了打印库在处理图片中需要交与外部调用者完成步骤一和步骤二。部分自定义协议的打印机自身会处理步骤一与步骤二,打印库就可以直接翻译到对应协议。为什么图片需要进行灰度二值化处理?因为对于票据打印机来说,图片像素点只有打与不打,所以不支持灰度与彩色图片。而我们的图片大多数都是灰度或者彩色图片,因此我们需要进行二值化处理。在 ESC/POS 协议中,打印图片的指令如下:其中 d1~dk就是图片的数据块,并且值只有 0与 1,1表示打印该点,0为不打印该点。图片二值化方案:这部分内容可以参考我们另一篇文章 有赞零售小票打印图片二值化方案。8.2 一行多列排版问题票据打印机原生不支持一行多列的排版,我们需要自己处理一行多列的排版问题。举个例子。如下图:对于打印机来说,这里只有两行数据。如果我们这边不对小票排版进行优化的话,输出实际结果大概如下:品名单价数量金额商品名称(规格)¥5.002份¥10.00所以一行多列的排版需要打印库实现。这里可以通过塞入空格进行排版填充。那么理论上应该塞入多少空格呢,不同纸张类型(58/80mm)大小也是不一样的?这里有一份数据:58mm能够打印32个英文字符,16个中文字符80mm能够打印48个英文字符,24个中文字符根据以上数据,我们可以正确的插入空格保证排版。品名 单价 数量 金额商品名称(规格)¥5.00 2份 ¥10.00还有一种情况,单列塞不下对应内容,比如 80mm 纸张能正常排满的小票,在 58mm 的纸则显示不正常。如下:品名 单价 数量 金额商品名称(规格)¥5.00 2份 ¥10.00分析原因本质在于,品名这一列只占据了 25% 的空间,在商品名称过长的时候,挤压了后续的空间。所以针对这种情况,我们需要进行内容切割。最终排版调整为:品名 单价 数量 金额商品名 ¥5.00 2份 ¥10.00称(规格)九、总结与展望目前在有赞零售中,PC 客户端、Java 端、iOS 端、Android 端都已经完成该打印库的接入,100% 的小票都经过 JS 打印库输出到打印机,已经稳定运行2年有余。价签条码、杯贴打印也统一接入了 JS 打印库,同时支撑了有赞零售自定义价签、自定义小票等一系列复杂的商家需求。在未来的规划里,有赞零售打印库将会对目前业务实践中的痛点进行解决。搭建 Node 打印服务,对外提供相关打印接口,降低业务方的接入成本。统一有赞打印标准,方便 ISV 进行接入有赞打印,利用生态的能力支持更多品牌的打印机。扩展阅读Android 沉浸式解析和轮子使用有赞移动助手App 本地抓包方案有赞Flutter插件开发与发布移动开发者的后端开发入门体验有赞零售小票打印图片二值化方案有赞移动 iOS 组件化(模块化)架构设计实践有赞零售移动端收银商品实践聊聊UI标准化有赞零售 App 离线切换技术方案有赞 Android 编译进阶之路——全量编译提效方案有赞iOS精准测试实践有赞 Android 编译进阶之路 —— 增量编译提效方案Savitar有赞iOS-基于二进制的编译提效策略Vol.295
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-10 06:02 , Processed in 0.578215 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表