12.5 使用 JSON 排序和构建行

迄今为止,我们一直围绕着让服务器端输出更多的HTML,以方便客户端脚本更简洁和更高效。现在,来考虑一种不同的情况:在JavaScript可用的情况下显示一组全新的信息。越来越多成熟的Web应用依赖于JavaScript提供内容,同时也依赖它操作内容。在本章第三个表格排序的例子中,我们也会实现相同的功能。首先,我们来编写两个函数:buildRow()buildRows()。前者用于构建表格中的一行,后者使用$.map()循环遍历数据集中的所有行,在每一行数据上调用buildRow(),如代码清单12-9所示。

代码清单12-9

  1. function buildRow(row) {
  2. var authors = [];
  3. $.each(row.authors, function(index, auth) {
  4. authors[index] = auth.first_name + ' ' + auth.last_name;
  5. });
  6. var html = '<tr>';
  7. html += '<td><img src="images/' + row.img + '"></td>';
  8. html += '<td>' + row.title + '</td>';
  9. html += '<td>' + authors.join(', ') + '</td>';
  10. html += '<td>' + row.published + '</td>';
  11. html += '<td>$' + row.price + '</td>';
  12. html += '</tr>';
  13. return html;
  14. }
  15. function buildRows(rows) {
  16. var allRows = $.map(rows, buildRow);
  17. return allRows.join('');
  18. }

虽然这里用一个函数也可以达到相同的目的,但使用两个独立的函数则可以方便我们在某个时刻单独地构建和插入一个表格行。这两个函数会从一次Ajax请求的响应取得数据,如代码清单12-10所示:

代码清单12-10

  1. $.getJSON('books.json', function(json) {
  2. $(document).ready(function() {
  3. var $table3 = $('#t-3');
  4. $table3.find('tbody').html(buildRows(json));
  5. });
  6. });

关于这段代码,有几个地方需要说明一下。首先,这两个函数是在$(document).ready()外部定义的。通过等待$.getJSON()的回调函数调用$(document).ready(),可以让部分代码不必依赖于DOM而提前执行。

其次,值得一提的是authors数据。这个数据项从服务器返回时是一个对象的数组,每个对象都带有first_namelast_name属性。而其他数据项则不是字符串就是数值。通过循环authors数组(尽管大多数行的这个数组中只有一个对象)拼接起了作者的名和姓。而在$.each()循环后面,又将生成的数组的值通过一个逗号和一个空格连接起来,得到了一个格式规范的名字列表。

buildRow()函数假设从JSON文件取得的文本是安全可靠的。因为需要把<img><td><tr>标签和文本内容连接成一个文本字符串,所以必须保证文本内容中不包含<>&字符。确保HTML字符串安全的一种方式就是在服务器上处理它们,比如把所有<转换成&lt;、把>转换成&gt;、把&转换成&amp;,等等。

 尽管我们满腔热情地使用这两个函数构建起了所有表格行,但其实使用Mustache(https://github.com/janl/mustache.js)或Handlebars(http://www.handlebarsjs.com/)等JavaScript模板系统,可以省去很多手工处理和连接字符串的工作。随着项目规模的增大,使用模板系统的好处会越来越明显。

12.5.1 修改JSON对象

如果我们只想调用一次buildRows()函数,那么目前处理authors数组的方式没有什么问题。然而,我们的计划是每次对行排序之后都要调用一次这个函数,因此最好是能够提前格式化好作者的信息。事实上,我们也可以针对排序来格式化书名和作者信息。这里跟第二个表格有所不同,那个表格中每一行都有一个data-book属性,该属性保存着可以用来排序的数据,而且单元格中也有可以显示的数据。而为第三个表中填充数据的JSON文件则只有一种格式。此时,需要再编写一个函数,在调用构建表格的函数之前,先修改、准备好用于排序和显示的数据,参见代码清单12-11。

代码清单12-11

  1. function prepRows(rows) {
  2. $.each(rows, function(i, row) {
  3. var authors = [],
  4. authorsFormatted = [];
  5. rows[i].titleFormatted = row.title;
  6. rows[i].title = row.title.toUpperCase();
  7. $.each(row.authors, function(j, auth) {
  8. authors[j] = auth.last_name + ' ' + auth.first_name;
  9. authorsFormatted[j] = auth.first_name + ' ' + auth.last_name;
  10. });
  11. rows[i].authorsFormatted = authorsFormatted.join(', ');
  12. rows[i].authors = authors.join(' ').toUpperCase();
  13. });
  14. return rows;
  15. }

通过给这个函数传入JSON数据,我们为表示每一行的对象又添加了两个属性:authorsFormattedtitleFormatted。这两个属性将用于显示表格内容,而原始的authorstitle属性则用于排序。而且,用于排序的属性也已经转换成了全部大写的形式,以确保排序操作不区分大小写。

当我们在$.getJSON()的回调函数中调用这个preRows()函数后,我们把修改后的JSON对象保存在变量rows中,然后基于这个修改后的对象进行排序和构建。这意味着还必须修改buildRow()函数,使其在提前准备数据的基础上能够变得更加简洁,参见代码清单12-12。

代码清单12-12

  1. function buildRow(row) {
  2. var html = '<tr>';
  3. html += '<td><img src="images/' + row.img + '"></td>';
  4. html += '<td>' + row.titleFormatted + '</td>';
  5. html += '<td>' + row.authorsFormatted + '</td>';
  6. html += '<td>' + row.published + '</td>';
  7. html += '<td>$' + row.price + '</td>';
  8. html += '</tr>';
  9. return html;
  10. }
  11. $.getJSON('books.json', function(json) {
  12. $(document).ready(function() {
  13. var $table3 = $('#t-3');
  14. var rows = prepRows(json);
  15. $table3.find('tbody').html(buildRows(rows));
  16. });
  17. });

12.5.2 按需重新构建内容

现在,我们已经为排序和显示准备好了内容。接下来该考虑如何修改列标题和编写排序的代码了,参见代码清单12-13。

代码清单12-13

  1. $.getJSON('books.json', function(json) {
  2. $(document).ready(function() {
  3. var $table3 = $('#t-3');
  4. var rows = prepRows(json);
  5. $table3.find('tbody').html(buildRows(rows));
  6. var $headers = $table3.find('thead th').slice(1);
  7. $headers
  8. .wrapInner('<a href="#"></a>')
  9. .addClass('sort');
  10. $headers.on('click', function(event) {
  11. event.preventDefault();
  12. var $header = $(this),
  13. sortKey = $header.data('sort').key,
  14. sortDirection = 1;
  15. if ($header.hasClass('sorted-asc')) {
  16. sortDirection = -1;
  17. }
  18. rows.sort(function(a, b) {
  19. var keyA = a[sortKey];
  20. var keyB = b[sortKey];
  21. if (keyA < keyB) return -sortDirection;
  22. if (keyA > keyB) return sortDirection;
  23. return 0;
  24. });
  25. $headers.removeClass('sorted-asc sorted-desc');
  26. $header.addClass(sortDirection == 1 ? 'sorted-asc'
  27. : 'sorted-desc');
  28. $table3.children('tbody').html(buildRows(rows));
  29. });
  30. });
  31. });

位于click处理程序中的代码与代码清单12-8中处理第二个表格的相应代码几乎完全相同。唯一的明显区别是这里的每次排序只向DOM中插入一次元素。在第一和第二个表格的例子中,即使是在优化了代码之后,仍然需要排序实际的DOM元素,然后一个一个地循环并将它们按照新的顺序添加到DOM中。例如,在代码清单12-8中,通过循环重新插入表格行的代码如下所示:

  1. $.each(rows, function(index, row) {
  2. $table2.children('tbody').append(row);
  3. });

从性能的角度来看,这种重复性的DOM插入操作是非常费时间的,需要插入的表格行越多,效率就越低。读者可以拿它与代码清单12-13中对应的代码作一比较:

  1. $table3.children('tbody').html(buildRows(rows));

在这里,buildRows()函数返回的是一个表示很多行的HTML字符串,一下子就把所有行插入到了DOM中;没有一个一个地移动现有的行,而是一次性替换所有行。