背景
最近在一個(gè) SCADA 項(xiàng)目中遇到了在 Web 頁面中展示設(shè)備報(bào)表的需求。一個(gè)完整的報(bào)表,一般包含了篩選操作區(qū)、表格、Chart、展板等多種元素,而其中的數(shù)據(jù)表格是最常用的控件。在以往的工業(yè)項(xiàng)目中,所有的表格看起來千篇一律,就是通過數(shù)字和簡單的背景顏色變化來展示相關(guān)信息。但是現(xiàn)在通過各種移動(dòng) App 和 Web 應(yīng)用的熏陶,人們的審美和要求都在不斷提高,尤其是在 Web 項(xiàng)目中,還采用老式的數(shù)字表格確實(shí)也有點(diǎn)落伍了。
如何選擇一個(gè)合適的 HTML 前端表格控件?此處可以省略一萬字。哈哈。jQuery、Angular、React 等陣營中的控件庫中都有不少成熟案例,但是這些基于 DOM 的控件也有不足,一個(gè)是效率問題:如果在數(shù)據(jù)量很大表格的中采用自定義的單元格控件,對(duì)瀏覽器的負(fù)擔(dān)實(shí)在太重,尤其是移動(dòng)端。另一個(gè)問題是開發(fā)效率,上述的控件庫中各自的封裝程度、接口形式都有所不同,但整體上還是要求開發(fā)者對(duì) CSS、JS 都有較深的了解。還有控件的復(fù)用、嵌入、發(fā)布、移植,也都是問題。
基于上面的考慮,最后采用了基于 Canvas 的 HT。通過 HT 表格控件的自定義渲染接口,以及 Web Worker 的多線程數(shù)據(jù)模擬,實(shí)現(xiàn)的表格控件效果如下:
開始
首先我們要做的就是結(jié)合業(yè)務(wù)邏輯,對(duì)表格中不同列的數(shù)據(jù),進(jìn)行不同的渲染。例如設(shè)備歷史信息中的運(yùn)行時(shí)間、停機(jī)時(shí)間等,比較適合用餅圖來匯總展示,用戶就可以很直觀的從列表上對(duì)比出設(shè)備的歷史狀況。 我們來看看這一步是怎樣實(shí)現(xiàn)的。
HT 有自己的 DataModel 數(shù)據(jù)模型,省略了我們對(duì)數(shù)據(jù)狀態(tài)管理、時(shí)間派發(fā)、ui刷新的開發(fā)工作。DataModel 容器中的子元素 Data,即是 HT 中最基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu),可以映射到不同的ui控件上。在畫布上,Data 可以展示成矢量、圖片或者文字等,在樹形控件上,Data 展示為樹的一個(gè)節(jié)點(diǎn)。在表格當(dāng)中每個(gè) Data 對(duì)應(yīng)著表格中的一行 Row。 也就是表格控件自身包含一個(gè) DataModel,在繪制時(shí),將這個(gè) Model 中的每個(gè) Data 都繪制成一行。 不同的列,展示的是該 Data 中的不同屬性。例如我們可以把設(shè)備的停機(jī)時(shí)間,保存到 Data 的 stopping 屬性。 在配置表格的列 Column 信息時(shí),我們可以指定該列的表頭描述“停機(jī)時(shí)間”,其數(shù)據(jù)單元格對(duì)應(yīng) Data 的 Stopping 屬性,以及自定義繪制格式:
{ name: 'stopping', //對(duì)應(yīng)的data屬性 accessType: 'attr', align: 'center', color: '#E2E2E2', //文字顏色 displayName: '停機(jī)', //表頭描述 drawCell: pageTable.getDrawLegend('stopping','#E2E2E2') },
自定義渲染
在單元格的基本顯示格式中,已經(jīng)默認(rèn)提供了文本、數(shù)組、顏色等類型,可以自動(dòng)的對(duì)數(shù)據(jù)格式化,并展示為文字或背景顏色等,但是還未滿足我們的個(gè)性需求,因此就要將 Column 中的 drawCell 重載為自定義的渲染函數(shù)。 drawCell 的參數(shù):function (g, data, selected, column, x, y, w, h, view),其中 g 是 Canvas 的環(huán)境信息,data 是該行的數(shù)據(jù)體,我們根據(jù)這些信息,再利用 HTML5 原生的 Canvas API 就可以畫出想要的效果。
懶得親自直接用 HTML5 的原生接口? HT 提供了對(duì) Canvas API 的封裝接口,包括各種矢量類型以及一些簡單的 Chart。利用該功能,可以輕松組合出復(fù)雜的效果,具體介紹可以參考我們的矢量手冊(cè)
先創(chuàng)建一個(gè)對(duì)象,該 image 矢量對(duì)象負(fù)責(zé)包含對(duì)組合矢量的描述信息,然后將該 image 對(duì)象以及 drawCell 的上下文信息,作為參數(shù)傳入 ht.Default.drawStretchImage 函數(shù),即可實(shí)現(xiàn)自定義繪制。
//drawCellfunction (g, data, selected, column, x, y, w, h, tableView) { var value = data.a(attr); var image = { width: 60, height: 30, comps: [ { type: 'rect', rect: [11,11,8,8], borderWidth: 1, borderColor: '#34495E', background: legendColor, depth: 3 }, { type: 'text', text: value, rect:[30, 0, 30, 30], align: 'left', color: '#eee', font: 'bold 12px Arial' } ]}; ht.Default.drawStretchImage(g, image, 'centerUniform', x, y, w, h); }
因?yàn)橛卸鄠€(gè) Legend 圖例顯示的列,所以我們可以簡單包裝一下,用了一個(gè) getDrawLegend 函數(shù),參數(shù)是該列圖例的顏色及 Data 屬性名稱,返回值是 drawCell 函數(shù)。
getDrawLegend: function(attr,legendColor){return drawCell}
至此,我們就完成了啟停時(shí)間這幾列的自定義繪制:
“統(tǒng)計(jì)”列的餅圖,實(shí)際上更簡單。還是利用 HT 的矢量接口,把上述幾項(xiàng)時(shí)間數(shù)據(jù)傳入餅圖矢量結(jié)構(gòu)即可。
var values = [ data.a('running'), data.a('stopping'), data.a('overhauling') ];var image = { width: 200, height: 200, comps: [ { type: 'pieChart', rect: [20,20, 150, 150], hollow: false, label: false, labelColor: 'white', shadow: true, shadowColor: 'rgba(0, 0, 0, 0.8)', values: values, startAngle: Math.PI, colors: pieColors } ] };
其他列的渲染過程大同小異。在“風(fēng)速”列中,我們可以根據(jù)風(fēng)速大小計(jì)算一個(gè)顏色透明值,來實(shí)現(xiàn)同一色系的映射變換,比原來那種非紅即綠的報(bào)警表,看起來更舒服一些。在“可用率”列,用 Rect 的不同長度變化,來模擬進(jìn)度條的效果。在功率曲線中稍微有點(diǎn)不同,因?yàn)橄雽?shí)現(xiàn)曲線覆蓋區(qū)域的顏色漸變,在 HT 的 lineChart 中沒有找到相關(guān)接口,所以直接采用了 Canvas 繪制。
為了運(yùn)行效率考慮,在表格的單元格中繪制 Chart,應(yīng)該追求簡潔大方,一目了然。這幾個(gè) Legend 圖例小矩形,其實(shí)是應(yīng)該畫在表頭的。我為了偷懶,就畫在了單元格,導(dǎo)致畫面顯得有點(diǎn)亂。
Web Worker
眾所周知,瀏覽器的 JS 環(huán)境是基于單進(jìn)程的,在頁面元素較多,而且有很大運(yùn)算需求的情況下,會(huì)導(dǎo)致無法兼顧渲染任務(wù)和計(jì)算任務(wù),造成頁面卡頓或失去響應(yīng)。在這種情況,可以考慮使用 Web Worker 的多線程,來分擔(dān)一些計(jì)算任務(wù)。
Web Worker 是 HTML5 的多線程 API,和我們?cè)瓉韨鹘y(tǒng)概念中的多線程開發(fā)有所不同。Web Worker 的線程之間,沒有內(nèi)存共享的概念,所有信息交互都采用 Message 的異步傳遞。這樣多線程之間無法訪問對(duì)方的上下文,也無法訪問對(duì)方的成員變量及函數(shù),也不存在互斥鎖等概念。在消息中傳遞的數(shù)據(jù),也是通過值傳遞,而不是地址傳遞。
在 Demo 中,我們利用 Web Worker 作為模擬后端,產(chǎn)生虛擬數(shù)據(jù)。并采用前端分頁的方式,從 worker 獲取當(dāng)前頁顯示條目的相關(guān)數(shù)據(jù)。 在主線程中,創(chuàng)建 Web Worker注冊(cè)消息監(jiān)聽函數(shù)。
worker = new Worker("worker.js"); worker.addEventListener('message', function(e) { //收到worker的消息后,刷新表格 pageTable.update(e.data); }); pageTable.request = function(){ //向worker發(fā)送分頁數(shù)據(jù)請(qǐng)求 worker.postMessage({ pageIndex: pageTable.getPageIndex(), pageRowSize: pageTable.getPageRowSize() }); }; pageTable.request();
本處的new Worker創(chuàng)建,對(duì)于主線程來說是異步的,等加載完 worker.js,并完成初始化后,該 worker 才是真正可用狀態(tài)。我們不需要考慮 worker 的可用狀態(tài),可以在創(chuàng)建語句后直接發(fā)送消息。在完成初始化之前向其發(fā)送的請(qǐng)求,都會(huì)自動(dòng)保存在主線程的臨時(shí)消息隊(duì)列中,等 worker 創(chuàng)建完成,這些信息會(huì)轉(zhuǎn)移到 worker 的正式消息隊(duì)列。
在 worker 中,創(chuàng)造虛擬隨機(jī)數(shù)據(jù),監(jiān)聽主線程消息,并返回其指定的數(shù)據(jù)。
self.addEventListener('message', function(e) { var pageInfo = getPageInfo(e.data.pageIndex, e.data.pageRowSize); self.postMessage(pageInfo); }, false);
由于前面提到的無法內(nèi)存共享,Web Worker 無法操作 Dom,也不適用于與主線程進(jìn)行大數(shù)據(jù)量頻繁的交互。那么在生產(chǎn)環(huán)境中,Web Worker 能發(fā)揮什么作用?在我們這種應(yīng)用場(chǎng)景,Web Worker 適合在后臺(tái)進(jìn)行數(shù)據(jù)清洗,可以對(duì)從后端取到的設(shè)備歷史數(shù)據(jù)進(jìn)行插值計(jì)算、格式轉(zhuǎn)換等操作,再配合上 HT 的前端分頁,就能實(shí)現(xiàn)大量數(shù)據(jù)的無壓力展示。
分頁
傳統(tǒng)上有后端分頁和前端分頁,我們可以根據(jù)實(shí)際項(xiàng)目的數(shù)據(jù)量、網(wǎng)速、數(shù)據(jù)庫等因素綜合考慮。
采用后端分頁的話,可以簡化前端架構(gòu)。缺點(diǎn)是換頁時(shí)會(huì)有延遲,用戶體驗(yàn)不好。而且在高并發(fā)的情況下,頻繁的歷史數(shù)據(jù)查詢會(huì)對(duì)后端數(shù)據(jù)庫造成很大壓力。
采用前端分頁,需要擔(dān)心的是數(shù)據(jù)量。整表的數(shù)據(jù)量太大,會(huì)造成第一次獲取時(shí)的加載太慢,前端資源占用過多。
在本項(xiàng)目中,得益于給力的 GOLDEN 實(shí)時(shí)數(shù)據(jù)庫,我們可以放心的采用前端分頁。歷史數(shù)據(jù)插值、統(tǒng)計(jì)等操作可以在數(shù)據(jù)庫層完成,傳遞到前端的是初步精簡后的數(shù)據(jù)。在數(shù)千臺(tái)設(shè)備的歷史查詢中,得到的數(shù)據(jù)量完全可以一次發(fā)送,再由前端分頁展示。
在某些應(yīng)用場(chǎng)景,我們會(huì)在表格中顯示一些實(shí)時(shí)數(shù)據(jù),這些數(shù)據(jù)是必須是動(dòng)態(tài)獲取的。類似在 Demo 中的趨勢(shì)刷新效果,我們可以在創(chuàng)建表格時(shí)批量獲取所有歷史數(shù)據(jù),然后再動(dòng)態(tài)向數(shù)據(jù)庫獲取當(dāng)前頁所需的實(shí)時(shí)數(shù)據(jù)。如果網(wǎng)速實(shí)在不理想,也可以先只獲取第一頁的歷史數(shù)據(jù),隨后在后臺(tái)線程慢慢接收完整數(shù)據(jù)。
這樣的架構(gòu)實(shí)現(xiàn)了海量數(shù)據(jù)的快速加載,換頁操作毫無延遲,當(dāng)前頁面元素實(shí)時(shí)動(dòng)態(tài)刷新的最終效果。
還有一些傳統(tǒng)客戶,喜歡在一張完整的大表上進(jìn)行數(shù)據(jù)篩選、排序等操作。
我們可以把 Demo 中的數(shù)據(jù)總量改成一萬條,單頁數(shù)量也是一萬條,進(jìn)行測(cè)試:
出乎意料的是,HT 面對(duì)上萬數(shù)據(jù)量的復(fù)雜表格,輕松經(jīng)受住了考驗(yàn)。頁面的滾動(dòng)、點(diǎn)擊等交互毫無影響,動(dòng)態(tài)刷新沒有延遲,表格加載、排序等操作時(shí),會(huì)有小的卡頓,在可接受的程度之內(nèi)。當(dāng)然也跟客戶端的機(jī)器配置有關(guān)??梢韵胂螅瑤兹f個(gè) Chart的展示以及動(dòng)態(tài)刷新,對(duì)于基于dom的控件幾乎是件無法完成的任務(wù)。關(guān)于 HT 的其他矢量和控件,同樣有高性能特性:
后記
如前文所述,我們基于 HT 的表格實(shí)現(xiàn)了海量數(shù)據(jù)的可定制展現(xiàn),并取得了令人滿意的效果。以下是一些還可以改進(jìn)的地方。
在 Demo 中,通過對(duì) HT 表格控件的 drawCell 進(jìn)行重載,實(shí)現(xiàn)了自定義渲染,然后把這些 drawCell 放到了 PageTable 的原型函數(shù)中,以供 Column 調(diào)用。實(shí)際上,更好的辦法應(yīng)該是把這些常見的 Chart、圖例封裝到 Column 的基本類型中,那樣在配置表格 Column 列時(shí),可以指定 type 為 pieChart 或 lineChart 即可,不需再自行繪制相關(guān)矢量。
對(duì)于這些表格中的 Chart,也可以增加一些交互接口,例如可以增加單元格 Tooltip 的自定義渲染功能,在鼠標(biāo)停留時(shí)浮出一個(gè)信息量更大的 Chart,可以對(duì)指定設(shè)備進(jìn)行更深入的了解。
界面美觀優(yōu)化。對(duì) HT 的控件進(jìn)行顏色定制,可以通過相關(guān)接口進(jìn)行配置:
var tableHeader = pageTable.getTablePane().getTableHeader(); tableHeader.getView().style.backgroundColor = 'rgba(51,51,51,1)'; tableHeader.setColumnLineColor('#777');var tableView = pageTable.getTablePane().getTableView(); tableView.setSelectBackground('#3D5D73'); tableView.setRowLineColor('#222941'); tableView.setColumnLineVisible(false); tableView.setRowHeight(30);
今后也可以對(duì)htconfig進(jìn)行全局配置,在單獨(dú)文件中進(jìn)行樣式的整體管理,實(shí)現(xiàn)外觀樣式與功能的分離,有助于工程管理。