資料呈現是RIA應用中的一個重點應用,各種JavaScript架構也一般都提供了自己的Grid小組件用於呈現表格類資料。而TreeGrid作為一種特殊的Grid,顧名思義,更是兼具了Tree多層級結構以及Grid的多資料項目複雜資料展示的優點,是一種很好的處理複雜多級資料的控制項。然而,無論對於Tree或者TreeGrid,通常由於實現方面的種種限制,對資料的消極式載入只能是針對層級結構而言的,即在展開某一節點時即時請求該節點下的全部子節點。儘管這對於一般的應用情境來說基本可以滿足需求,但在當次級節點下資料結構較複雜,節點繁多的情況下,則可能造成極其嚴重的效能問題。針對這一特定需求,Dojo從1.6開始推出了一個全新的控制項——LazyTreeGrid。
作為Dojo DataGrid的擴充,LazyTreeGrid在整體的資料結構上採用的仍然是MVC結構。LazyTreeGrid的結構
圖1. LazyTreeGrid結構模型
圖1就是TreeGrid的一個基本架構模型,就整體結構而言,LazyTreeGrid與TreeGrid、DataGrid並沒有太大區別。視圖即為使用者直接可見的部分,包括了Grid的表頭、行、列、儲存格及TreeGrid特有的節點展開按鈕等直觀內容,整個TreeGrid通過內容視圖中的虛擬捲軸的滾動事件以及節點展開按鈕Expando的展開或關閉事件的觸發來擷取資料並構建內容。
然而,為了滿足針對次級節點的分頁消極式載入及渲染功能,LazyTreeGrid則需要基於樹狀層級結構要求在Model和View部分進行相應的擴充。下面就基於LazyTreeGrid的資料模型及視圖結構來對其設計思路及實現方式做一個簡單介紹。
LazyTreeGrid的資料模型
正常狀態的樹狀結構資料是層級嵌套模式的,如下例所示:
data = { identifier: 'id',label: 'name', items: [ { id: 'AF',name: 'Africa', children: [ { id: 'EG', name: 'Egypt' }, { id: 'KE', name: 'Kenya', children: [ { id: 'Nairobi', name: 'Nairobi', type: 'city' }, { id: 'Mombasa', name: 'Mombasa',type: 'city' } ] }, ... ] }, ... ]}
與其他的可消極式載入設計的Tree類型應用的資料實現要求類似,為了達到消極式載入次級資料的目的,需要對父節點資料做壓平處理,將其與子節點在結構上進行分離,這樣才可以在進行資料的最初請求時只載入必須的父節點資料,僅在展開父節點時再消極式載入其下的次級資料。另外,在LazyTreeGrid中,對於某些子節點數目很少,不需要消極式載入的情況,這裡也允許存在未被壓平的資料節點,如下例所示:
data = { identifier: 'id',label: 'name', items: [ { id: 'AF', name: 'Africa',children: 10 }, { id: 'EG', name: 'Egypt',children: false }, { id: 'KE',name: 'Kenya', children: [ { id: 'Nairobi',name: 'Nairobi',type: 'city' }, { id: 'Mombasa', name: 'Mombasa',type: 'city' } ] }, ... ]}
LazyTreeGrid所要求的資料結構允許父節點將原有的嵌套資料替換為一個正整數數值或布爾值,用以代表其下的子節點數目或者是否有子節點(false,非正數或者沒有相應屬性都代表該節點沒有子節點)。需要注意的是,LazyTreeGrid要求資料必須擁有一個唯一的主鍵id,這樣才可以通過該id去伺服器端請求相應資料條目的子資料。
在LazyTreeGrid資料模型(model)部分,除了DataGrid原有的DataStore(Dojo資料存放區器)外,由於Dojo本身的DataStore對樹形資料結構的API方面的支援不足,另外增加了一個TreeModel用於提供針對樹形結構資料的特定支援。而為了能夠滿足分頁載入次級資料的要求,LazyTreeGrid實現了一個特殊的TreeModel:dojox.grid.LazyTreeGridStoreModel,其主要功能就是建立一個後台資料擷取協議,通過指定父節點與子節點序列來使伺服器端正確返回相應的分頁資料,在請求次級資料時,LazyTreeGrid將向後台服務端發送類似如下的一條請求:
http://localhost:8080/TreeGrid/FakeDataServlet?parentId=root1&start=0&count=25
在這裡,parentId即為要請求資料的父節點id,而start和count分別代表了請求起始的子節點序列和請求的節點個數。
下面的代碼給出了如何建立一個簡單的LazyTreeGridStoreModel:
// programmaticvar treeModel = new dojox.grid.LazyTreeGridStoreModel({ store: queryReadStore, serverStore: true});// declarative<span data-dojo-type="dojox.grid.LazyTreeGridStoreModel" data-dojo-props="store:queryReadStore, serverStore:true" ></span>
建立一個LazyTreeGridStoreModel需要確定兩個參數:store和serverStore,store用於指定擷取資料的dojo DataStore;serverStore接收一個布爾值,用於確定是否資料由伺服器端傳遞且滿足資料條目是被壓平儲存、傳輸的(儲存在用戶端的資料沒必要採用消極式載入模式)。對於次級資料量不大,不需要分頁載入子資料的情況,使用者也可以選擇使用Dojo原有的dijit.tree.ForestStoreModel。
LazyTreeGrid的視圖(View)
在Dojo1.6之前存在的dojo.grid.TreeGrid,採用的視圖構建方式是認為所有的子節點都是最上級父節點的內容擴充,即在一行之內渲染出所有的展開的子節點結構,如所示:
圖2. Dojox.grid.TreeGrid視圖
在這個圖例中,Grid每行最左側的就是rowSelector——行選擇按鈕,根據rowSelector的分配情況,我們就可以清楚的看出其行結構是按第一級節點進行劃分的。
儘管就整體視圖結構來看這一做法並無不妥,但由於TreeGrid複用了DataGrid的按行結構進行分頁的消極式載入與渲染機制,因此位於當前頁的所有行的內容就都會被一次載入及渲染。那麼當次級節點較多、較複雜的情況下,這種載入,尤其是渲染所帶來的資源消耗以及回應時間就會變得非常突出和難以忍受了。
在LazyTreeGrid中,根據在資料模型中對資料進行的預先處理,在保留層級資訊的基礎上對資料進行了分離處理,這樣就可以在視圖中將各條資料都作為一條獨立資料行進行載入渲染。由圖3中可以看出,每條資料都是單獨的一行,因此,在複用了DataGrid的Virtual Scroller機制的前提下,即使資料中包含了很多的次級節點,也會忽略其層級結構,僅根據對Grid的分頁配置進行劃分,消極式載入與渲染資料條目,從而達到效能上的極大提升。
圖3. LazyTreeGrid視圖
LazyTreeGrid中的其他特性
在樹狀結構資料中,可能會存在不同層級的資料條目中的資料列不完全相同的情況,更常見的是上級資料為概要據,而次級資料則作為詳細資料存在。因此在這一類情況下,使用者有可能需要對不同層級資料定製出不同的表達格式。針對這種需求,LazyTreeGrid分別支援分級合併儲存格設定和分級資料格式化設定。
圖4. LazyTreeGrid執行個體
圖4就是一個基於一個簡單化的儲存管理表格樣本,最頂級資料是儲存空間群組,第二級和第三級資料分別是物理磁碟和虛擬磁碟。在這一樣本中,就根據根級資料和次級資料的資料不一致性做了分級合併儲存格和分級儲存格格式化,使得整體視圖更加清晰明了。
分級合併儲存格需要在聲明LazyTreeGrid時為其添加一個colSpans屬性:
colSpans: {0: [ {start: 0, end: 1}, {start: 2, end: 3, primary: 2}], 1: […], 2:… }
colSpans屬性接收一個JSON對象,該對象中的鍵0/1/2/…分別對應各個層級,其值則用於指定儲存格合并的細節,start代表從第幾個儲存格開始合并,end指合并到第幾個儲存格(都是以0作為起始值),而primary則指示了合并後顯示第幾個儲存格的內容,在沒有指定的情況下,其預設值等於start的值。
而分級儲存格格式化則繼承了與DataGrid的儲存格格式化相同的方式,即分別為各個列設定formatter函數,不同的是,所提供的formatter函數的第三個參數為該行資料所在的層級,下例給出了一個簡單的formatter函數:
var fmtByLevel = function(value, idx, level) { return level == 0 ? "root" : level == 1 ? "2nd" : "3rd";};LazyTreeGrid的使用
下面,我們就基於圖4中的樣本,簡述如何構建一個LazyTreeGrid。
簡單而言,由於LazyTreeGrid繼承於DataGrid,所以其基本建立過程與DataGrid基本一致,主要的區別就是前面提到過的需要在Grid和DataStore之間加入一個TreeModel。下面就是以JS編程方式建立這一LazyTreeGrid的前台程式碼範例。
首先需要對LazyTreeGrid的視圖結構進行定義:
// LazyTreeGrid結構定義var layout = [{ name: "Name", field: "name", width: "150px", formatter: fmtName}, { name: "Status", field: "status", width: "40px", formatter: fmtStatus}, { name: "Capacity", field: "capacity", width: "80px", formatter: fmtCapacity}, { name: "UID", field: "uid", width: "240px"}];
Layout主要定義了Grid的列名、列寬度、相應的取值資料項目等,另外我們也可以看到,針對前三個列分別定義了三個formatter函數,用於定義分級的儲存格格式化。其中的fmtCapacity函數採用了對第一級資料返回一個progressbar的小組件:
// progress bar formattervar fmtCapacity = function (value, idx, level) { return level == 0 new dijit.ProgressBar({ progress: value * 100, maximum: 100, report: function (percent) { return (percent * 100).toFixed(2) + "% Storage Online"; } }): value + "GB"; };
DataStore方面採用的是dojox.data.QueryReadStore,通過同域中的一個servlet擷取資料,並基於這個DataStore建立了一個LazyTreeGridStoreModel來串連到LazyTreeGrid。如下段代碼所示:
// 建立DataStorevar queryReadStore = new dojox.data.QueryReadStore({ id: "queryReadStore", url: "FakeDataServlet"});// 建立TreeModelvar treeModel = new dojox.grid.LazyTreeGridStoreModel({ id: "treeModel", serverStore: true, store: queryReadStore});
最後,我們需要在頁面中放入一個id為'gridContainer'的div作為LazyTreeGrid的容器元素,並在頁面載入完成後對LazyTreeGrid的建立及初始化操作:
// 建立LazyTreeGridvar grid;dojo.addOnLoad(function () { grid = new dojox.grid.LazyTreeGrid({ id: "grid", rowSelector: true, treeModel: treeModel, structure: layout, colSpans: { 0: [{ start: 0, end: 1 }, & nbsp; { start: 2, end: 3, primary: 2 }] } }); dojo.byId('gridContainer').appendChild(grid.domNode); grid.startup();});
至此,我們已經完成了LazyTreeGrid的頁面建立工作,接下來的工作就是建立合適的後台代碼來正確響應LazyTreeGrid的資料請求。
小結
作為Dojo1.6新引入的一個Widget,儘管LazyTreeGrid仍有很多亟待解決的缺陷或問題,如尚不支援初始化及運行時的Expand/Collapse All的功能、可調用API較少等,但它作為一種呈現複雜多層級資料的RIA應用小組件,其對於消極式載入與延遲渲染方面所提出的解決方案還是具有一定的突破性的,具有這方面應用要求的使用者不妨一試。
本文已經首發於InfoQ中文站,著作權,原文為《Dojo中的LazyTreeGrid控制項》,如需轉載,請務必附帶本聲明,謝謝。
InfoQ中文站是一個面向中高端技術人員的線上獨立社區,為Java、.NET、Ruby、SOA、敏捷、架構等領域提供及時而有深度的資訊、高端技術大會如QCon 、線下技術交流活動QClub、免費迷你書下載如《架構師》等。