预览ExtJS 4.0的新功能(二):客户端的对象关系映射(ORM)

来源:互联网 发布:网络黑界是什么意思 编辑:程序博客网 时间:2024/05/29 14:05

4.0之前,Ext 的功能已经十分丰富,数据层却难以令人满意。作为 UI 框架,数据层可能不是重点,但明显攸关到业务顺利的展开与否。然而,3.0 之后,虽然过增加了 Ext.data.Api/REST/Direct 等的新协议,但无论 3.0 还是 2.0,Ext.data.* 底层仍维持 1.0 的对象模型。越来越多的新协议和新机制加入到 Ext.data.*后,反而造成 API 逾加凌乱。这样就有了改造 data 的必要了。可喜的是,4.0 中已经重构 Ext.data.* 部分,重点解决旧版里面所存在的问题。

新4.0比较3.x/2.x说明Ext.data.Model
继承自
Ext.util.ObserveableExt.Direct崭新的模型类。用于业务对象(Business Objects)的建模。Ext.data.Field
继承自
ObjectExt.data.Record
继承自
Object已弃置Record,4.0改用Field代替表示字段及其信息或元数据的内容。Ext.data.Types
继承自
Object,为静态类Ext.data.Types
继承自
Object,为静态类Types是Store的数据类型。于Ext 3.3的时候被引入。Ext.data.Validation
继承自
Objectn/aExt.data.Validation是4.0中新功能,用于围绕数据模型的数据验证。

一、Ext.data.Model

为了改进客户端数据的语义能力和更划一 API,新 data 部分经过重构后,许多接口已经发生变化。最明显的是 Ext.data.Model类,这是一个崭新的模型类,目的在于加厚数据层、强化业务与对象的结合、处处围绕域对象——应运而生的、并十分显著的这么一个类,尽可能提供一个清晰高效的UI数据绑定模型,以达到企业级开发的新水平。

为大家更清晰地了解 Model 内容,我们张贴出一张简单的 Model 的类图(如右图,使用 Spket 导出)。

现阶段根据文档提供的信息,我们得知,所谓 Model 模型,实际上是与程序所管理的对象一一对应的。例如,为用户、产品、汽车……现实世界中等等可见或不可见的事物,只要您认为可以的便可加入到Model系统中,为之“建模”。你可以向Ext.ModelMgr(模型管理器)去登记一个模型对象。模型的概念一般是抽象的,而它的实体数据就是 Ext.data.Store 所携带的数据(进一步讲用于 Ext 组件的通用数据绑定)——但必须明白,Model 本身不会携带实体数据。一般感性地认为,模型是抽象的。毋庸多说,抽象归抽象,只是相对“具象(实体)”而言,但模型中总得包含一些内容吧?是的,那模型中有些什么呢?API就规定,模型由一系列的字段(包括名称、类型、默认值等的元数据),还有业务相关的方法/属性所组成(如下例中的changName())。

Edit 2011-9-22 示例代码可能与最新版本的 Ext Model 有区别,但不影响主干意思——感谢 Thanks to QiuQiu/太阳。

view plaincopy to clipboardprint?
  1. Ext.regModel('User', {  
  2.     fields: [  
  3.         {name: 'name',  type: 'string'},  
  4.         {name: 'age',   type: 'int'},  
  5.         {name: 'phone', type: 'string'},  
  6.         {name: 'alive', type: 'boolean', defaultValue: true}  
  7.     ],  
  8.     changeName: function() {  
  9.         var oldName = this.get('name'),  
  10.             newName = oldName + " 绰号 半支烟";  
  11.         this.set('name', newName);  
  12.     }  
  13. });  

view plain
  1. Ext.regModel('User', {  
  2.     fields: [  
  3.         {name: 'name',  type: 'string'},  
  4.         {name: 'age',   type: 'int'},  
  5.         {name: 'phone', type: 'string'},  
  6.         {name: 'alive', type: 'boolean', defaultValue: true}  
  7.     ],  
  8.     changeName: function() {  
  9.         var oldName = this.get('name'),  
  10.             newName = oldName + " 绰号 半支烟";  
  11.         this.set('name', newName);  
  12.     }  
  13. });  

 

执行Ext.regModel()方法来登记模型的过程中,保存元数据的数组会经过 Ext.ModelMgr(模型管理器)转化为 Ext.util.MixedCollection的结构,MixedCollection 是一种复杂的数据结构,提供一些比Array数组更健壮的事件和自动处理方法,解决 UI 组件复杂数据结构的问题。在向ModelMgr登记过程中,还会将所有的函数和属性就指向Model的原型prototype。

旧版本中使用 Ext.data.Record.create({....}) 创建Record对象,现如今Record已消失,或被弃置(dep),创建Model的方法通用却还是 create()。我们测试一下,创建一个业务模式“用户 user”,代码如下:

view plaincopy to clipboardprint?
  1. var user = Ext.ModelMgr.create({  
  2.     name : 'Frank',  
  3.     age  : 27,  
  4.     phone: '888-88-888'  
  5. }, 'User');  
  6. user.changeName();  
  7. user.get('name'); //返回" Frank 绰号 半支烟"  

view plain
  1. var user = Ext.ModelMgr.create({  
  2.     name : 'Frank',  
  3.     age  : 27,  
  4.     phone: '888-88-888'  
  5. }, 'User');  
  6. user.changeName();  
  7. user.get('name'); //返回" Frank 绰号 半支烟"  

 如上面两例,因为我们在 regModel() 方法除了定义各个字段的名称、类型之外,还自定义了一个完全新的方法 changeName(),所以在 cretae() 方法中的第二参数中,输入了User 模型的参数。这样,在 user 模式的实例中,便可使用 user.changeName() 方法。也就是说,user 有了业务方法 user.changeName()。

实际上,如果我们对 Record 还有印象,其实可以从旧版中的 Record 类身上找到 Model 的影子的。原因可能 Record 的语义比较笼统,同时旧版中赋予的任务比较繁重,所以干脆废掉 Record,创建 Model(当然Model所涵盖的意义比Record大得多,也抽象得多),并改用 Field 定义字段。

二、字段类 Ext.data.Field

2.x/3.x的Ext,Record类不但封装了Record的作为显示数据字段的相关定义信息,还封装了 Ext.data.Store 所使用的 Recrod 对象的操作方法,包括beginEdit()、commit()、reject()、dirty/modifed等。到4.0中,首先迁移了这些方法到Store和Model(Ext.util.Stateful),纯粹是描述字段消息用的类,显得更简单;其次,Record的命名更改为Field。新的Field类依然封装了字段信息,不过供 Ext.regModel方法中,声明字段定义对象时所用(所以,开发者一般不需要实例化这该类。Ext.regModel创建过程中会该类的实例)。

Field类新增加sortType:Function的配置项,作用是:按照既定的排序而设的函数。在这个函数中,将Field的值转换为可比较的数值。我们可以一、使用预定义的函数储存在Ext.data.SortTypes;二、自定义函数。自定义的例子如下:

view plaincopy to clipboardprint?
  1. sortType: function(value) {  
  2.    switch (value.toLowerCase()) // native toLowerCase():  
  3.    {  
  4.       case 'first'return 1;  
  5.       case 'second'return 2;  
  6.       defaultreturn 3;  
  7.    }  
  8. }  

view plain
  1. sortType: function(value) {  
  2.    switch (value.toLowerCase()) // native toLowerCase():  
  3.    {  
  4.       case 'first'return 1;  
  5.       case 'second'return 2;  
  6.       defaultreturn 3;  
  7.    }  
  8. }  
函数执行的结果如下:

view plaincopy to clipboardprint?
  1. // current sort     after sort we want  
  2. // +-+------+          +-+------+  
  3. // |1|First |          |1|First |  
  4. // |2|Last  |          |3|Second|  
  5. // |3|Second|          |2|Last  |  
  6. // +-+------+          +-+------+  

view plain
  1. // current sort     after sort we want  
  2. // +-+------+          +-+------+  
  3. // |1|First |          |1|First |  
  4. // |2|Last  |          |3|Second|  
  5. // |3|Second|          |2|Last  |  
  6. // +-+------+          +-+------+  

还记得Grid ColumnModel中的format函数吗?format的作用是把Store中的数据按照一定的规则转换为我们想要的格式,呈现在Cell单元格。借助funciton是一个不错的主意,值得我们好好学习,但是format定义在UI层中,只能属于Grid所使用。如果要在其他UI组件如Form、Tree就不能那样使用了。如果我想要在全体UI组件,不问具体何种组件,都可以自动转换格式,达到format函数的功能,该怎么做?——新Field告诉了我们答案。既然Field用于定义字段信息,位于数据层之中,为什么不加入format的功能呢?的确,这就是Ext.data.Field.convet做的事情。它把Reader提供的用于转换值,将值变为Record下面的对象。好比说sex字段的true/false,从数据库返回到JsonReader也是true/false,那么就可以用conver配置项定义的转换函数转换,从数据层的层面上根本解决格式化数据的问题。例如下面的fullName和location都是转换函数:

view plaincopy to clipboardprint?
  1. function fullName(v, record){  
  2.     return record.name.last + ', ' + record.name.first;  
  3. }  
  4. function location(v, record){  
  5.     return !record.city ? '' : (record.city + ', ' + record.state);  
  6. }  
  7. var Dude = Ext.ModelMgr.create([  
  8.     {name: 'fullname',  convert: fullName},  
  9.     {name: 'firstname', mapping: 'name.first'},  
  10.     {name: 'lastname',  mapping: 'name.last'},  
  11.     {name: 'city', defaultValue: 'homeless'},  
  12.     'state',  
  13.     {name: 'location',  convert: location}  
  14. ]);  
  15. // create the data store  
  16. var store = new Ext.data.Store({  
  17.     reader: new Ext.data.JsonReader(  
  18.         {  
  19.             idProperty: 'key',  
  20.             root: 'daRoot',  
  21.             totalProperty: 'total'  
  22.         },  
  23.         Dude  // recordType  
  24.     )  
  25. });  
  26. var myData = [  
  27.     { key: 1,  
  28.       name: { first: 'Fat',    last:  'Albert' }  
  29.       // notice no city, state provided in data object  
  30.     },  
  31.     { key: 2,  
  32.       name: { first: 'Barney', last:  'Rubble' },  
  33.       city: 'Bedrock', state: 'Stoneridge'  
  34.     },  
  35.     { key: 3,  
  36.       name: { first: 'Cliff',  last:  'Claven' },  
  37.       city: 'Boston',  state: 'MA'  
  38.     }  
  39. ];  

view plain
  1. function fullName(v, record){  
  2.     return record.name.last + ', ' + record.name.first;  
  3. }  
  4. function location(v, record){  
  5.     return !record.city ? '' : (record.city + ', ' + record.state);  
  6. }  
  7. var Dude = Ext.ModelMgr.create([  
  8.     {name: 'fullname',  convert: fullName},  
  9.     {name: 'firstname', mapping: 'name.first'},  
  10.     {name: 'lastname',  mapping: 'name.last'},  
  11.     {name: 'city', defaultValue: 'homeless'},  
  12.     'state',  
  13.     {name: 'location',  convert: location}  
  14. ]);  
  15. // create the data store  
  16. var store = new Ext.data.Store({  
  17.     reader: new Ext.data.JsonReader(  
  18.         {  
  19.             idProperty: 'key',  
  20.             root: 'daRoot',  
  21.             totalProperty: 'total'  
  22.         },  
  23.         Dude  // recordType  
  24.     )  
  25. });  
  26. var myData = [  
  27.     { key: 1,  
  28.       name: { first: 'Fat',    last:  'Albert' }  
  29.       // notice no city, state provided in data object  
  30.     },  
  31.     { key: 2,  
  32.       name: { first: 'Barney', last:  'Rubble' },  
  33.       city: 'Bedrock', state: 'Stoneridge'  
  34.     },  
  35.     { key: 3,  
  36.       name: { first: 'Cliff',  last:  'Claven' },  
  37.       city: 'Boston',  state: 'MA'  
  38.     }  
  39. ];  

上例中还透露一个数据转换的技巧,在location()转换函数中。对于不完整的数据,可以在转换函数中有机会得到修正。

三、自定义数据类型 Types

Field的定义中可以允许我们声明字段的类型。例如{name:'age', type:'int'}就是一个age字段,类型是整数int。

JavaScript提供的数据类型(Data Types)很少,充其量是基础的数据类型:Number、String、Boolean……。Ext在此基础上封装了一系列的如int、float、auto类型,提供更细致的数据类型描述,便于正确显示数据。但现实使用往往都是不够的,没有办法自己加入新的数据类型。于是v3.3引入了Ext.data.Types包,开放给用户,允许自定义数据类型。

由于Ext.data.Field包含了字段的类型信息,所以如要测试某个字段的类型,拿它与Ext.data.Types['类型名称']比较即可。

如果需要自定义数据类,这是Ext 4.0新引入的内容,应该在Ext.data.Field的命名空间下定义新的类型,如Ext.data.Types.USER、Ext.data.Types.TABLE_HEAD之类的。还有一个必须遵循的约定的就是,必须用大写(UPPERCASE)。

新建的数据类型,一定要以下的三项属性齐备:

  • covert:Fucntion:形成自定义数据类型的转换函数。从数据源得到是原始数据,可能性需要经过特定转换,变为符合该数据类型其结构的对象。该函数有两个参数供调用:一、v:Mixed,Reader 读取过的值(一切合法的JavaScript的数据类型),如果没有的话便读取 Ext.data.Field.defaultValue 的值(默认值)。二、rec:Mixed,Reader读取过的整行数据。如果这是一个 ArrayReader,那么这将是一个内存中的 Array;如果这是一个 JsonReader,那么这将是一个 JSON 对象;如果这是一个 XmlReader,那么这是一个 XML Element 节点或元素的对象。
  • sortType:Function:排序函数。根据Ext.dtaa.SortTypes来定义的。
  • type:String:自定义类型的名称。

实际上,我们可以模仿 Ext.data.Types.STRING、Ext.data.Types.FLOAT……API 中写好的代码,新加入属于自己的数据类型。

定义新类型后,我们就可以在字段声明中使用了。假设有这么一个例子,创建一个VELatLong字段,微软在线地图专用的数据类型,才可以访问微软 Bing 地图的 API。首先我们从 JsonReader 中得到数据块,其中包含了 laittude/longitude 信息和内容,即 lat 和 long 属性,那么,我们可以这样定义 VELatLong 数据类型:

view plaincopy to clipboardprint?
  1. Ext.data.Types.VELATLONG = {  
  2.     convert: function(v, data) {  
  3.         return new VELatLong(data.lat, data.long);  
  4.     },  
  5.     sortType: function(v) {  
  6.         return v.Latitude;  // When sorting, order by latitude  
  7.     },  
  8.     type: 'VELatLong'  
  9. };  

view plain
  1. Ext.data.Types.VELATLONG = {  
  2.     convert: function(v, data) {  
  3.         return new VELatLong(data.lat, data.long);  
  4.     },  
  5.     sortType: function(v) {  
  6.         return v.Latitude;  // When sorting, order by latitude  
  7.     },  
  8.     type: 'VELatLong'  
  9. };  

view plaincopy to clipboardprint?
  1. var types = Ext.data.Types;   
  2. UnitRecord = Ext.ModelMgr.create([  
  3.     { name: 'unitName', mapping: 'UnitName' },  
  4.     { name: 'curSpeed', mapping: 'CurSpeed', type: types.INT },  
  5.     { name: 'latitude', mapping: 'lat', type: types.FLOAT },  
  6.     { name: 'latitude', mapping: 'lat', type: types.FLOAT },  
  7.     { name: 'position', type: types.VELATLONG }  
  8. ]);  

view plain
  1. var types = Ext.data.Types;   
  2. UnitRecord = Ext.ModelMgr.create([  
  3.     { name: 'unitName', mapping: 'UnitName' },  
  4.     { name: 'curSpeed', mapping: 'CurSpeed', type: types.INT },  
  5.     { name: 'latitude', mapping: 'lat', type: types.FLOAT },  
  6.     { name: 'latitude', mapping: 'lat', type: types.FLOAT },  
  7.     { name: 'position', type: types.VELATLONG }  
  8. ]);  

C++ 比 C 明显进步的一个地方可以允许自定义数据类型,而 JavaScript 乃一门弱类型的脚步语言,由于使用场合比较不同,似乎没有可比性,不过当今实实在在的却是,Ext 4.0 企图引入“数据类型 DataTypes”的一环。姑且不论怎么样,还是值得我们注意的,尤其一方面服务端的JavaScript日渐声隆,复杂的 “数据类型DataTypes“ 乃似乎不可或缺。

四、数据验证 Validations

通过 Ext.data.validations 验证器,Model 内部可实现数据验证功能。再如 User 模型,

view plaincopy to clipboardprint?
  1. Ext.regModel('User', {  
  2.     fields: [  
  3.         {name: 'name',     type: 'string'},  
  4.         {name: 'age',      type: 'int'},  
  5.         {name: 'phone',    type: 'string'},  
  6.         {name: 'gender',   type: 'string'},  
  7.         {name: 'username', type: 'string'},  
  8.         {name: 'alive',    type: 'boolean', defaultValue: true}  
  9.     ],  
  10.     validations: [  
  11.         {type: 'presence',  field: 'age'},  
  12.         {type: 'length',    field: 'name',     min: 2},  
  13.         {type: 'inclusion', field: 'gender',   list: ['Male''Female']},  
  14.         {type: 'exclusion', field: 'username', list: ['Admin''Operator']},  
  15.         {type: 'format',    field: 'username', matcher: /([a-z]+)[0-9]{2,3}/}  
  16.     ]  
  17. });  

view plain
  1. Ext.regModel('User', {  
  2.     fields: [  
  3.         {name: 'name',     type: 'string'},  
  4.         {name: 'age',      type: 'int'},  
  5.         {name: 'phone',    type: 'string'},  
  6.         {name: 'gender',   type: 'string'},  
  7.         {name: 'username', type: 'string'},  
  8.         {name: 'alive',    type: 'boolean', defaultValue: true}  
  9.     ],  
  10.     validations: [  
  11.         {type: 'presence',  field: 'age'},  
  12.         {type: 'length',    field: 'name',     min: 2},  
  13.         {type: 'inclusion', field: 'gender',   list: ['Male''Female']},  
  14.         {type: 'exclusion', field: 'username', list: ['Admin''Operator']},  
  15.         {type: 'format',    field: 'username', matcher: /([a-z]+)[0-9]{2,3}/}  
  16.     ]  
  17. });  

只需要调用 validate() 函数就可进入验证的程序,该程序返回一个 Ext.data.Errors 对象。

view plaincopy to clipboardprint?
  1. var instance = Ext.ModelMgr.create({  
  2.     name: 'Ed',  
  3.     gender: 'Male',  
  4.     username: 'edspencer'  
  5. }, 'User');  
  6. var errors = instance.validate();  

view plain
  1. var instance = Ext.ModelMgr.create({  
  2.     name: 'Ed',  
  3.     gender: 'Male',  
  4.     username: 'edspencer'  
  5. }, 'User');  
  6. var errors = instance.validate();  

Validations 下面的一系列方法和属性用于验证数据所必须的数据类型,如右图所示。多数情况下是来自于 Model 的自动调用或者 validate() 调用 Validations.* 的方法,不必用户调用。

在数据层就加入了数据验证,并配合Errors对象,无疑大大加强了数据的健壮性,使得Model的概念得到扩展,更为完整。但这里的数据验证可不是要代替表单验证的用户输入验证,要区别开来。

五、小结

本文根据手头上所能接触到Model/Field/Types/Validitons,发掘它们一些特性和新内容,重点围绕4.0的数据模型作初步的了解。当然,现阶段的Ext 4.0仍在Alpha阶段中,API应该还会有变化,所以文多回避细节问题,希望呈现一个大致轮廓,所以连一个UML图都没有,不过就是适合于大家初步认识新的数据模型。

至于何谓客户端的ORM?本文仍着墨较少,当然并不是说本文离题, ORM涉及的模型都可以在这里找到答案……请要注意,话还没有讲完,Ext.data.*还有ModelMgr、hasMany、belongto分配关系的重要内容,不过限个人精力有限,难以通一文述之,还是留在下回分解吧~——Model的一对一、一对多、多对对,并如何用JavsScript来实现,那才是真正的闪光的地方!

原创粉丝点击