一、目的

定义出一个专门用于处理二维数据的组件,所谓二维数据就是能用二维表格显示出来的数据,所谓处理就是增删改查,很简单。

二、约束

外部程序给该组件传入如下形式的对象,让该组件自行解析。

  1. var testData = {
  2. metadata: [{name: 'fid', label: 'fid', datatype: 'string', visible: 'false'},
  3. {name: 'fName', label: '名称', datatype: 'string', visible: 'true'},
  4. {name: 'fAge', label: '年龄', datatype: 'int', visible: 'true'}],
  5. data: [{fid: 'id_1', fName: 'yi', fAge: 25},
  6. {fid: 'id_2', fName: 'anna', fAge: 24},
  7. {fid: 'id_3', fName: 'kate', fAge: 26}]
  8. };

testData分为metadata和data两部分,两部分均为数组。

metadata内的每一个元素可以对应于表格的一列,持有列标识(键名暂默认为name),列标签,数据类型,是否可见等属性,并不仅限于传入以上属性,外部程序想传入什么就传入什么,组件只管解析和缓存,不管功能。

data内每一个元素的键集合就是所有metadata的列标识,值就是对应的数据。

可以用表格把testData展现为如下外观,其中列明对应于metadata的列标签值(label)

fid 名称 年龄
id_1 yi 25
id_2 anna 24
id_3 kate 26

三、实现思路

组件命名为DataModel,仅有一个实例,获取方式为DataModel.getInstance。并提供以下公共方法:

parse: 解析数据

size: 获取数据数量

getData(index, prop): 根据索引和属性获取数据值

getMetadata(key, prop): 根据键和属性获取元数据属性值

foreach(type, fn): 迭代数据或元数据。其中type仅限于'metadata'和'data',迭代过程中会给fn传入迭代对象

update(index, prop, newValue[, callback, url]): 更新数据,返回布尔值。其中callback会在更新成功后调用,url是服务端更新数据地址

addView(view): 添加视图,view必须定义render方法,DataModel更新后(update或parse执行)会触发调用,传入更新信息,通知视图更新

四、单元测试

这里采用TDD的方式,先写测试用例,再写实现。单元测试用例可以在一定程度上减轻手工测试的负担,快速验证基本功能的正确性。并且用例覆盖的场景越全面,质量的有力保障手段,有了它,对代码的修改不再像过去那般战战兢兢了。

1、场景设置

parse
预处理:插入预设数据

size

场景1:获取数据size
getData
场景2:根据索引和属性获取数据值
getMetadata
场景3:根据键和属性获取元数据属性值
foreach
场景4:迭代元数据
场景5:迭代数据
update
场景6:给字符串类型的字段更新数据
场景7:给int类型的字段更新字符串数据
场景8:给int类型的字段更新int数据
场景9:更新一个等于旧值的数据
addView
场景10:更新数据,所有注册于DataModel上的视图都会得到通知

2、qunit测试框架

这是jQuery团队所使用的单元测试框架,轻量级,入门简单,基本用法可以参阅cookbook,这里不作详细介绍。

测试DataModel的代码如下所示:

  1. /**
  2. * 测试DataModel
  3. */
  4.  
  5. // 预处理:插入预设数据
  6. var testData = {
  7. metadata: [{name: 'fid', label: 'fid', datatype: 'string', visible: 'false'},
  8. {name: 'fName', label: '名称', datatype: 'string', visible: 'true'},
  9. {name: 'fAge', label: '年龄', datatype: 'int', visible: 'true'}],
  10. data: [{fid: 'id_1', fName: 'yi', fAge: 25},
  11. {fid: 'id_2', fName: 'anna', fAge: 24},
  12. {fid: 'id_3', fName: 'kate', fAge: 26}]
  13. };
  14.  
  15. // 获取DataModel实例
  16. var model = DataModel.getInstance();
  17. model.parse(testData);
  18.  
  19. // 测试parse效果
  20. // -场景1:获取数据size
  21. test('data size', function(){
  22. var size = model.size();
  23. equal(size, 3, 'get data size');
  24. });
  25.  
  26. // -场景2:根据索引和属性获取数据值
  27. test('get data value by index and prop', function(){
  28. var data_1 = model.getData(1, 'fName'),
  29. data_2 = model.getData(2, 'fAge');
  30. strictEqual(data_1, 'anna', 'OK, name is ' + data_1);
  31. strictEqual(data_2, 26, 'OK, age is ' + data_2);
  32. });
  33.  
  34. // -场景3:根据键和属性获取元数据属性值
  35. test('get metadata value by key and prop', function(){
  36. var val_1 = model.getMetadata('fid', 'visible'),
  37. val_2 = model.getMetadata('fName', 'label'),
  38. val_3 = model.getMetadata('fAge', 'datatype');
  39. strictEqual(val_1, false, 'OK, visible is ' + val_1);
  40. strictEqual(val_2, '名称', 'OK, label is ' + val_2);
  41. strictEqual(val_3, 'int', 'OK, datatype is ' + val_3);
  42. });
  43. // -测试foreach
  44. test('test foreach', 7, function(){
  45. var i = 0;
  46. // 场景4:迭代元数据
  47. model.foreach('metadata', function(obj){
  48. var result = {name: obj.name, label: obj.label, datatype: obj.datatype, visible: obj.visible};
  49. deepEqual(result, testData.metadata[i], 'for each one metadata testing');
  50. i++;
  51. });
  52. // 场景5:迭代数据
  53. i = 0;
  54. model.foreach('data', function(obj){
  55. deepEqual(obj, testData.data[i], 'for each one data testing');
  56. i++;
  57. });
  58. try{
  59. model.foreach('other', function(obj){
  60. ok(false, "shouldn't be called");
  61. });
  62. }catch(e){
  63. ok(true, 'should reach here');
  64. }
  65. });
  66.  
  67. // 写入数据
  68. // -更新数据, 预期断言数是8,更新失败后回调不应该被调用,里面的断言也不会被执行。
  69. test('update data by index, prop and new value', 9, function(){
  70. var newValue = 'yi_2', newValue_int = 30;
  71. // 场景6:给字符串类型的字段更新数据
  72. var result = model.update(0, 'fName', newValue);
  73. strictEqual(result, true, 'update string value success');
  74. // 场景7:给int类型的字段更新字符串数据
  75. result = model.update(1, 'fAge', newValue, function(obj){
  76. ok(false, "callback function shouldn't be called if update failed");
  77. });
  78. strictEqual(result, false, 'OK, update string value into int field failed');
  79. // 更新失败,保留的还是旧值
  80. var data = model.getData(1, 'fAge');
  81. strictEqual(data, 24, 'ok, value is not changed if update failed');
  82. // 场景8:给int类型的字段更新int数据
  83. result = model.update(1, 'fAge', newValue_int, function(obj){
  84. strictEqual(obj.index, 1, 'in calllback obj index is right');
  85. strictEqual(obj.newValue, 30, 'in callback obj newValue is right');
  86. strictEqual(obj.prop, 'fAge', 'in callback obj prop is right');
  87. });
  88. strictEqual(result, true, 'OK, update int value into int field success');
  89. // 更新成功,保留的是新值
  90. data = model.getData(1, 'fAge');
  91. strictEqual(data, 30, 'ok, value is changed if update success');
  92. // 场景9:更新一个等于旧值的数据
  93. result = model.update(1, 'fAge', 30, function(obj){
  94. ok(false, "this shouldn't be called if newValue is equal to oldValue");
  95. });
  96. strictEqual(result, true, "update should return true if old value is equals to new value");
  97.  
  98. });
  99. // -更新数据,检验渲染视图
  100. test('render data after update data model', 8, function(){
  101. // 场景10:更新数据,所有注册于DataModel上的视图都会得到通知
  102. var view1 = {render: function(obj){checkRender(obj);}},
  103. view2 = {render: function(obj){checkRender(obj);}};
  104. model.addView(view1).addView(view2);
  105. model.update(1, 'fAge', 28);
  106. function checkRender(obj){
  107. strictEqual(obj.id, 'id_2', 'check id');
  108. strictEqual(obj.index, 1, 'check index');
  109. strictEqual(obj.prop, 'fAge', 'check prop');
  110. strictEqual(obj.newValue, 28, 'check newValue');
  111. }
  112. });

严格来说,流程走到这里,因为还没有写实现,应该会报找不到符号之类的错才对。

还有一点要提一下,可以看出,把每个测试用例展开后显示的绿色字,就是给每个断言方法传入的第三个字符串参数。这个字符串更好的写法也许应该是对应于刚才列出的场景。这里没有改,我也是写到这里才想到这一点的。另外我也是第一次用这个东西,所以应该还会有更好的规范写法是我没有发现的,这点请大家自行思考。

五、具体实现

这里贴出DataModel的代码,还有很多功能点没有实现,例如删除,校验,还有parse的处理方式实在太蹩脚了,data只是单纯的赋值,按照数据类型校验或转换啥的都没有。这里的目的更多的是为了表达一种思想。另外这里贴出的肯定也不是最好的方式,因为本人也是缺少经验的js初学者,写的过程中经常发现这里不对,那种处理方式更好一些,大大小小的重构是经常的。这里之所以要写测试用例也是因为这个原因,管我怎么摆弄这些代码,写好了就run一下测试,只要公共接口能正确跑对就ok。

  1. var DataModel = (function(){
  2. // 惰性加载
  3. var _instance;
  4. return {
  5. getInstance: function(){
  6. if(!_instance)
  7. _instance = constructor();
  8. return _instance;
  9. }
  10. };
  11. function constructor(){
  12. var metadata, data,
  13. viewList = [];
  14. function renderView(obj){
  15. for(var i=0, len=viewList.length; i<len; i++){
  16. viewList[i].render(obj);
  17. }
  18. }
  19. return {
  20. parse: function(obj){
  21. if(obj && obj.metadata){
  22. metadata = {};
  23. var meta, name, i, len;
  24. for(i=0, len=obj.metadata.length; i<len; i++){
  25. meta = obj.metadata[i];
  26. name = meta.name;
  27. metadata[name] = new Metadata(meta);
  28. }
  29. // 注意:data是可以被清空的
  30. data = obj.data;
  31. // 给注册的视图发送消息
  32. renderView();
  33. }else{
  34. throw new Error('the param is invalid');
  35. }
  36. },
  37. // 从服务端取数据
  38. fetchDataFromServer: function(url){
  39. var self = this;
  40. yi.xhr.request('POST', url, {
  41. success: function(responseText){
  42. if(responseText){
  43. var obj = eval("(" + responseText + ")");
  44. self.parse(obj);
  45. }
  46. }
  47. });
  48. },
  49. size: function(){
  50. if(data)
  51. return data.length;
  52. else
  53. throw new Error('data is null');
  54. },
  55. getData: function(index, prop){
  56. if(data)
  57. return data[index][prop];
  58. else
  59. throw new Error('data is null');
  60. },
  61. getMetadata: function(key, prop){
  62. if(metadata)
  63. if(metadata[key])
  64. if(typeof metadata[key][prop] == 'boolean' || metadata[key][prop]){
  65. var val = metadata[key][prop];
  66. if(val == 'true' )
  67. return true;
  68. else if(val == 'false')
  69. return false;
  70. else
  71. return val;
  72. }
  73. throw new Error('metadata[' + key + '] has not the prop: ' + prop);
  74. throw new Error('metadata has not the key: ' + key);
  75. throw new Error('metadata is null');
  76. },
  77. update: function(index, prop, newValue, callback, url){
  78. // 新旧值如果相等就直接返回true可以了
  79. var oldValue = data[index][prop];
  80. if(oldValue === newValue)
  81. return true;
  82. var datatype = this.getMetadata(prop, 'datatype');
  83. // 根据数据类型校验
  84. switch(datatype){
  85. case 'int':
  86. if(isNaN(parseInt(newValue)) || parseFloat(newValue) != parseInt(newValue)){
  87. return false;
  88. }
  89. newValue = parseInt(newValue);
  90. break;
  91. case 'float':
  92. case 'number':
  93. if(isNaN(newValue = parseFloat(newValue))){
  94. return false;
  95. }
  96. break;
  97. }
  98. // 更新数据
  99. data[index][prop] = newValue;
  100. // 通知服务端更新数据
  101. if(url){
  102. var id = data[index]['fid'],
  103. req = 'id=' + id + '&key=' + prop + '&value=' + newValue,
  104. self = this;
  105. yi.xhr.request('post', url, {
  106. failure: function(){return false;}
  107. }, req);
  108. }
  109. var obj = {'id': data[index]['fid'], 'index': index, 'prop': prop, 'newValue': newValue};
  110. // 给注册的视图发送消息
  111. renderView(obj);
  112. // 更新后回调
  113. if(callback) callback(obj);
  114. return true;
  115. },
  116. // 遍历metadata或者data
  117. foreach: function(type, fn){
  118. if(!fn) throw new Error('param[fn] is required.');
  119. if(type === 'metadata'){
  120. for(var key in metadata){
  121. fn(metadata[key]);
  122. }
  123. }else if(type === 'data'){
  124. for(var i=0, len=data.length; i<len; i++){
  125. fn(data[i]);
  126. }
  127. }else{
  128. throw new Error('param[type] is only limit in "metadata" and "data".');
  129. }
  130. },
  131. // 添加视图
  132. addView: function(view){
  133. for(var i=0, len=viewList.length; i<len; i++){
  134. if(view == viewList[i])
  135. return this;
  136. }
  137. viewList.push(view);
  138. return this;
  139. }
  140. };
  141. }
  142. })();
  143.  
  144. // 元数据数据结构
  145. function Metadata(config){
  146. for(var prop in config){
  147. var val = config[prop];
  148. this[prop] = val;
  149. }
  150. // name属性必须指定
  151. if(!this['name'])
  152. throw new Error('name must be provided!');
  153. }
  154. Metadata.prototype = {
  155. constructor: Metadata,
  156. // 默认属性
  157. label: 'Default Label',
  158. datatype: 'string',
  159. visible: true
  160. };

简单说明下这段代码的结构:

1、新建DataModel变量

首字母大写,因为它是个类。该变量的值由以下的结构返回

  1. var DataModel = (function(){
  2. // 惰性加载
  3. var _instance;
  4. function constructor(){}
  5. return {
  6. getInstance: function(){
  7. if(!_instance)
  8. _instance = constructor();
  9. return _instance;
  10. }
  11. };
  12. })();

=号右边的是一个定义后立刻执行的函数,起到闭包的作用(外部不能访问私有成员_instance和constructor;getInstance方法可以访问_instance和constructor,外部可以访问getInstance;这两个私有成员值不会消失)。

getInstance里面是经典的单例模式写法。由于constructor有可能是一个比较耗费资源的操作,因此不必在页面加载的时候立刻执行,延迟到用户需要的时候,再通过getInstance获取。

2、另一个闭包,constructor函数

  1. function constructor(){
  2. var metadata, data,
  3. viewList = [];
  4. function renderView(obj){
  5. for(var i=0, len=viewList.length; i<len; i++){
  6. viewList[i].render(obj);
  7. }
  8. }
  9. return{
  10. // parse
  11. // fetchDataFromServer
  12. // size
  13. // getData
  14. // ...
  15. };
  16. }

外部程序通过getInstance获得constructor返回的那个对象字面量,然后就可以访问组件的公共api(parse,getData,etc.)了。

可以通过addView给私有成员viewList添加成员。update或parse执行到最后会内部调用私有方法renderView,给所有注册的视图发送消息(观察者模式)。

注意,注册到DataModel的视图必须实现render方法。

3、另外,这个实现代码存在两个约束。

一是在update方法里面,通知服务端更新数据之后,构造更新信息obj那里,其中id值是取data[index]['fid']。也就是说,数据的主键名已经写死为fid了。这里我能够想到的就只有两种处理方式,一种就是现在所使用的,一种是生成传递给parse方法的参数obj(自己写或者服务端生成)时,指定一个元数据对象为主键(可以给主键元数据对象增加一个identity或primary属性)。经过考虑,我还是觉得用写死的处理方式比较好。二是元数据里面,列标识的键名也写死为name了,而且在构造Metadata对象时,参数必须指定name属性值。

JS二维数据处理逻辑封装探究的更多相关文章

  1. JS二维数组排序组合

    需求是这样的:http://q.cnblogs.com/q/29093/ 这里简述一下: 现在有一个不确定长度的数组.比如:var temp=[["Fu","Hai&qu ...

  2. js 二维数组 for 循环重新赋值

    javascript 二维数组的重新 组装 var arr = [[1,2],[3,4],[5,6],[7,8]]; var temp = new Array(); for(var i= 0 ;i&l ...

  3. js二维数组定义和初始化的三种方法总结

    js二维数组定义和初始化的三种方法总结 方法一:直接定义并且初始化,这种遇到数量少的情况可以用var _TheArray = [["0-1","0-2"],[& ...

  4. js二维数组与字符串

    1. 二维数组:数组中的元素,又引用了另一个数组对象 何时使用:只要保存横行竖列的数据, 具有上下级包含关系的数据, 创建二维数组: 1. var arr=[]; col arr[0]=[" ...

  5. js 二维码生成 插件

    <div onclick="liaotian()">点击生成二维码</div><div id="qrcode"></d ...

  6. 简单又炫酷的two.js 二维动画教程

      前  言 S     N 今天呢给大家介绍一个小js框架,Two.JS.其实在自己学习的过程中并没有找到合适的教程,所以我这种学习延迟的同学是有一定难度的,然后准备给大家整理一份,简单易懂的小教程 ...

  7. js二维码插件总结

    jquery.qrcode.js生成二维码插件&转成图片格式 http://blog.csdn.net/u011127019/article/details/51226104

  8. vue.js 二维码生成组件

    安装 通过NPM安装 npm install vue-qart --save 插件应用 将vue-qart引入你的应用 import VueQArt from 'vue-qart' new Vue({ ...

  9. html5扫面二维码逻辑

    写在前面 项目中有这样的需求,在android端嵌入的html5应用中,需要扫描二维码,而一般的浏览器是不允许你调用摄像头的.最后时限方式是由app的webview进行扫描,将扫描结果返回,也就是js ...

随机推荐

  1. Windows Phone 8.1 应用生命周期

    原文:Windows Phone 8.1 应用生命周期 一.“后退键”不会终止应用 关于 Windows Phone 8.1 的应用生命周期,第一个要知道的关键就是:“后退键”不会终止应用! 在 8. ...

  2. Android 2.3.5源码 更新至android 4.4,能够下载,度娘网盘

    Android 4.4源代码下载(linux合并) ==============================切割线结束========================= 旧版本号的能够使用115, ...

  3. 经常使用git命令集

    //创建本地仓库 mkdir git_root;cd git_root;git init // //查看 git status . git log git log ./kernel/driver/ g ...

  4. new 和delete

    转自:http://www.cnblogs.com/charley_yang/archive/2010/12/08/1899982.html 一直对C++中的delete和delete[]的区别不甚了 ...

  5. Iframe父页面与子页面之间的调用

    原文:Iframe父页面与子页面之间的调用 Iframe父页面与子页面之间的调用 专业词语解释如下:     Iframe:iframe元素是文档中的文档.     window对象: 浏览器会在其打 ...

  6. 三星GT-S7572换屏幕教程

    家里人手机被摔坏了,尽管不是什么值钱的手机.可是自从上了大学之后,就一直认为赚钱真的非常不easy,不到逼不得已,就不要乱花钱.于是,就从淘宝上买了外屏.以下是我在淘宝上的链接:点击打开链接.好不ea ...

  7. C语言中嵌入式SQL语句

    原文:[转载]C语言中嵌入式SQL语句 http://blog.csdn.net/cnlht/archive/2007/12/12/1930960.aspx原文地址 实验内容: 掌握SQL Serve ...

  8. 对于GetBuffer() 与 ReleaseBuffer() 的一些分析

    先 转载一段别人的文章 CString类的这几个函数, 一直在用, 但总感觉理解的不够透彻, 不时还实用错的现象. 今天抽时间和Nico一起分析了一下, 算是拨开了云雾: GetBuffer和Rele ...

  9. MVC5+ 路由特性

    MVC5+ 路由特性 概述 ASP.NET MVC 5支持一种新的路由协议,称为路由特性. MVC5也支持以前定义路由的方式,你可以在一个项目中混合使用这两种方式来定义路由. 案例 1.使用Visua ...

  10. System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(string, string)已过时的解决办法

    FormsAuthentication.HashPasswordForStoringInConfigFile 方法是一个在.NET 4.5中已经废弃不用的API,参见: https://msdn.mi ...