数据呈现是 RIA 应用中的一个重点应用,各种 JavaScript 框架也一般都提供了自己的 Grid 小部件用于呈现表格类数据。而 TreeGrid 作为一种特殊的 Grid,顾名思义,更是兼具了 Tree 多层级结构以及 Grid 的多数据项复杂数据展示的优点,是一种很好的处理复杂多级数据的控件。然而,无论对于 Tree 或者 TreeGrid,通常由于实现方面的种种限制,对数据的延迟加载只能是针对层级结构而言的,即在展开某一节点时即时请求该节点下的全部子节点。尽管这对于一般的应用场景来说基本可以满足需求,但在当次级节点下数据结构较复杂,节点繁多的情况下,则可能造成极其严重的性能问题。针对这一特定需求,Dojo 从 1.6 开始推出了一个全新的控件——LazyTreeGrid。
LazyTreeGrid 的结构
作为 Dojo DataGrid 的扩展,LazyTreeGrid 在整体的数据结构上采用的仍然是 MVC 结构。
图 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:
// programmatic var 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 formatter var 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。如下段代码所示:
// 创建 DataStore var queryReadStore = new dojox.data.QueryReadStore({ id: "queryReadStore", url: "FakeDataServlet" }); // 创建 TreeModel var treeModel = new dojox.grid.LazyTreeGridStoreModel({ id: "treeModel", serverStore: true, store: queryReadStore });
最后,我们需要在页面中放入一个 id 为’gridContainer’的 div 作为 LazyTreeGrid 的容器元素,并在页面加载完成后对 LazyTreeGrid 的创建及初始化操作:
// 创建 LazyTreeGrid var 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 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。
评论