做者:王前、林昊(魚乾)
零售商家的平常經營中,小票打印的場景無處不在,顧客的每筆消費都會收到商家打印出的消費小票,這個是顧客的消費憑證,因此小票的內容對顧客和商家都尤其重要。對於有贊零售應用軟件來講,小票打印功能也是必不可少的,諸多業務場景都須要提供相應的小票打印能力。javascript
過去咱們存在的痛點:前端
<font color=red>其中最主要的痛點仍是在於第一點,多端的不統一問題。因爲不統一,致使開發和維護的成本成倍級增加。</font>java
針對以上痛點,小票打印技術方案須要解決的三個主要問題:git
針對以上三個問題,咱們提出了一個涉及前端、移動端和服務端的跨平臺解決方案,github
架構設計的核心在於經過 JS 實現支持跨平臺的小票解析腳本,並具備動態更新的優點;經過服務端下發可編輯的樣式模板實現小票內容的靈活定製;客戶端啓動 JS 執行器執行 JS 小票腳本引擎(如下簡稱:JS 引擎)並負責打印機設備的鏈接管理。json
JS 引擎主要能力就是處理小票模版和業務數據,將業務數據整合到模版中(處理不了的交給移動端處理,好比圖片),而後將整合模版數據轉換成打印指令返給移動端。後端
* 小票格式中,打印機是一行一行的輸出。那麼基本輸出佈局單位,咱們定義爲 layout * 默認一行有一個內容塊,即一個 layout 裏面有一個 content object * 當一行有多列內容的時候,即一個 layout 裏面包含 N 個 content object 。 各自內容塊有 pagerWeight 表明每一個內容的寬度佔比 * 每一行的後面的是一個佔位符,用數據模型的 key 作佔位
小票 layout 樣式描述:緩存
content block 內容塊:架構
不一樣類型內容所支持的能力:app
這裏使用了 HandleBars.js 做爲模板編譯的庫。此外,目前還額外提供了部分能力支持。
自定義能力:
主要進行適配指令集解析適配,根據鏈接不一樣設備進行不一樣指令解析。目前已適配設備:365wifi 、 sunmi 、 sprt80 、 sprt58 、 wangpos 、 aclas 、 xprinter 。若是鏈接未適配的設備拋出找不到相應打印機解析器 error。
兼容性問題
容錯處理
小票模板的動態編輯和下發,模版動態配置信息存儲和各業務全量模版存儲,提供移動端動態配置信息接口,拉取業務小票模版接口,各業務方業務數據接口。
shopId:店鋪 ID
business:業務方
type:打印內容類型
content:layout 中 content 內容
sortWeight:排序比重,用於輸出模板 layout 順序
shopId:店鋪 ID
business:業務方
type:打印內容類型
params:須要替換填充的內容
{ "business": "shopping", "shopId": 111111, "id": 321, "version": 0, "layouts": [{ "name": "LOGO", "content": "[{\"content\":\"http://www.test.com/test.jpg\",\"contentType\":\"image\",\"textAlign\":\"center\",\"width\":45}]" },{ "name": "電話", "content": "[{\"content\":\"電話:{{mobile}}\",\"contentType\":\"text\",\"textAlign\":\"left\",\"fontSize\":\"default\",\"pagerWeight\":1}]" },...] }
其中相關動態數據後端已經作過整合替換,須要替換的業務數據保留在模板 json 中,等獲取業務數據後由 JS 引擎進行替換。
上面 json 中 http://www.test.com/test.jpg
就是動態整合替換數據,{{mobile}}
是一個須要替換的業務數據。
移動端除了動態模版配置以外,主要的就是打印流程。移動端只須要關心須要打印什麼業務小票,而後去後端拉取業務小票模版和業務數據,將拉取到的數據傳給 JS 引擎進行預處理,返回模版中處理不了的圖片 url 信息,而後移動端進行下載圖片,進行二值轉換,輸出像素的 16 進制字符串,替換原來模版中的 url ,最後將鏈接的打印機類型和處理後的模版傳給 JS 引擎進行打印指令轉換返回給打印機打印。
動態配置小票內容,支持 LOGO 、店鋪數據、營銷活動配置等。左側爲在 80mm 和 58mm 上預覽樣式。經過動態配置模版,實現後端接口模版更新,而後能夠實時同步修改打印內容。網頁零售軟件上動態配置內容和移動端同樣。
該業務流程,移動端徹底脫離數據,只須要作一些額外能力以及傳輸功能,有效解決了業務數據修改依賴移動端發版的問題。 Android 和 iOS 流程統一。
動態化在本解決方案裏是必不可少的一環,實時更新業務數據模板依賴於後端,可是 JS 解析引擎的下發要依靠移動端來實現,爲了及時修復發現的 JS 問題或者快速適配新設備等功能。更新流程圖以下:
這裏說明一下,由於可能會出現執行 JS 的過程當中,正在執行本地 JS 文件更新,致使執行 JS 出錯。因此在完成本地更新後會發送一個通知,告知業務方 JS 已更新完成,這時業務方可根據自身需求作邏輯處理,好比從新加載 JS 進行處理業務。
iOS 使用 JavaScriptCore 框架,Android 使用 J2V8 框架,具體框架的介紹這裏就不說明了。JS 執行器設計包含加載指定 JS 文件,調用 JS 方法,獲取 JS 屬性,JS 異常捕獲。
/** 初始化 JSExecutor @param fileName JS 文件名 @return JSExecutor */ - (instancetype)initWithScriptFile:(NSString *)fileName; /** 加載 JS 文件 @param fileName JS 文件名 */ - (void)loadSriptFile:(NSString *)fileName; /** 執行 JS 方法 @param functionName 方法名 @param args 入參 @return 方法返回值 */ - (JSValue *)runJSFunction:(NSString *)functionName args:(NSArray *)args; /** 獲取 JS 屬性 @param propertyName 屬性名 @return 屬性值 */ - (JSValue *)getJSProperty:(NSString *)propertyName; /** JS 異常捕獲 @param handler 異常捕獲回調 */ - (void)catchExceptionWithHandler:(JSExceptionHandler)handler;
加載 JS 文件方法,能夠加載動態下發的 JS 。邏輯是先判斷本地下發的文件是否存在,若是存在就加載下發 JS ,不然加載 app 中 bundle 裏面的 JS 文件。
- (void)loadSriptFile:(NSString *)fileName{ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); if (paths.count > 0) { NSString *docDir = [paths objectAtIndex:0]; NSString *docSourcePath = [docDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.js", fileName]]; NSFileManager *fm = [NSFileManager defaultManager]; if ([fm fileExistsAtPath:docSourcePath]) { NSString *jsString = [NSString stringWithContentsOfFile:docSourcePath encoding:NSUTF8StringEncoding error:nil]; [self.content evaluateScript:jsString]; return; } } NSString *sourcePath = [[YZCommonBundle bundle] pathForResource:fileName ofType:@"js"]; NSAssert(sourcePath, @"can't find jscript file"); NSString *jsString = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil]; [self.content evaluateScript:jsString]; }
這時候可能會有人疑問,爲何這裏是直接強制加載本地下發 JS ,而不是對比版本優先加載。這裏主要有兩點緣由:
JS 異常捕獲功能,將異常拋出給業務方,可讓調用者各自實現邏輯處理。
因爲模板和數據都在後端,須要拉取兩次接口進行打印,因此須要提供一套緩存機制來提升打印體驗。因爲業務數據須要實時拉取,因此必須走接口,模板相對於業務數據來講,能夠容許必定的延遲。因此,模板採用本地文件緩存,業務數據採用和業務打印頁面掛鉤的內存緩存,業務數據只須要第一次打印是請求接口,從新打印直接使用。
流程圖:
本緩方案存會存在偶現的模板不一樣步問題,在即將打印時,若是網頁後臺修改了模板,就會出現本次打印模板不是最新的,可是在下一次打印時就會是最新的了。因爲出現的概率比較低,模板也容許有一點延遲,因此不會影響總體流程。
對於離線場景,咱們在 app 中存放一個最小可用模板,專門用於離線下小票打印使用。爲何是最小可用模板,由於離線下,業務數據及一些其餘數據有可能不全,因此最小可用模板能夠保證打印出來的數據準確性。
因爲 JS 引擎是不能解析圖片文件的,因此在最初模板中存在圖片連接時,所有由移動端進行處理,而後進行替換。圖片處理主要就是下載圖片,圖片壓縮,二值圖處理,圖片像素點壓縮(打印指令要求),每一個字節轉換成 16 進制,拼接 16 進制字符串。
採用 SDWebImage 進行下載緩存,建立並行隊列進行多圖片下載,每下載成功一張後回到主線程進行後續的相關處理。全部圖片都處理完成或,回調給 JS 引擎進行指令解析。
根據 JS 引擎模板要求的 width(必須是 8 的倍數,後續說明),進行等比例壓縮,轉換成 jpg 格式,過濾掉 alpha 通道。
遍歷每個像素點,進行 RGB 取值,而後算出 RGB 均值與 255 的比值,根據比值進行取值 0 或 255 。這裏沒有使用直方圖尋找閾值 T 的方式進行處理,是出於性能和時間考慮。
因爲打印機指令要求,須要對轉換成二值後的每一個點進行 width 上壓縮,須要將 8 個字節壓縮到 1 個字節,這裏也是爲何圖片壓縮時 width 必須是 8 的倍數的緣由,不然打印出來的圖片會錯位。
由於打印機打印圖片接收的是 16 進制字符串,因此須要將處理後的每一個字節轉換成 16 進制字符,而後拼成一個字符串。
因爲業務場景須要,須要自動打印多張小票,因此設計了屢次打印邏輯。因爲每次打印都是異步線程中,因此不能夠直接循環打印,這裏使用信號量 dispatch_semaphore_t
,在異步線程中建立和 wait 信號量,每次打印完成回調線程中 signal 信號量,實現屢次打印,保證每次打印依次進行。若是中途打印出錯,則終止後續打印。
dispatch_async(dispatch_get_global_queue(0, 0), ^{ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); for (int i = 1; i <= printCount; i++) { if (stop) { break; } [self print:template andCompletionBlock:^(State state, NSString *errorStr) { dispatch_async(dispatch_get_main_queue(), ^{ if (errorStr.length > 0 || i == printCount) { if (completion) { completion(state, errorStr); } stop = YES; } dispatch_semaphore_signal(semaphore); }); }]; dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 15*NSEC_PER_SEC)); } });
本方案已經實施,在零售 app 中使用來看,已經知足目前大部分業務場景及需求,後續的開發及維護成本也會大幅度下降,提升了研發效率,接入新業務小票也比較方便。客戶使用上來講,使用體驗和之前沒有較大差異,同時在處理客戶反映的問題來講,也能夠作到快速修改,實時下發等。不過目前還存在一些不足點,好比說圖片打印的功能,還不能徹底知足全部圖片都作到完美打印,畢竟圖片處理考慮到性能體驗方面;還有模板後續能夠增長版本號,這樣在模板存在異常時也能夠回滾或兼容處理等;再者就是緩存優化能夠後續進一步優化體驗,好比加入模板推送,本地緩存優化等。