HT for Web(http://www.hightopo.com/guide/readme.html)提供了涵蓋通用組件、2D拓撲圖形組件以及3D引擎的一站式解決方案,正如Hightopo官網所表達的我們希望提供:Everything you need to create cutting-edge 2D and 3D visualization. 這個願景從功能上是個相當長的戰線,從設計架構上也是極具挑戰性的,其實HT團隊是非常保守的,我們從不貪多圖大,只做我們感覺自己能得更好,能給用戶綜合體驗更佳的功能,在這樣理念驅動下我們慢慢形成了這樣的願景,慢慢實現了幾個有意義的里程碑,慢慢積累下了不少圖形組件設計上的創新和經驗,我不知道這個系列會寫多少篇,也許永遠也不會結束,也沒有系統的提綱規划,想到什么就寫什么,只希望文章能啟發有興趣的同學對圖形組件設計更深的思考就足夠了。
討論前先設定話題的邊界,HT是基於HTML5的圖形組件庫,因此文章的案例更多會涉及HTML和JavaScript語言,但並不局限於Web前端,設計思想上同樣適用於任何GUI語言平台。完整的前端設計是需要考慮到后台加載並發等因素,但本系列更側重於純客戶端圖形組件,不涉及網絡通訊部分的思考,例如最近阿里無線前端招聘讓談談:講講輸入完網址按下回車,到看到網頁這個過程中發生了什么。這是個能討論出很多方方面面,讓你了解面試者的好話題,但這里討論的話題會與以下關鍵字更為相關:企業應用、Single Page Application、重客戶端交互、監控、MV*等。
如Linus大神所言:Talk is cheap, show me the code. 因此我選擇在話題展開之前,先用HT來擴展定制幾個應用案例,以便大家了解HT組件及其擴展設計思路。
http://www.hightopo.com/guide/guide/core/propertyview/examples/example_custom.html
熟悉Flex的程序員應該都了解Tour de Flex這個包羅萬象的大雜燴,其中的網絡監控拓撲Network Monitor特別其動畫切換效果一直給我很深印象,這里不可能有篇幅實現完整例子,我們僅嘗試實現其展示CPU和MEM的界面部分。
實現的最終效果如上圖所示,模型數據就兩個數值,一個代表CPU占用率,一個代表內存占用率,左側通過HT的圖形組件GraphView自定義了矢量圖形展示,右上角自定義了屬性頁PropertyView的兩單元格的Renderer,右下角兩個Slider可拖動改變CPU和MEN值。
此例子麻雀雖小五臟俱全,三個部分分別采用三種方式實現了自定義組件,同時不同組件共享同一數據源,在呈現的基礎上還支持桌面和移動端的Mouse和Touch的交互,還有不同終端屏幕的組件布局功能。
業務上需要在占用率小於40%時呈現律師,40%-70%時顯示黃色,超過70%時呈現紅色,因此定義了如下getColor的工具函數:
getColor = function(value) { if (value < 40) return '#00A406'; if (value < 70) return '#FFCC00'; return '#A60000'; };
PropertyView上采用的最基礎和原始的方式,通過Canvas畫筆進行單元格的自定義繪制,在注冊PropertyView時重載drawPropertyValue函數即可實現單元格自定義Renderer的繪制
drawFunc = function(g, value, x, y, w, h){ g.fillStyle = '#A1A1A3'; g.beginPath(); g.rect(x, y, w, h); g.fill(); g.fillStyle = getColor(value); g.beginPath(); g.rect(x, y, w * value / 100, h); g.fill(); ht.Default.drawText(g, value + '%', '12px Arial', 'white', x, y, w, h, 'center'); }; propertyView.addProperties([ { displayName: 'CPU', drawPropertyValue: function(g, property, value, rowIndex, x, y, w, h, data, view) { drawFunc(g, data.a('cpu'), x, y, w, h); } }, { displayName: 'MEM', drawPropertyValue: function(g, property, value, rowIndex, x, y, w, h, data, view) { drawFunc(g, data.a('mem'), x, y, w, h); } } ]);
Slider拉條部分直接在HT封裝的組件之上應用,因而無需接觸到最底層的Canvas畫筆繪制,僅需要在onValueChanged時更新leftBackgroud拉條左側顏色即可,其實也可以通過重載Slider的getLeftBackground函數實現:
formPane.addRow(['CPU', { slider: { step: 1, onValueChanged: function(){ var value = this.getValue(); node.a('cpu', value); this.setLeftBackground(getColor(value)); }, value: node.a('cpu') } }], [50, 0.1]); formPane.addRow(['MEM', { slider: { step: 1, onValueChanged: function(){ var value = this.getValue(); node.a('mem', value); this.setLeftBackground(getColor(value)); }, value: node.a('mem') } }], [50, 0.1]);
GraphView部分采用了《HT全矢量化的圖形組件設計》文章介紹的HT自定義的矢量方式來實現圖形效果,這種方式介於以上兩種擴展方式之間,需要自定義繪制效果,但通過HT提供的矢量格式,用戶可采用較為直觀易讀的JSON格式來描述圖形,並通過數據綁定的方式實現模型數據與界面呈現的關聯,避免如第一種自定義renderer的方式,即需要接觸到底層繪制函數,同時業務邏輯代碼與繪制代碼混雜一起不易維護的問題。
ht.Default.setImage('server_image', { width: 300, height: 200, comps: [ { type: "roundRect", rect: [3, 5, 291, 189], background: "#E3E3E3", gradient: "linear.northeast", shadow: true }, { type: "text", text: "CPU", font: "16px Arial", rect: [20, 45, 59, 41] }, { type: "text", text: "MEM", font: "16px Arial", rect: [20, 108, 59, 41] }, { type: "rect", rect: [82, 55, 145, 22], background: "#A1A1A3" }, { type: "rect", rect: { func: function(data) { return [82, 55, 145 * data.a('cpu') / 100, 22]; } }, background: { func: function(data) { return getColor(data.a('cpu')); } } }, { type: "rect", rect: [82, 117, 145, 22], background: "#A1A1A3" }, { type: "rect", rect: { func: function(data) { return [82, 117, 145 * data.a('mem') / 100, 22]; } }, background: { func: function(data) { return getColor(data.a('mem')); } } }, { type: "text", font: "16px Arial", rect: [240, 49, 53, 31], text: { func: function(data) { return data.a('cpu') + '%'; } }, color: { func: function(data) { return getColor(data.a('cpu')); } } }, { type: "text", font: "16px Arial", rect: [240, 108, 47, 39], text: { func: function(data) { return data.a('mem') + '%'; } }, color: { func: function(data) { return getColor(data.a('mem')); } } } ] });
以上代碼注冊了名為server-image的圖片,綁定了attr上的mem和cpu的兩個屬性,因此做完這些手腳架的基礎工作后,應用人員只需要構建ht.Node對象,通過node.setImage('server-image')即可實現該圖元在GraphView上呈現'server-image'描述的矢量效果,並且PropertyView、Slider和GraphView三個組件都通過node的attr上的cpu和mem來顯示界面,這樣當后台獲取到采集的實時數據后,只需要更新到node的attr上的cpu和mem屬性,則界面上的所有組件就會自定更新顯示:
node = new ht.Node(); node.setName('SERVER'); node.setImage('server_image'); node.a({ cpu: 30, mem: 70 }); dataModel.add(node);
當然實際應用中並不需要拉條改變CPU和MEN值,這些值一般通過后台采集實時自動更新僅作為呈現,但有了前端這些組件的一站式支持,我們不需要連接后台也可以很方便在客戶端進行模擬測試,有了這樣的機制我們就可以分離模塊一步步測試,例如我們現在不需要連接服務器也可以測試矢量描述定義的是否正確,數值改變后綠黃紅的業務顏色更新是否正確,各個組件的數據同步是否正常,Mouse和Touch交互是否能正常操作,界面在不同設備屏幕上顯示是否正常等等,這些純客戶端組件的封裝工作都做到位后,你就可以安心連接后台數據進行測試了。
見過太多客戶出問題時只會說:界面顯示不對。這樣的問題描述完全無法定位根源,到底時后台數據庫問題,網絡通訊問題,解析數據問題,設置模型問題還是組件封裝問題?這也是MVC/MVP/MVVM存在的另外一個層面的意義,MV*除了事件派發數據綁定外,能更好的進行呈現、模型和業務邏輯的分工切割進行獨立測試的重要意義,就行TCP/IP七層協議的分類,每個協議層都應該確保正確實現自己的協定約定,並且每一層可進行獨立的測試,這才是可維護可擴展的系統,因此對於HT客戶遇到問題時,我們一般也是一層層的幫忙梳理找根源,如果矢量描述沒問題呈現出錯,那是HT組件庫的問題,如果模擬到Node上的attr數據顯示正確,那就去找找實際運行后台通信解析后的數據是否正確的設置到模型上,這樣一步步的分析很容易就能定位到問題的根源。
以上三種擴展方式各有利弊,我將在下篇中繼續展開分析,本篇結尾上一段該例子在移動終端的運行操作視頻