之前我們介紹了有贊零售小票列印跨平臺解決方案,詳情請見有贊零售小票列印跨平臺解決方案。其中涉及到列印庫只是做了簡單的介紹。從上次文章至今,列印庫也經歷了從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 模板:
<span style="font-size:24px;text-align:right">有贊</span>
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}}
<p>
<span style="font-size: 12px; text-align: left; width: 100%;">
{{ $value.titleSkuDesc }}
</span>
</p>
<p>
<span style="font-size: 12px; text-align: left; width: 33.33%;">
{{$value.unitPrice}}
</span>
<span style="font-size: 12px; text-align: center; width: 33.33%;">
{{$value.quantityDesc}}
</span>
<span style="font-size: 12px; text-align: right; width: 33.33%;">
{{$value.itemAmount}}
</span>
</p>
<p>
<span style="font-size: 12px; text-align: right; width: 33.33%;">
</span>
<span style="font-size: 12px; text-align: center; width: 33.33%;">
實發重量
</span>
<span style="font-size: 12px; text-align: right; width: 33.33%;">
應退差價
</span>
</p>
{{each $value.priceDiffInfoList}}
<p>
<span style="font-size: 12px; text-align: right; width: 33.33%;">
</span>
<span style="font-size: 12px; text-align: center; width: 33.33%;">
{{$value.realWeight}}
</span>
<span style="font-size: 12px; text-align: right; width: 33.33%;">
{{$value.diffAmount}}
</span>
</p>
{{/each}}
{{if $index!=itemInfoList.length-1}}
<br />
{{/if}}
{{/each}
另一大原因是原有的 JSON 模板只能描繪小票這種自上而下的一維信息,而標籤列印,杯貼列印等其它列印都是基於坐標的二維列印,原有的模板無法支撐相關的業務,而採用 HTML 之後,藉助 CSS 的能力,我們能夠輕鬆地描繪小票、杯貼、價籤、條碼的列印需求。
在實際小票列印中,一套小票模板樣式是固定的,但是裡面的實際內容是可變的,所以我們需要使用模板引擎來實現相關的替換工作。列印庫 V1 版本中模板引擎為 Handlebars,而在列印庫 V2 版本裡我們替換成 art-template 這款模板引擎。對比 V1 的模板引擎,它擁有以下特性:
在 V1 的模板引擎中,要實現判斷值是否存在,需要註冊一個 Helper 方法,才能使用相關能力,而在 V2 的模板引擎中天然支持。
{{if user}}
<h2>{{user.name}}</h2>
{{/if}}
配合模板引擎,我們可以得到第一個公式:
模板數據 + 業務數據 = 融合數據
模板數據
<p>儲值編號:{{orderNo}}</p>
業務數據
{
orderNo: "E1278909900990"
}
融合數據
<p>儲值編號:E1278909900990</p>
完整的 HTML 模板
<p>儲值編號:{{orderNo}}</p>
<p>儲值時間:{{createTime | formatDate}}</p>
<p>操作人員:{{operator}}</p>
<hr />
<p>
<span style="text-align:left;width:50%;">
活動名稱
</span>
<span style="text-align:right;width:50%;">
{{ruleName}}
</span>
</p>
<p>
<span style="text-align:left;width:50%;">
支付方式:
</span>
<span style="text-align:right;width:50%;">
{{payDesc}}
</span>
</p>
<p>
<span style="text-align:left;width:50%;">
儲值金額:
</span>
<span style="text-align:right;width:50%;">
{{rechargeAmount}}
</span>
</p>
<p>
<span style="text-align:left;font-size:24px;width:50%;">
帳戶餘額:
</span>
<span style="text-align:right;font-size:36px;width:50%;">
{{balance}}
</span>
</p>
<hr />
<p>會員:{{buyerName}}</p>
<p>電話:{{mobile}}</p>
<qrcode>www.youzan.com</qrcode>
<br />
<p style="text-align:center">掃碼關注店鋪公眾號</p>
<hr />
<p style="text-align:center">本次儲值贈送白金卡,5張10元優惠券</p>
至此一套描繪列印模板的模板語言已經設計完成。
七、翻譯層模板渲染層幫助我們實現了對列印業務的描繪,列印模板語言與印表機型號協議無關,只與列印業務的類型(小票、標籤)有關。而到了翻譯層,這一層負責將模板翻譯成印表機指令。而要實現相關能力,我們需要對印表機協議有進一步了解。印表機協議從業務形態上分可以分為兩大類:票據(小票)印表機與標籤印表機。
7.1 票據印表機協議目前市面上票據(小票)印表機協議可以分為以下二種。
ESC/POS 協議
基於 ESC/POS 封裝的上層協議
目前市面上的 99% 的票據印表機都支持 ESC/POS 協議,是票據印表機的事實標準。而第二種基本都是為了方便開發者使用的二次包裝,多存在於雲印表機廠商。故我們如果能夠實現 模板到 ESC/POS 指令的功能,我們可以做到快速對接大部分票據印表機。而針對第二種情況,列印庫提供單獨的適配,
ESC/POS 協議
該列印控制命令(WPSON StandardCode for Printer)是 EPSON 公司自己制定的針式印表機的標準化指令集,現在已成為針式印表機控制語言事實上的工業標準。ESC/POS 列印命令集是 ESC 列印控制命令的簡化版本,現在大多數票據列印都採用 ESC/POS 指令集。
目前市面上標籤印表機協議沒有類似 ESC/POS 的通用協議,根據列印庫對接的幾款標籤印表機來看,印表機廠商的提供的協議文檔都是對底層協議進行了封裝。該協議的特點在於,每一個元素都需要提供 x, y 的坐標以進行定位。這邊列印庫則提供了 Point 坐標列印協議進行映射標籤印表機協議。
7.3 HTML 到 ESC/POS 協議指令示範HTML:
<p style="font-size:24px;"><span style="text-align:right;">有贊</span></p>
等於
一行右對齊的中等字號的有贊
等於
右對齊指令 + 中等字號指令 + 文本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 實例模板:
<p style="text-align:left;width:50%;font-size:24px;">有你有贊</p>
輸出 ESC/POS 協議
1C43001B61001B21001D2100D2BBB6FEC8FDCBC4CEE5C1F9C6DFB0CBBEC5CAAE2020202020202020202020200A1D564200
某 A 雲印表機協議
<B>有你有贊</B>
某 B 雲印表機協議
<html>
<body>
<label style="font-size:48px;text-align:left;">有你有贊</label>
</body>
</html>
八、典型難點
在開發列印庫過程中,實際會遇上不少難題。接下來我會介紹兩個典型難題:圖片與小票排版問題
8.1 圖片問題圖片是小票中的重要元素,在之前文章中介紹過列印庫本身不處理圖片,交於外部處理。原因是列印庫運行在模擬的 JS 運行環境庫中,沒有能力處理圖片。
下面是一張圖片模板示例:
<img src="https://tech.youzan.com/content/images/2019/03/banner.png"/>
我們要翻譯一張圖片要經過以下步驟:
下載圖片
圖片灰度二值化處理
翻譯印表機指令
步驟一,依賴網絡連接進行下載圖片。步驟二,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 進行接入有贊列印,利用生態的能力支持更多品牌的印表機。
擴展閱讀