最近在做一个网站的改版项目,开了一次需求会(尼玛需求文档都没有,对着PS图呱呱了半天),给了一个月时间,100多个页面,几十个特效,而且最坑爹的是80%都是前端工作。当时内心真的是有千万只草泥马在奔腾。额,扯远了…回到正题,由于之前的项目代码比较老旧而且不易于扩展和维护,所以需要重建项目并且要做到模块化及可配置。网站中用到了大量的表格来显示数据并且这些表格定义有可能经常需要变动,所以领导的意思是最好能将其做成可配置易于维护和改动的。经过一番搜索,最终确定了用一款jquery表格插件Datatables。它是一个高度灵活的工具,可以将任何HTML表格添加高级的交互功能。

至于Datatables简单的应用就不再赘述,可参考它的官方文档。这里讲述一下在项目中应用时遇到的一些问题以及解决办法。

整体框架

由于网站的并发数据访问量比较大,而且已经有了通用的做过抗压处理的数据接口(以web API的形式提供)。所以我们只需要关注怎么实现将数据接口的数据实时动态可配置的呈现出来。
整体框架图

数据绑定与配置文件的定义

数据接口返回的数据格式类似于数据库中一行一行的数据,并且大多数接口是通用的。而且对于返回的数据,不同的表格需要显示的列是不一样的。如下图所示:
数据接口与表格
由于返回的数据并非Json格式的数据,所以数据绑定采用了数组的形式。

为了做到完全的可配置化,定义的配置文件格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
{
"tbReqParams": [
{
"tableName": "table1",
"requestParam": "xxxx",
"columns": "1-2-3-5-4"
},
{
"tableName": "table2",
"requestParam": "xxxx",
"columns": "1-2-5-6-8"
}
],
"creatRowCallback": "renderTableHelper.createRow",
"dtb": {
"columns": [
{
"title": "市场",
"name": "(MarketType)",
"dataIndex": 0
},
{
"title": "代码",
"name": "(Code)",
"dataIndex": 1
},
{
"title": "名称",
"name": "(Name)",
"dataIndex": 2,
"render": "renderCommon.getCommonCodeLink"
},
{
"title": "最新价",
"name": "(Close)",
"dataIndex": 3
},
{
"title": "涨跌幅",
"name": "(ChangePercent)",
"dataIndex": 4,
"render": "renderCommon.getChangePercent"
},
{
"title": "涨跌额",
"name": "(Change)",
"dataIndex": 5
},
{
"title": "最高",
"name": "(High)",
"dataIndex": 6
},
{
"title": "最低",
"name": "(Low)",
"dataIndex": 7
},
{
"title": "换手率",
"name": "(TurnoverRate)",
"dataIndex":8
}
]
}
}

整个配置文件采用了Json的格式。其中”tbReqParams”里的每一条配置代码一个表格,例如:

1
2
3
4
5
{
"tableName": "table1", //table名字,绑定时传送给datatable的table id
"requestParam": "xxxx", //Web API请求数据参数
"columns": "1-2-3-5-4" //需要显示的列以及其顺序,索引从0开始
},

"creatRowCallback": "renderTableHelper.createRow"是用来动态绑定Datatables控件的createdRow事件的,主要用来改变表格中的样式(例如给数据添加颜色,加粗显示等)。主要实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
eval("var createRowCallB =" + selected_table.creatRowCallback);
var t = $(tableParams.tableID).DataTable(
{
......
createdRow: createRowCallB,
}
)
createRow: function (row, data, dataIndex) {
var index = this.renderIndex;
var minus = parseFloat(data[index.CloseIndex]);
var colorClose = getTextColor(minus);
$('td', row).eq(index.CloseIndexDisplay).css('color', colorClose);
}

其中index.CloseIndex以及index.CloseIndexDisplay的定义与作用可见前端数据定制化小节。

1
2
3
4
5
6
7
8
9
10
11
12
13
dtb": {
"columns": [
{
"title": "市场",
"name": "(MarketType)",
"dataIndex": 0
},
{
"title": "代码",
"name": "(Code)",
"dataIndex": 1
},
......

columns为对应的Web API返回的列的定义,title是显示的列头,name用来做定制化数据时使用,dataIndex用来标示该列对应Web API返回的数据的哪一列。

前端数据定制化

由于前端显示的部分列需要做特殊处理,例如名称列需要加跳转链接,而跳转链接是由该条数据的市场(MarketType),代码(Code)和url前缀拼接出来的。这里应用了Datatables中为每一列定义的render函数:

1
2
3
4
5
for (var j = 0; j < selected_table.dtb.columns.length; j++) {
eval("var a = " + selected_table.dtb.columns[j].render);
selected_table.dtb.columns[j].render = a;
...
}

配置文件中定义需要绑定的render函数

1
2
3
4
5
6
{
"title": "名称",
"name": "(Name)",
"dataIndex": 2,
"render": "renderCommon.getCommonCodeLink"
}

在Render函数定义文件Renders.js文件中定义相应函数:

1
2
3
4
5
6
7
8
9
var renderCommon = {
......
getCommonReleateLink: function (data, type, row, meta) {
return getSingleLink(gubaBaseUrl, '', row[meta.settings.nTable.renderIndex.CodeIndex], "股吧") + ' '
+ getSingleLink(zjlBaseUrl, '', row[meta.settings.nTable.renderIndex.CodeIndex], "资金流") + ' '
+ getSingleLink(yanbaoBaseUrl, '', row[meta.settings.nTable.renderIndex.CodeIndex], "研报");
}
......
}

这里需要注意的是meta.settings.nTable.renderIndex.CodeIndex代表的是Code这列在绑定到Datatables之后的数据源中的索引,同样的上一节中提到的index.CloseIndex代表Close这列在绑定到Datatables之后的数据源中的索引。而index.CloseIndexDisplay代表的则是最终显示的时候Close这列的索引,这里大家可能会有疑问为什么CloseIndex和CloseDisplayIndex不一样呢?因为在对Name列做特殊处理的时候需要Code列的数据,所以在绑定Datatables数据源的时候采用的策略是所有的接口列都绑定进去,根据”columns”: “2-3-4-5-6”得到需要显示的列并把它的visible及bVisible属性置为true,而其他的则置为false。这样就造成了CloseIndex和CloseDisplayIndex不一样。
为了动态获取这些索引, 应用了Datatables的preDrawCallback函数。

1
2
3
4
5
6
var t = $(tableParams.tableID).DataTable(
{
......
preDrawCallback: renderCommon.getRenderColumnsIndex,
}
)

Renders.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var renderCommon = {
getRenderColumnsIndex: function (settings) {
settings.nTable.renderIndex = new renderColIndex();
var hidenCols = 0;
for (var i = 0; i < settings.aoColumns.length; i++) {
if (settings.aoColumns[i].bVisible === false)
hidenCols += 1;
//动态设置相应的Index及IndexDisplay
var columnName = settings.aoColumns[i].sName.substr(1, settings.aoColumns[i].sName.length - 2);
eval("settings.nTable.renderIndex." + columnName + "Index = " + i + ";");
eval("settings.nTable.renderIndex." + columnName + "IndexDisplay = " + (i - hidenCols) + ";");
}
this.renderIndex = settings.nTable.renderIndex;
},
......
}

服务器端分页

由于每页最多显示20条数据,如果每次都请求接口的所有数据的话。一来会造成资源浪费,二来也会降低程序的响应速度,所以最好是每次只请求需要的数据。关于Datatables服务器端分页定义及基本实现可见 服务器处理 Server-side processing。当你打开服务器模式的时候,每次绘制表格的时候,DataTables 会给服务器发送一个请求(包括当前分页,排序,搜索参数等等)。DataTables 会向 服务器发送 一些参数 去执行所需要的处理,然后在服务器组装好 相应的数据 返回给 DataTables。
项目的数据返回接口已经存在了很久,并且会被其他项目或程序用到,是不能随便改动的。所以就不能应用常规的服务端分页实现方式。还好,接口支持传递page以及pageSize参数,同时提供能总条数total的返回。这里用到的是模拟服务端分页请求及返回来实现服务端分页功能的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
ajax: function (data, callback, settings) {
$.getScript(reqUrl, function () {
var rawRecords = []; //接口返回的数据
var data1 = eval(jsDataName);
var obj = data1.rank;
var count = data1.pages;
var total = data1.total;
for (var i = 0; i < obj.length; i++) {
var temp = obj[i].split(",");
rawRecords.push(temp);
}
var allRecords = []; //根据后端配置文件重新组合的用来显示的数据
for (var i = 0; i < rawRecords.length; i++) //一行数据
{
var adjustRow = [];
for (var j = 0; j < adjustColumn.length; j++) {
if (adjustColumn[j].dataIndex != -1) {
adjustRow.push(rawRecords[i][adjustColumn[j].dataIndex]);
} else {
adjustRow.push(0);
}
}
allRecords.push(adjustRow);
}
//模拟服务器返回的数据结构
var returnData = {};
returnData.draw = data.draw;//这里直接自行返回了draw计数器,本来应该由后台返回
returnData.recordsTotal = total;//返回数据全部记录
returnData.recordsFiltered = total;//后台不实现过滤功能,每次查询均视作全部结果
returnData.data = allRecords;//返回的数据列表
callback(returnData);
})
}

其实也就是自己封装服务器需要返回的数据(Returned data),并调用calllback。当然,得先开启服务器端分页功能,设置serverSide: true。

一点建议

由于Datatables控件比较复杂,所以要想很好的去掌握它,除了认真的去研读它的文档,最重要的是要善于运用浏览器的调试功能,多加断点,这样才能看清每一个函数或结构所返回的数据,进而得到自己可以用的属性或api。