基於 HTML5 的 Web SCADA 報表


背景

最近在一個 SCADA 項目中遇到了在 Web 頁面中展示設備報表的需求。一個完整的報表,一般包含了篩選操作區、表格、Chart、展板等多種元素,而其中的數據表格是最常用的控件。在以往的工業項目中,所有的表格看起來千篇一律,就是通過數字和簡單的背景顏色變化來展示相關信息。但是現在通過各種移動 App 和 Web 應用的熏陶,人們的審美和要求都在不斷提高,尤其是在 Web 項目中,還采用老式的數字表格確實也有點落伍了。 

如何選擇一個合適的 HTML 前端表格控件?此處可以省略一萬字。哈哈。jQuery、Angular、React 等陣營中的控件庫中都有不少成熟案例,但是這些基於 DOM 的控件也有不足,一個是效率問題:如果在數據量很大表格的中采用自定義的單元格控件,對瀏覽器的負擔實在太重,尤其是移動端。另一個問題是開發效率,上述的控件庫中各自的封裝程度、接口形式都有所不同,但整體上還是要求開發者對 CSS、JS 都有較深的了解。還有控件的復用、嵌入、發布、移植,也都是問題。 

基於上面的考慮,最后采用了基於 Canvas 的 HT。通過 HT 表格控件的自定義渲染接口,以及 Web Worker 的多線程數據模擬,實現的表格控件效果如下: 

http://www.hightopo.com/demo/pagetable/index.html 

開始

首先我們要做的就是結合業務邏輯,對表格中不同列的數據,進行不同的渲染。例如設備歷史信息中的運行時間、停機時間等,比較適合用餅圖來匯總展示,用戶就可以很直觀的從列表上對比出設備的歷史狀況。 我們來看看這一步是怎樣實現的。 

HT 有自己的 DataModel 數據模型,省略了我們對數據狀態管理、時間派發、ui刷新的開發工作。DataModel 容器中的子元素 Data,即是 HT 中最基礎的數據結構,可以映射到不同的ui控件上。在畫布上,Data 可以展示成矢量、圖片或者文字等,在樹形控件上,Data 展示為樹的一個節點。在表格當中每個 Data 對應着表格中的一行 Row。 也就是表格控件自身包含一個 DataModel,在繪制時,將這個 Model 中的每個 Data 都繪制成一行。 不同的列,展示的是該 Data 中的不同屬性。例如我們可以把設備的停機時間,保存到 Data 的 stopping 屬性。 在配置表格的列 Column 信息時,我們可以指定該列的表頭描述“停機時間”,其數據單元格對應 Data 的 Stopping 屬性,以及自定義繪制格式:

{
    name: 'stopping',       //對應的data屬性
    accessType: 'attr',
    align: 'center',        
    color: '#E2E2E2',       //文字顏色
    displayName: '停機',    //表頭描述
    drawCell: pageTable.getDrawLegend('stopping','#E2E2E2')
},

自定義渲染

在單元格的基本顯示格式中,已經默認提供了文本、數組、顏色等類型,可以自動的對數據格式化,並展示為文字或背景顏色等,但是還未滿足我們的個性需求,因此就要將 Column 中的 drawCell 重載為自定義的渲染函數。 drawCell 的參數:function (g, data, selected, column, x, y, w, h, view),其中 g 是 Canvas 的環境信息,data 是該行的數據體,我們根據這些信息,再利用 HTML5 原生的 Canvas API 就可以畫出想要的效果。 

懶得親自直接用 HTML5 的原生接口? HT 提供了對 Canvas API 的封裝接口,包括各種矢量類型以及一些簡單的 Chart。利用該功能,可以輕松組合出復雜的效果,具體介紹可以參考我們的矢量手冊(http://www.hightopo.com/guide/guide/core/vector/ht-vector-guide.html)。

先創建一個對象,該 image 矢量對象負責包含對組合矢量的描述信息,然后將該 image 對象以及 drawCell 的上下文信息,作為參數傳入 ht.Default.drawStretchImage 函數,即可實現自定義繪制。

//drawCell
function (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);
}

因為有多個 Legend 圖例顯示的列,所以我們可以簡單包裝一下,用了一個 getDrawLegend 函數,參數是該列圖例的顏色及 Data 屬性名稱,返回值是 drawCell 函數。

getDrawLegend: function(attr,legendColor){return drawCell}

至此,我們就完成了啟停時間這幾列的自定義繪制: 

“統計”列的餅圖,實際上更簡單。還是利用 HT 的矢量接口,把上述幾項時間數據傳入餅圖矢量結構即可。

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
        }
    ]
};

其他列的渲染過程大同小異。在“風速”列中,我們可以根據風速大小計算一個顏色透明值,來實現同一色系的映射變換,比原來那種非紅即綠的報警表,看起來更舒服一些。在“可用率”列,用 Rect 的不同長度變化,來模擬進度條的效果。在功率曲線中稍微有點不同,因為想實現曲線覆蓋區域的顏色漸變,在 HT 的 lineChart 中沒有找到相關接口,所以直接采用了 Canvas 繪制。 

為了運行效率考慮,在表格的單元格中繪制 Chart,應該追求簡潔大方,一目了然。這幾個 Legend 圖例小矩形,其實是應該畫在表頭的。我為了偷懶,就畫在了單元格,導致畫面顯得有點亂。

Web Worker

眾所周知,瀏覽器的 JS 環境是基於單進程的,在頁面元素較多,而且有很大運算需求的情況下,會導致無法兼顧渲染任務和計算任務,造成頁面卡頓或失去響應。在這種情況,可以考慮使用 Web Worker 的多線程,來分擔一些計算任務。 

Web Worker 是 HTML5 的多線程 API,和我們原來傳統概念中的多線程開發有所不同。Web Worker 的線程之間,沒有內存共享的概念,所有信息交互都采用 Message 的異步傳遞。這樣多線程之間無法訪問對方的上下文,也無法訪問對方的成員變量及函數,也不存在互斥鎖等概念。在消息中傳遞的數據,也是通過值傳遞,而不是地址傳遞。 

在 Demo 中,我們利用 Web Worker 作為模擬后端,產生虛擬數據。並采用前端分頁的方式,從 worker 獲取當前頁顯示條目的相關數據。 在主線程中,創建 Web Worker注冊消息監聽函數。

worker = new Worker("worker.js");    
worker.addEventListener('message', function(e) {  
    //收到worker的消息后,刷新表格
    pageTable.update(e.data);
});

pageTable.request = function(){
    //向worker發送分頁數據請求
    worker.postMessage({
        pageIndex: pageTable.getPageIndex(),
        pageRowSize: pageTable.getPageRowSize()        
    });                 
}; 
pageTable.request();

本處的new Worker創建,對於主線程來說是異步的,等加載完 worker.js,並完成初始化后,該 worker 才是真正可用狀態。我們不需要考慮 worker 的可用狀態,可以在創建語句后直接發送消息。在完成初始化之前向其發送的請求,都會自動保存在主線程的臨時消息隊列中,等 worker 創建完成,這些信息會轉移到 worker 的正式消息隊列。 

在 worker 中,創造虛擬隨機數據,監聽主線程消息,並返回其指定的數據。

self.addEventListener('message', function(e) {
    var pageInfo = getPageInfo(e.data.pageIndex, e.data.pageRowSize);   
    self.postMessage(pageInfo);
}, false);

由於前面提到的無法內存共享,Web Worker 無法操作 Dom,也不適用於與主線程進行大數據量頻繁的交互。那么在生產環境中,Web Worker 能發揮什么作用?在我們這種應用場景,Web Worker 適合在后台進行數據清洗,可以對從后端取到的設備歷史數據進行插值計算、格式轉換等操作,再配合上 HT 的前端分頁,就能實現大量數據的無壓力展示。

分頁

傳統上有后端分頁和前端分頁,我們可以根據實際項目的數據量、網速、數據庫等因素綜合考慮。 

采用后端分頁的話,可以簡化前端架構。缺點是換頁時會有延遲,用戶體驗不好。而且在高並發的情況下,頻繁的歷史數據查詢會對后端數據庫造成很大壓力。 

采用前端分頁,需要擔心的是數據量。整表的數據量太大,會造成第一次獲取時的加載太慢,前端資源占用過多。 

在本項目中,得益於給力的 GOLDEN 實時數據庫,我們可以放心的采用前端分頁。歷史數據插值、統計等操作可以在數據庫層完成,傳遞到前端的是初步精簡后的數據。在數千台設備的歷史查詢中,得到的數據量完全可以一次發送,再由前端分頁展示。 

在某些應用場景,我們會在表格中顯示一些實時數據,這些數據是必須是動態獲取的。類似在 Demo 中的趨勢刷新效果,我們可以在創建表格時批量獲取所有歷史數據,然后再動態向數據庫獲取當前頁所需的實時數據。如果網速實在不理想,也可以先只獲取第一頁的歷史數據,隨后在后台線程慢慢接收完整數據。 

這樣的架構實現了海量數據的快速加載,換頁操作毫無延遲,當前頁面元素實時動態刷新的最終效果。 

還有一些傳統客戶,喜歡在一張完整的大表上進行數據篩選、排序等操作。 

我們可以把 Demo 中的數據總量改成一萬條,單頁數量也是一萬條,進行測試: 

出乎意料的是,HT 面對上萬數據量的復雜表格,輕松經受住了考驗。頁面的滾動、點擊等交互毫無影響,動態刷新沒有延遲,表格加載、排序等操作時,會有小的卡頓,在可接受的程度之內。當然也跟客戶端的機器配置有關。可以想象,幾萬個 Chart的展示以及動態刷新,對於基於dom的控件幾乎是件無法完成的任務。關於 HT 的其他矢量和控件,同樣有高性能特性:http://www.hightopo.com/demo/fan/index.html

后記

如前文所述,我們基於 HT 的表格實現了海量數據的可定制展現,並取得了令人滿意的效果。以下是一些還可以改進的地方。 

在 Demo 中,通過對 HT 表格控件的 drawCell 進行重載,實現了自定義渲染,然后把這些 drawCell 放到了 PageTable 的原型函數中,以供 Column 調用。實際上,更好的辦法應該是把這些常見的 Chart、圖例封裝到 Column 的基本類型中,那樣在配置表格 Column 列時,可以指定 type 為 pieChart 或 lineChart 即可,不需再自行繪制相關矢量。 

對於這些表格中的 Chart,也可以增加一些交互接口,例如可以增加單元格 Tooltip 的自定義渲染功能,在鼠標停留時浮出一個信息量更大的 Chart,可以對指定設備進行更深入的了解。 
界面美觀優化。對 HT 的控件進行顏色定制,可以通過相關接口進行配置:

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);

今后也可以對htconfig進行全局配置,在單獨文件中進行樣式的整體管理,實現外觀樣式與功能的分離,有助於工程管理。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM