Extjs 4引入新的数据包,其中新增了不少新类并对旧有的类作出了修整。使数据包更强大和更容易使用。
本章我们将学习一下内容:
Extjs4的数据包引入了如Model类的新特性。同时对Store和Proxy类进行了修整。大部分的修整都是向后兼容的。最大的变化是在Record、Store和Proxy类中。Extjs4的数据包是其中一个与Sencha Touch共享的包。
Model是数据包中其中一个最重要的类。它的前身是Record类。现在我们可以通过Model类来代表现实对象了,其中可以运用关联(associations)和合法性验证(validations)。以下的表格罗列出Extjs3的Record与Extjs4的Model的异同:
现在Proxy能够直接附加到Model和Store中。Proxy接收负责读写和对从服务端接收或发送到服务端的数据做编码的Reader和Writer的实例化对象。Extjs4引入了客户端、服务器端的Proxy。而Store也有了新特性,如排序(sorting)、过滤(filtering)和分组(grouping)。
一个Model代表一个实体、一个对象;并包含一系列的方法和字段来操作实体或对象的数据。Model类类似于旧有版本的Extjs中用于创建字段和方法来操作数据的Record类。但Model类更强大,我们可以创建多个Model子类并通过关联(Associations)将它们连接在一起,也可以通过合法性验证(Validations)来对数据进行验证,同时我们还可以通过Proxy来让Model子类直接从服务端读写数据。下面的图表展现出Model的主要功能:
创建一个具体的Model类,我们必须通过Ext.define来创建一个继承Ext.data.Model的新类,并定义所需的字段,如下:
Ext.define("Patient", {
extend: "Ext.data.Model",
fields: [
{name: "name"},
{name: "age", type: "int"},
{name: "phone", type: "string"},
{name: "gender", type: Ext.data.Types.STRING},
{name: "birthday", type: "date", dateFormat: "d/m/Y"},
{name: "alive", type: "boolean", defaultValue: true},
{name: "weight", type: "float"},
{name: "weightKg", type: "float", convert: function(val, record){
var weightPounds = record.get("weight");
return Math.round(weightPounds * 0.45459237);
}}
]
});
上面我们定义了一个名为Patient的Model子类。并通过fields属性来定义Patient类的字段。在讲解字段(Ext.data.Field)之前我们先了解字段的一个重要的组成部分字段类型(Ext.data.Types)。
Ext.data.Types内置的类型有下列几种:
其中auto为未显示指定字段类型时的默认值。可以看到代码中的gender字段使用Ext.data.Types.STRING,而其他字段均使用字符串形式表示类型。其实“Ext.data.Types.大写字符串”是类型类型的全名,在使用前需要已完成该类的加载。而使用字符串形式的话,若在定义该类的时候该类型的类还没加载,那么就会发起类文件同步加载的请求,待加载完成后再完成Model子类的定义工作。
Ext.data.Types的作用是将Reader从服务端读取的数据装换成Ext.data.Types指定的类型。
自定义类型:
可通过"Ext.data.Types.大写字符串={convert:....,sortType:....,tyep:...}"的形式自定义类型,具体例子如下:
Ext.data.Types.UCSTRING = {
type: "ucString",
convert: function(val, record){
return val.toUpperCase();
},
sortType: function(){
??????????
}
};
type:是类型的别名,就是在设置Ext.data.Field.type时的字符串。
convert:是Reader从服务端读取数据后转换成指定数据类型的数据类型转换函数。
sortType:
下面我们把目光转向Ext.data.Field吧。
Ext.data.Model中的所有字段均为Ext.data.Field的实例,Ext.data.Field有多个初始化配置项,下面让我们逐一讲解。
name:用于定义字段名称(必填)
type:用于定义字段的数据类型(选填)
defaultValue:设置字段的默认值(选填,若不填则使用字段数据类型对应的默认值)
dateFormat:当字段数据类型为Ext.data.Types.DATE时,可通过该项来设置日期时间的表现格式(格式字符串与php中的相同)(选填)
mapping:用于设置Model中字段与Ext.data.reader.Reader的子类(Ext.data.reader.Array、Ext.data.reader.Json和Ext.data.reader.Xml)所读取的数据结构中字段的对应关系。(选填,若不设置则以字段的name属性为依据在所读取的数据结构中寻找同名的字段作为对应字段)
persist:用于确定若修改该字段后,该字段是否会列入Ext.data.Model.modified属性中并通过Ext.data.writer.Writer写到服务器端。(选填,默认为true)
sortDir:初始化排序方向(选填)
sortType:
useNull:当字段数据类型为Ext.data.Types.INT或Ext.data.Types.FLOAT时,若Reader读取的数据不能转换成数字类型,那么useNull为true时则返回null,否则返回0(选填)
convert:用于设置Model字段与Reader读取的数据结构的对应关系,通过定义函数Function(val, record){}来设置比mapping更复杂的对应关系。注意:因为convert是用于处理mapping无法处理的复杂对应关系,所以跟mapping一样是用于在Reader读取数据时或实例化该Model子类时初始化各字段值时生效,后期再通过“Model子类实例.set('字段名', '值')”来修改于convert函数相关字段后并不会影响convert属性所属字段的值,例子如下:
Ext.define("A", {
extend: "Model",
fields: [{
name: "a", defaultValue: "1", type: "int"
}, {
name: "b", type: "int", convert: function(val, record){
return 1 + record.get("a");
}
}]
});
var instance = new A();
instance.get("a"); // 结果为1
instance.get("b"); // 结果为2
instance.set("a", 10);
instance.get("a"); // 结果为10
instance.get("b"); // 结果 为2
另外值得注意的一点是,因convert函数中获取当前操作的整条记录,并可通过"record.get('字段名称')"的方式获取记录中的某个字段,但要注意的是,获取的字段必须是已经初始化完成的,否则无法获取。例子如下:
Ext.define("A", {
extend: "Model",
fields: [{
name: "b", type: "int", convert: function(val, record){
return 1 + record.get("a");
}
}, {
name: "a", defaultValue: "1", type: "int"
}]
});
var instance = new A(); 此时就会抛出异常。
(选填,若不设置则使用字段数据类型的默认convert)
现在我们已经学会在Model中定义字段了,下面我们继续探讨在Model中定义方法,并在方法中操作字段吧!首先直接上代码:
Ext.define("A", {
extend: "Ext.data.Model",
fields: [{
name: "key", defaultValue: 2, type: "int"
}],
showKey: function(){
return this.get("key");
}
});
var a = new A();
a.showKey(); // 结果为key
合法性验证是Extjs4的Model类的新特性,通过该特性我们可以设置根据某些规则来定义合法性验证。而Extjs3的Store类并没有改功能。
合法性验证(涉及的类是Ext.data.validations)的声明语句与字段的声明语句类似,我们必须声明验证的类型(共6种),并且定义验证的字段,还有其他配置项让我们能细化验证规则。注意:我们可以为某个字段设置多条验证规则。例子如下:
Ext.define("Patient", {
extend: "Ext.data.Model",
fields: [......],
validations: [{
field: "age", type: "presence"
}, {
field: "name", type: "presence"
}, {
field: "name", type: "length", min: 2, max: 60
}, {
field: "name", type: "format", matcher: /([a-z ]+)/
}, {
field: "gender", type: "inclusion", list: ['M', 'F']
}, {
field: "weight", type: "exclusion", list: [0]
}, {
field: "email", type: "email"
}]
});
var p = Ext.create("Patient", {
name: "L",
phone: "9876-5432",
gender: "Unknown",
birthday: "95/26/1986"
});
var errors = p.validate();
errors.isValid();
下面我们来具体了解这六种验证规则
- presence:用于设置字段为必填项(0为有效值,空字符串未无效值),若字段没有值或为无效值则不通过验证;
- length:用于设置字段值的最短和最长长度,附加属性为min和max;
- format:用于设置字段值必须符合的格式,附加属性为matcher:正则表达式;
- inclusion:用于设置字段值必须为列表中的一项,附加属性list:候选值数组;
- exclusion:用于设置字段值必须不为列表中的一项,附加属性list:排除值数组;
- email:验证规则format的实例,用于验证字段值必须为有效的email地址。
了解了上述的验证规则后,我们会想如果验证失败怎么也要反馈失败信息吧,那么我们就要学习Ext.data.validations和Ext.data.Errors了。
Ext.data.validations是一个单例对象,无需创建即可直接使用。(可从其并不是以大写字母开头来命名看出它不是一个类,而是一个对象)
Model的validations属性使用的6种验证规则就是对应Ext.data.validations对象中的6种验证函数。而每种验证函数均有一个可修改的验证失败提示语与其对应,如presence就对应Ext.data.validations.presenceMessage,这样我们就可以自定义错误提示语了。但这种方式定义的错误提示语是全局性的,也就是说如果我们设定Ext.data.validations.presenceMessage = "请填写该项目!",那么所有Model子类的的presence验证规则的失败提示语均一样。那如何设置专属于某一个验证规则的失败提示语(局部失败提示语)呢?就是在定义验证规则时,设置message属性。具体如下:
Ext.define("Patient", {
extend: "Ext.data.Model",
fields: [......],
validations: [{
field: "age", type: "presence", message: "请填写年龄!"
},......}]
});
注意:局部失败提示语的优先级高于全局失败提示语。
定义失败提示语后,我们就要执行合法性验证并获取验证结果了。
我们可以通过"Model子类实例化对象.validate()"来执行合法性验证,该方法将返回Ext.data.Errors的实例,它就是用于封装合法性验证结果的类。以下是常用的属性和方法:
length:验证失败信息个数
isValid():合法性验证是否通过
getByField():通过Model的字段名来获取该字段的验证失败信息,对于一个字段可以有多个验证失败信息。格式:[{field: "字段名", message: "失败提示语"},.......]
Extjs4中我们可以通过在Model中设置proxy配置项,来实现从服务端加载和保存数据。代码如下:
Ext.define("Blog", {
extend: "Ext.data.Model",
fields: [
{name: "id", type: "int"},
{name: "name", type: "string"},
{name: "url", type: "string"}
],
proxy: {
type: "rest",
url: "data/blogs",
format: "json",
reader: {
type: "json",
root: "blogs"
}
}
});
以上代码,我们定义了含id、name和url三个字段的Model子类Blog,并配置了proxy属性。该proxy采用RESTFul URLs,并使用JsonReader。设置完成后,我们就可以通过Model子类来实现从服务端加载和保存数据了。具体操作如下:
1. 从服务端根据记录id加载单条记录
Blog.load(1, {
success: function(blog){
console.log("blog: ", blog.get("url"));
}
});
服务端响应的内容如下:
{
"blogs": [{
"id": 1,
"name": "fsjohnhuang",
"url": "fsjohnhuang@hotmail.com"
}]
}
2. 修改记录内容
blog.set("name", "john huang");
blog.save({
success: function(){
console.log("success!");
}
});
3. 删除记录
blog.destroy({
success: function(){
console.log("success!");
}
});
4. 新增记录
var blog = Ext.create("Blog", {
name: "John Huang",
url: "fsjohnuang.cnblogs.com"
});
blog.save();
服务端必须返回新增记录的ID信息,服务端响应的内容如下:
{
"id": 2,
"name": "John Huang",
"url": "fsjohnuang.cnblogs.com"
}
下面我们将学习如何在Store中使用Model吧
var store = Ext.create("Ext.data.Store", {
model: "Blog",
proxy: {
type: "rest",
url: "data/blogs",
format: "json",
reader: {
type: "json",
root: "blogs"
}
}
});
store.load(function(records){
..............
});
可以看到在通过Store的model配置项即可将指定的Model子类附加到Store中;
而Store中同样能设置proxy属性,这样做的好处是即使加载的url不同也能重用Model子类;
"model子类.load()"是加载单条记录,而"Store子类实例.load()"是加载多条记录。
当我们开发实际应用时,需要建立多个model子类,而他们之间或多或少会存在某种关联。在旧版的Extjs中,我们无法通过Record类来设置关联关系,而Extjs4 的Model类就可以通过associations(Ext.data.Association)来建立一对多、一对一的关联关系(多对多的关联实际是通过两个一对多的关联来组成的)
这里有三种类型的Associations:
- Ext.data.association.HasMany(Model.hasMany属性与之对应):用于表示一对多的关系
- Ext.data.association.BelongsTo(Model.belongsTo属性与之对应):用于表示多对一的关系
- Ext.data.association.HasOne(Model中没有与之直接对应的属性,要用):用于表示一对一的关系
下面通过实例来理解吧!
现在有Author、Book和Chapter三个表。一个author可以写多本book,一本book可以有多个章节。(译者语:下面我们只关注Author和Book的关联关系,因为Book和Chapter的关联关系与之相同就不重复叙述了)
Ext.define("Author", {
extend: "Ext.data.Model",
requires: ["book"],
fields: [{
name: "id", type: "int"
}, {
name: "name", type: "string"
}],
hasMany: {
// filterProperty: "name",
model: "Book",
foreignKey: "authorId",
primaryKey: "id",
name: "books",
autoLoad: true
}
});
Ext.define("Book", {
extend: "Ext.data.Model",
fields: [{
name: "id", type: "int"
}, {
name: "title", type: "string"
}, {
name: "authorId", type: "int"
}],
proxy: {
type: "ajax",
format: "json",
url: "getDummyData.ashx",
reader: {
type: "json",
root: "datas"
}
}
});
var author = Ext.create("Author", {
id: 1,
name: "John Huang"
});
var books = author.books(); // 同步操作,会阻塞线程
下面我们逐一学习Ext.data.association.HasMany的配置项:
- associatedModel(就是上述的model)(必填):指定关联Model子类(即实例中的Book)
- ownerModel(必填,通过Ext.data.Model.hasMany属性设置时,自动设置为当前Model子类):指定主Model子类(即实例中的Author)
- name(选填):设置获取关联Model子类实例化对象集(数据类型为Ext.data.Store)的方法名称,默认为"关联Model子类小写s",如Book的该方法名为books;
- foreignKey(选填):设置关联Model子类中与主Model子类主键对应的外键字段名称,默认为"主Model子类名称小写_id",如Book中应存在author_id字段;
- primaryKey(选填):设置主Model子类中与关联Model子类外键对应的主键字段名称,默认为"id"。API文档中描述其默认值为Ext.data.Model.idProperty的值(用于设置Model的主键字段名称,默认为"id"),但实践证明即使修改了Ext.data.Model.idProperty,primaryKey依旧为"id"。
- autoLoad(选填):
- 在执行上述代码中"var books = author.books();"时是否向服务端发起获取Book数据的请求,true为发起,false为不发起。默认值为false。
- 在执行"Author.load(1,function(record){.......});"时是否向服务端发起获取Book数据的请求,true为发起,false为不发起。默认值为false。
- associationKey(选填):设置从哪个字段读取关联Model子类的数据,默认为"关联Model子类的名称小写s"。(多数用于自关联或嵌套结构的数据中,详情请查看下面的“通过Store我们可以一次性加载所有数据”内容)
- filterProperty(选填):设置筛选关联Model子类实例数据的字段名称,默认为Ext.data.association.HasMany.foreignKey的值。注意:该属性的值必须为主Model子类和关联Model子类中均存在的且名称相同的字段的名称,否则将抛异常或得不到预想的结果。下面是由主Model子类获取关联Model子类实例数据的分析图,希望能让大家更好的理解这一属性的作用。
上述分析图,我们将filterProperty设置为des。注意:即使服务端返回的数据当中存在不符合条件的数据,但因客户端会自动执行筛选操作,所以最后结果只含有符合条件的数据。其实primaryKey和foreignKey是filterProperty的一种变形,分析图如下:
通过上述的学习我们已经掌握设置一对多关系(hasMany)的内容了,既然我们可以通过如author.books()的方式(注意该方法为同步方法)来获取关联Model子类实例的集合,那么是否可以执行添加、修改、删除的操作呢?答案是肯定的。
1. 对于修改、删除操作其实就是Model子类实例对象的修改、删除操作,方法与2.2.3.中描述的一致;
2. 而添加操作是对Store实例的操作,下面我们来学习一下吧!
var books = author.books();
books.add({
title: "Ext js 4 First Look"
});
books.add({
title: "javascript"
});
books.sync();
在执行books.add()语句后,会自动设置authorId为1;然后执行books.sync()语句就将数据保存到服务端。
通过Store我们可以一次性加载所有数据(主Model子类和关联Model子类的数据),实例如下:
Ext.sycnRequire(["Author","Book"]);
var s = Ext.create("Ext.data.Store", {
model: "b",
autoLoad: true,
proxy: {
type: "ajax",
format: "json",
url: "getDummyData.ashx",
reader: {
type: "json",
root: "data"
}
}
});
Ext.define("Author", {
extend: "Ext.data.Model",
requires: ["Book"],
fields: [{
name: "id", type: "int"
}, {
name: "name", type: "string"
}],
hasMany: {
//autoLoad: true,
model: "Book",
foreignKey: "authorId",
primaryKey: "id",
name: "books",
associationKey: "childData" // 默认为books
}
});
Ext.define("Book", {
extend: "Ext.data.Model",
fields: [{
name: "id", type: "int"
}, {
name: "title", type: "string"
}, {
name: "authorId", type: "int"
}],
proxy: {
type: "ajax",
format: "json",
url: "getDummyData.ashx",
reader: {
type: "json",
root: "datas"
}
}
});
服务端返回的数据格式为
{"data":[
"id":1,
"name": "John Huang",
"childData": [{
"id": 1,
"title": "Javascript",
"authorId": 1
}, {
"id": 2,
"title": "Javascript cookbook",
"authorId": 1
}]
]}
此时实例化Store对象时就会一次性获取获取author和book数据。而book的数据就用author的associationKey设置的值(childData)来标识映射关系。此时如果b中的hasMany属性中设置了autoLoad为true,那么除了Store发起请求外,还会触发Book向服务端发起数据请求。
注意:我想加载也许都会注意到上述几段代码中都发现出现Ext.syncRequire和requires的语句,这是因为设置Ext.data.Store.model和Ext.data.Association.associatedModel均为类文件同步加载,关于类文件同步加载的内容请参考第一章。
Ext.define("Author", {
extend: "Ext.data.Model",
requires: ["book"],
fields: [{
name: "id", type: "int"
}, {
name: "name", type: "string"
}],
proxy: {
type: "ajax",
format: "json",
url: "getDummyData.ashx",
reader: {
type: "json",
root: "datas"
}
}
});
Ext.define("Book", {
extend: "Ext.data.Model",
requires: ["Author"],
fields: [{
name: "id", type: "int"
}, {
name: "title", type: "string"
}, {
name: "authorId", type: "int"
}],
belongsTo: {
model: "Author",
primaryKey: "id",
foreignKey: "authorId",
getterName: "getA",
setterName: "setA"
}
});
var book = Ext.create("Book", {
id: 1,
title: "javascript",
authorId: 1
});
var author = null;
// 异步操作
book.getA(function(author, operation){
author = author;
});
或
book.getA({
reload: true, // 强制向服务端发送数据请求,而不是读cache内容
scope: this,
success: function(author, operation){
author = author;
},
failure: function(author, operation){
..............
},
callback: function(author, operation){
..............
}
});
下面我们逐一学习Ext.data.association.BelongsTo的配置项:
- associatedModel(就是上述的model)(必填):指定关联Model子类(即实例中的Author)
- ownerModel(必填,通过Ext.data.Model.hasMany属性设置时,自动设置为当前Model子类):指定主Model子类(即实例中的Book)
- foreignKey(选填):设置主Model子类中与关联Model子类主键对应的外键字段名称,默认为"关联Model子类名称小写_id",如Book中应存在author_id字段;
- primaryKey(选填):设置关联Model子类中与主Model子类外键对应的主键字段名称,默认为"id"。API文档中描述其默认值为Ext.data.Model.idProperty的值(用于设置Model的主键字段名称,默认为"id"),但实践证明即使修改了Ext.data.Model.idProperty,primaryKey依旧为"id"。
- getterName(选填):设置获取关联Model子类对象的函数名称,默认为"get关联Model子类名称",如getAuthor。该方法为异步函数;
- setterName(选填):设置设置关联Model子类对象的函数名称,默认为"get关联Model子类名称",如setAuthor。该方法调用时要注意:
- 调用该方法仅仅修改Model子类间的对应关系,而无法修改关联Model子类的字段值,等同于book.set("authorId", 2);
- 若调用时只传入一个参数,如book.setAuthor(2)。此时修改结果仅保存在客户端而不会向服务端发起保存请求;
- 若调用时传入两个参数,如book.setAuthor(2, function(){....})。此时就会将修改结果发送到服务端;
- 该方法的内部实现如下: function (value, options, scope) { if (value && value.isModel) { value = value.getId(); } this.set(foreignKey, value); if (Ext.isFunction(options)) { options = { callback: options, scope: scope || this }; } if (Ext.isObject(options)) { return this.save(options); } },因此options的具体内容可以参考Ext.data.Model.save方法的参数配置。
- associationKey(选填):设置从哪个字段读取关联Model子类的数据,默认为"关联Model子类的名称小写s"。(多数用于自关联或嵌套结构的数据中)
现在我们可以通过hasMany和belongsTo两个属性来使Model子类间可双向互操作了。
Ext.define("Author", {
extend: "Ext.data.Model",
requires: ["book"],
fields: [{
name: "id", type: "int"
}, {
name: "name", type: "string"
}],
proxy: {
type: "ajax",
format: "json",
url: "getDummyData.ashx",
reader: {
type: "json",
root: "datas"
}
}
});
Ext.define("Book", {
extend: "Ext.data.Model",
requires: ["Author"],
fields: [{
name: "id", type: "int"
}, {
name: "title", type: "string"
}, {
name: "authorId", type: "int"
}],
associations: { type: 'hasOne', model: 'Author' }
});
var book = Ext.create("Book", {
id: 1,
title: "javascript",
authorId: 1
});
var author = null;
// 异步操作
book.getA(function(author, operation){
author = author;
});
或
book.getA({
reload: true, // 强制向服务端发送数据请求,而不是读cache内容
scope: this,
success: function(author, operation){
author = author;
},
failure: function(author, operation){
..............
},
callback: function(author, operation){
..............
}
});
Ext.data.association.HasOne的配置项与Ext.data.association.BelongsTo的配置项一样。
代理的职责是加载和保存数据。它可以用于Store,也可以直接用于Model。
在Extjs 3中,我们只能从服务端加载数据和将数据保存到服务端。而Extjs 4引入了三种新的代理,通过它们我们可以从客户端加载数据和保存数据到客户端。
Exjts 4中含有两类代理:客户端代理(LocalStorageProxy、SessionStorageProxy和MemoryProxy),服务端代理(AjaxProxy、ScriptTagProxy、DirectProxy和RestProxy)。具体如下图:
客户端代理使用浏览器存储器,这是HTML5的新特性,所以并不是所有浏览器均支持客户端代理。下列为支持该类型代理的部分浏览器:
IE 8.0+
Firefox 3.5+
Safari 4.0+
Chrome 4.0+
Opera 10.5+
IPhone 2.0+
Android 2.0+
HTML5的浏览器存储器以键值对的方式存储数据(数据类型为js的原生数据类型null,undefined,string,number,boolean),其工作方式类似于cookies。但其好处在于不会像cookies那样在每次请求时均在客户端、服务器间传送,并且默认容量为5M且无记录条目数限制,比cookies灵活高效。
下面是客户端代理的类关系图:
- Ext.data.proxy.Client:是所有客户端代理的祖先类,由Extjs框架内部使用;
- Ext.data.proxy.WebStorage:是Ext.data.proxy.LocalStorage和Ext.data.proxy.SessionStorage的父类,由Extjs框架内部使用;
- Ext.data.proxy.LocalStorage
- Ext.data.proxy.SessionStorage
- Ext.data.proxy.Memory
LocalStorageProxy利用HTML5新特性localStorage API来从浏览器加载数据和保存数据到浏览器。而localStorage是根据域名来划分作用域,就是说每个localStorage存储的数据均属于某个域名并仅在该域名下的网页可见。localStorage是持久化存储器,所以若不手动清理的话会一直存储在浏览器中,即使关掉浏览器或重启系统。当然每种浏览器都有自己独立的localStorage,并且彼此不能共享。
localStorage是以键值对得形式保存数据,我们可以直接保存js原始数据类型的值(null,undefined,string,number,boolean),但不能直接保存Array、Object类数据类型值。既然不能直接保存那么我们可以使用JSON.stringify(类数据类型值)将其转换成符合JSON格式的字符串来保存。其实Extjs已经帮我们完成这一切了。LocalStorageProxy会自动完成JSON的序列化和反序列化工作。下面我们通过实例来更好的理解吧!
Ext.define("UserPreference", {
extend: "Ext.data.Model",
fields: [{
name: "id", type: "int"
}, {
name: "description", type: "string"
}],
proxy: {
type: "localstorage",
id: "userpreference"
}
});
1. proxy属性的id为必填项,用于指定保存该Model子类记录id值的localStorage键名;而记录在localStorage的键名由该属性(id)的值加上"-"后再加上记录的id值组成;若Model子类没有设置idgen属性(id生成器)时,就会生成一个名为该属性(id)的值加上"-"后再加上counter的localStorage键来记录该Model子类所保存的记录中已生成的记录id最大值,用于当保存一条无id的新记录到localStorage时为其生成一个id值;
2. 若使用Store来操作localStorage,proxy属性的id成员属性没有设置时,Extjs会将storeId作为值作为proxy属性的id成员属性值。若来两者都没有设置那么就会抛出异常;
3. 若浏览器不支持localStorage,使用该proxy就会抛出异常。
通过Store实例保存数据
var store = Ext.create("Ext.data.Store", {
model: "UserPreference"
});
store.load();
store.add({description: "Blue theme"});
store.add({description: "Loiane Groner"});
store.sync();
通过Model实例保存数据
var a = Ext.create("UserPreference", {
description: "Blue theme"
});
a.save();
var b = Ext.create("UserPreference", {
description: "Loiane Groner"
});
b.save();
结果如下:
若Model子类中设置了idgen属性就不会出现userpreference-counter这一项。
通过Store实例加载数据
var store = Ext.create("Ext.data.Store", {
model: "UserPreference"
});
store.load();
通过Model子类加载数据
UserPreference.load(1,{success:function(record,operation){......}});
(译者语:该小节为译者为让大家更好地理解Model而自行添加的内容)
Ext.data.Model.idgen用于配置记录id生成情况(自增、全球唯一、空),默认为空,就是设为记录字段id的值或者该字段的默认值。而该属性值得类型正是Ext.data.IdGenerator类。Ext.data.IdGenerator为抽象类,其实现子类为Ext.data.SequentialIdGenerator和Ext.data.UuidGenerator 。
Ext.data.SequentialIdGenerator使用方式如下:
Ext.define("Book", {
extend: "Ext.data.Model",
fields: [....],
idgen: "sequential"
});
或者
Ext.define("Book", {
extend: "Ext.data.Model",
fields: [....],
idgen: {
type: "sequential",
seed: 100,
prefix: "John"
}
});
配置项说明:
1. type:使用的id生成器类型;
2. seed:Ext.data.SequentialIdGenerator起始id的值,其余的为上一个id+1;
3. prefix:id的前缀,如上实例,id将形如:John100
Ext.data.UuidGenerator使用方式如下:
Ext.define("Book", {
extend: "Ext.data.Model",
fields: [....],
idgen: "uuid"
});
或者
Ext.define("Book", {
extend: "Ext.data.Model",
fields: [....],
idgen: {
type: "uuid",
id: "test",
version: 1
}
});
配置项说明:
1. type:使用的id生成器类型;
2. id:设置全球唯一Id生成器的ID;
3. version:1代表全球唯一id是基于时间的;4代表全球唯一id是通过伪随机数的。
SessionStorage也是HTML5的新特性,用法跟LocalStorage一样,唯一的区别是关闭浏览器后保存到其中的信息将被清空。
MemoryProxy主要用于加载内联数据,当页面刷新时MemoryProxy的数据将会丢失。当要加载临时数据时,它将会是首选。
实例如下:
Ext.define("Gender", {
extend: "Ext.data.Model",
fields: [{
name: "id", type: "int
}, {
name: "name", type: "string"
}]
});
var data = {
genders: [{
id: 1,
name: "Female"
}, {
id: 2,
name: "Male"
}, {
id: 3,
name: "Unknown"
}]
};
var store = Ext.create("Ext.data.Store", {
autoLoad: true,
model: "Gender",
data: data,
proxy: {
type: "memory",
reader: {
type: "json",
root: "genders"
}
}
});
// ComboBox using the data store
var comboBox = Ext.create("Ext.form.field.ComboBox", {
fieldLabel: "Gender",
renderTo: Ext.getBody(),
displayField: "name",
200,
labelWidth: 50,
store: store,
queryMode: "local",
typeAhead: false
});
结果:
服务端代理通过HTTP请求从web服务端读取数据和保存数据到web服务端。下面为类图:
- Ext.data.proxy.Server:是所有服务端代理的祖先类,由框架内部调用;
- Ext.data.proxy.Ajax:同域异步请求
- Ext.data.proxy.Rest:Ext.data.proxy.Ajax的扩展
- Ext.data.proxy.JsonP:跨域异步请求
- Ext.data.proxy.Direct:使用Ext.direct.Manager来发送请求
AjaxProxy是最常用到的代理类,它将使用Ajax来向服务端发送读取、保存数据的请求。相当于Extjs3中的Ext.data.HttpProxy。
我们只需简单地在Model、Store的proxy属性中设定type为"ajax"就可以了。代码如下:
Ext.define("Book", {
extend: "Ext.data.Model",
fields: [.......],
proxy: {
type: "ajax",
url: "getDummyData.ashx"
}
});
上述的代码与下面的代码功能一样:
var ajaxProxy = Ext.create("Ext.data.proxy.Ajax", {
url: "getDummyData.ashx",
model: "Book",
reader: "json"
});
Ext.define("Book", {
extend: "Ext.data.Model",
fields: [.......],
proxy: ajaxProxy
});
在第一段代码中我们只需定义proxy属性的type和url成员属性即可。在第二段代码中我们添加了model和reader,而在第一段代码中这两个属性使用了默认值,因为Model子类Book已经知道AjaxProxy使用哪个Model子类和默认的Reader是JsonReader。
当我们从服务端读取数据时(read),代理会使用GET方式进行请求,而其他请求(update、insert和delete)均用POST方式。上述的代码设置使得各种操作(read,update,insert,delete)均请求同一个url,其实我们可以通过AjaxProxy的api属性设置不同的操作请求不同的url。实例:
var ajaxProxy = Ext.create("Ext.data.proxy.Ajax", {
api: {
read:"getDummyData.ashx",
update: "updateDummyData.ashx",
insert: "insertDummyData.ashx",
destroy: "deleteDummyData.ashx"
},
model: "Book",
reader: "json"
});
通过Model子类或Store可以加加载数据(译者语:因操作与之前的Model、Store加载数据代码一样,此处省略原文内容)
下面我们来了解AjaxProxy的配置项:
- filterParam:设置url查询参数filter的键名,默认是为filter;设置为undefined时表示url中不存在filter的键名
- groupParam:设置url查询参数group的键名,默认为group;设置为undefined时表示url中不存在group的键名
- pageParam:设置url查询参数page的键名,默认为page,用于在服务端获取特定页码的数据;设置为undefined时表示url中不存在page的键名
- startParam:设置url查询参数start的键名,默认为start,用于在服务端分页;设置为undefined时表示url中不存在start的键名
- limitParam:设置url查询参数limit的键名,默认为limit,用于在服务端分页;设置为undefined时表示url中不存在limit的键名
- sortParam:设置url查询参数sort的键名,默认为sort;设置为undefined时表示url中不存在sort的键名
- directionParam:设置url查询参数dir的键名,默认为dir,用于设置排序方向(DESC或ASC),仅当simpleSortMode为true时有效。设置为undefined时表示url中不存在dir的键名
- simpleSortMode:设置是否只允许对单个字段进行排序,默认为false(即可以对多个字段进行排序);具体实例请看下面的代码。
- extraParams:通过该属性设置的参数在每一次向服务端发起的请求中都会被发送到服务端。若请求的url中存在与之同名的参数,则会覆盖该属性设置的参数。
- api:设置CRUD操作对应各自的url。格式为{create: "具体的url",update: "具体的url",read: "具体的url",destroy: "具体的url"}
- url:设置CRUD操作对应统一的url。api的优先级高于url,若api中某操作对应的url为undefined,则采用url属性的值
- model(必填项):设置绑定的Model子类,可以是类名字符串或类实例对象
- noCache:设置是否每次请求均读取服务端数据,默认为true
- cacheString:设置为了每次请求均读取服务器数据而生产的请求url参数键,默认为_dc。仅noCache为true时有效。
- batchActions:设置启用批量操作,默认为true。批量操作就是对Store实例中的记录作CUD后,执行“store实例.sync()”批量将操作发送到服务端。
- batchOrder:已以逗号作分隔符的方式设置批量操作的顺序,默认为"create,update,destroy"。仅batchActions为true时有效。
- reader:设置解码服务端数据的Reader,默认为"json"
- writer:设置编码客户端数据的Writer,默认为"json"
- timeout:设置等待服务端响应的时限(单位:毫秒),默认30000
译者语:在上面的代码中我们都将代理附加到Model或Store中,初看之下觉得只要配置好代理(客户端、服务端代理)后就能和数据源(服务端数据源或客户端数据源localStorage等)通信,其实是框架帮我们配置了另一个十分重要的实例——Ext.data.Operation,代表在代理中执行的单个读或写操作,而批量操作就是由多个Ext.data.Operation实例组成,一般无需我们直接使用。除非我们直接使用Ext.data.proxy.Ajax实例的read方法,该方法第一个参数就是Ext.data.Operation实例。
下面我们先了解Ext.data.Operation的属性吧:
- action:CRUD操作名,read、update、create、destroy之一。
- filters:Ext.util.Filter实例数组
- sorters:Ext.util.Sorter实例数组
- limit:读取的记录数
- start:读取的首条记录的索引
- groupers:Ext.data.Grouper实例数组
- page:读取的页码
- synchronous:设置是否启用批量操作时,该Ext.data.Operation实例是否参与并发操作,否则要等待某操作执行完成时才会执行。默认为true。
- batch:Ext.data.Batch实例(Ext.data.Batch用于管理批量操作中的各个Ext.data.Operation实例,一般在Ext.data.proxy.Proxy内部使用,就是proxy会自动配置该项),配置时不用配置。
- callback: 操作完成时的回调函数
- scope:回调函数执行上下文对象中的this成员属性值
filters、sorts、limit、start、groupers均在action为"read"时会将值附加到url上。
译者语:为了更好地理解它我们就探讨一下它和Ext.data.proxy.Ajax的关系吧。从Ext.data.proxy.Ajax的属性上我们可以看出,Ext.data.proxy.Ajax设置的均是客户端与数据源(客户端、服务端)的关联和url查询参数的键名,我在这里把这些归纳为设定通信规则;而Ext.data.Operation用于将客户端与数据源的具体通信信息封装起来,在两者之间传递。
实例:
var proxy = Ext.create("Ext.data.proxy.Ajax", {
url: "getDummyData.ashx",
model: "Book",
startParam: "startIndex"
});
var oper = Ext.create("Ext.data.Operation", {
action: "read",
start: 0,
limit: 5
});
proxy.read(oper);
发送到服务器的url为getDummyData.ashx?_dc=........&startIndex=0&limit=5
下面我们尝试排序和过滤吧!
var oper = Ext.create("Ext.data.Operation", {
action: "read",
sorters: [
Ext.create("Ext.util.Sorter", {
property: "Title",
direction: "DESC"
}),
Ext.create("Ext.util.Sorter", {
property: "Name",
direction: "ASC"
})
],
filters: [
Ext.create("Ext.util.Filter", {
property: "Pages",
value: "100"
})
]
});
proxy.read(oper);
发送到服务器的url为getDummyData.ashx?_dc=........&sort=[{property:"Title",direction:"DESC"},{property:"Name",direction:"ASC"}]&filter=[{property:"Pages",value:"100"}]
若设置Ext.data.proxy.Ajax的simpleSortMode配置项为true,则如下:
var proxy = Ext.create("Ext.data.proxy.Ajax", {
url: "getDummyData.ashx",
model: "Book",
startParam: "startIndex",
simpleSortMode: true
});
那么发送到服务器的url为getDummyData.ashx?_dc=........&sort=Title&dir=DESC&filter=[{property:"Pages",value:"100"}]
我们可以看到sorters和filters数组均编码为JSON格式的字符串,其实我们可以通过重写Ext.data.proxy.Ajax的encodeFilters(filters)和encodeSorters(sorters)函数来自定义编码方式。注意:一般我们认为只能设置类在API文档中的Configs类型的成员属性和方法,其实我们可以重写Methods类型的方法。
注意:Ext.data.proxy.Ajax只能向相同域名、端口号、协议、子域名的服务端发起请求。
RestProxy是AjaxProxy的子类,但其是使用RESTful URLs来处理所有CRUD操作。
RESTful URLs的基本原则是在一个基础URL链接服务,使用JSON、XML或YAML编码格式来交换数据。而Extjs中只接受JSON和XML格式,并且使用HTTP中的GET、POST、DELETE和PUT来执行CRUD操作。下面表格展现URL和CRUD操作的映射关系:
使用实例如下:
Ext.define("Book", {
extend: "Ext.data.Model",
fields: [....],
proxy: {
type: "rest",
appendId: true,
// format: "json",
url: "/getDummyData"
}
})
Ext.data.proxy.Rest有两个特殊的属性appendId和format。
- appendId:设置是否启用自动添加记录ID到url后面(如url为/data,记录ID为123,那么最后的url为/data/123),默认为true;若并不是操作单条记录,那么就不会自动添加记录ID到url后面。
- format:设置最终url的后缀(如url为/data,format为json。那么最终url为/data.json;若记录ID为123,那么最终url为/data/123.json)
译者语:因具体的CRUD操作与之前通过Model或Store的一致,因此此处省略原文中的代码。
注意:Ext.data.proxy.Rest只能向相同域名、端口号、协议、子域名的服务端发起请求;
服务端要启用PUT、DELETE等请求方式。(如IIS中默认允许使用PUT、POST请求方式,但没有PUT和DELETE的请求方式)
JsonP代理用于请求不同域名下的数据。Ext.data.proxy.JsonP等同于Extjs3中的Ext.data.ScriptTagProxy类,顾名思义就是每次请求均在页面中添加一个script标签到DOM树中。如使用JsonP代理请求url为http://loiane.com的数据时,就会生成<script src="http://loiane.com?callback=someCallback"></script>
使用实例:
Ext.define("Book", {
extend: "Ext.data.Model",
fields: [.....],
proxy: {
type: "jsonp",
url: "http://loianegroner.com/getDummyData.ashx",
callbackKey: "customerizeKey"
}
});
Book.load(2, {success:function(book, operation){
................................
}});
生成的script标签如下:
<script src="http://loiane.com?customerizeKey=someCallback"></script>
配置项讲解:
callbackKey:设置回调函数名称在url查询参数中的键,默认为callback。
特别注意点:
JsonP的使用不仅需要在客户端设置,还要服务端配合才能生效。正如上述讲解那样,JsonP其实是通过script标签来加载异域数据,假如服务端仅仅返回数据,即使检测到script加载完成也无法使用已加载的数据。因此服务端必须返回一个以JSON格式数据为实参调用回调函数的完成Javascript语句作为返回值,如someCallback({name:"John Huang", age: 25});。当然someCallback这个javascript函数已经有Extjs框架为我们定义好了。
Store的职责是封装Model并配置代理来读取、保存数据。它具有排序、过滤和分组的功能。以下是相关的类图:
我们一般直接用到的是Ext.data.Store和Ext.data.TreeStore,而其他(除Ext.data.AbstractStore外)均为proxy的类型而框架内部设置而成,不能直接实例化使用。
配置项说明:
- autoSync:true为每修改一条记录均会马上发送修改到数据源,false为要显式调用store.sync()才会批量将修改发送到数据源;默认为false。
- remoteSort:true发送Get请求到服务端来进行排序,false客户端排序;默认为false。
Reader类负责将从数据源(服务端或客户端)获取的数据(raw data)进行解码并加载到Model实例或Store实例中。与Extjs3不同的是,Extjs4是用proxy来将Reader进行分组而不是Store。另一个不同点是Extjs3中所有的Reader均位于Ext.data包中,而Extjs4均位于Ext.data.reader包中。但配置项两个版本是相同的,就是说Extjs4的Reader是向后兼容的。下面是类关系图:
下面我们一起来学习Reader中主要的配置项吧!
- idProperty:从数据源读取的记录的id属性名,默认与Model的idProperty一致;
- messageProperty:从数据源读取的数据的message属性名,默认为message;
- root:从数据源读取的数据的记录入口属性名,默认为空字符串;
- successProperty:从数据源读取的数据的success属性名,默认为success;其值为true则表示成功,为false且存在errors属性则表示服务端失败,为false且无errors属性则表示链接失败。
- totalProperty:从数据源读取的数据的total属性名,默认为total;表示总记录数目。
- record:(Ext.data.reader.Xml)从数据源读取的记录起始标签;表示该标签代表一条记录。
Ext.data.reader.Json实例:
var jsonStore = Ext.create("Ext.data.Store", {
autoLoad: true,
fields: [{
name: "id", mapping: "myId", type: "int"
}, {
name: "name", type: "string"
}],
proxy: {
type: "json",
url: "getDummyData.ashx",
reader: {
type: "json",
root: "Books",
successProperty: "s",
totalProperty: "t",
messageProperty: "m"
}
}
});
服务端数据:
{
"t": 2,
"s": true,
"m": "成功了!",
"Books": [{
"myId": 1,
"name": "John Huang"
}]
}
Ext.data.reader.Array实例:
var jsonStore = Ext.create("Ext.data.Store", {
autoLoad: true,
fields: ["id", {
name: "name", type: "string", mapping: 1
}],
proxy: {
type: "array",
url: "getDummyData.ashx",
reader: {
type: "json",
root: "Books",
successProperty: "s",
totalProperty: "t",
messageProperty: "m",
idProperty: "myId"
}
}
});
服务端数据:
{
"t": 2,
"s": true,
"m": "成功了!",
"Books": [[1,"John Huang"]]
}
Ext.data.reader.Xml实例:
var jsonStore = Ext.create("Ext.data.Store", {
autoLoad: true,
fields: ["id", {
name: "name", type: "string"
}],
proxy: {
type: "array",
url: "getDummyData.ashx",
reader: {
type: "json",
root: "Books",
successProperty: "s",
totalProperty: "t",
messageProperty: "m",
record: "Book"
}
}
});
服务端数据:
<t>2</t>
<s>true</s>
<m>成功了!</m>
<Books>
<Book>
<id>1</id>
<name>John Huang</name>
</Book>
</Books>
Writer的职责是发送数据到数据源(客户端或服务端)。和Reader一样,在Extjs4中Writer以proxy来分组。所有Writer均属于Ext.data.writer命名空间。下面是Writer的类关系图:
下面我们一起了解Writer的配置项吧!
- nameProperty:设置发送到数据源(客户端或服务端)的键值对中键名所来源的Ext.data.Field的属性,默认为name;
- 实例:
- writeAllFields:设置为true时将记录的所有字段均发送到数据源(客户端或服务端);false时则仅发送标记为modified的字段到数据源。默认值为true。注意被标记为persist为false的字段将不发送到数据源。
(译者语:因本人觉得原文过于累赘且没有很好地讲解JsonWriter的用法,因此以下内容均为结合API文档后的理解笔记)
望文生义可知JsonWriter就是把Model实例的字段编码成JSON格式字符串,并发送到数据源。因此它的配置项又在上述的配置项之上添加了两个,下面我们来学习吧!
- root:设置数据的入口成员属性名称,默认为空字符串;实例:root为"datas",那么传送的JSON格式字符串为{"datas":[{数据对象1},{数据对象2},.......]}
- encode:false时编码;true时采取Ext.encode对数据进行编码,默认为false。注意,该属性仅在设置了root属性的情况下生效,因为encode为true时会对"[数据对象1,数据对象2,.......]"整体进行编码,那么数据源端仅能通过root属性值来获取已编码的数据字符串。
- allowSingle:false时要求数据必须被包含在数组内,默认为true;实例:allowSingle为true,数据的JSON格式字符串可以形如{"datas":{数据对象1}};为false时,必须为{"datas":[{数据对象1}]}
(译者语:因本人觉得原文过于累赘且没有很好地讲解JsonWriter的用法,因此以下内容均为结合API文档后的理解笔记)
望文生义可知JsonWriter就是把Model实例的字段编码成XML格式字符串,并发送到数据源。因此它的配置项又在上述的配置项之上添加了其他配置项,下面我们来学习吧!
- defaultDocumentRoot:设置默认的xml文档的顶级节点标签名称,默认为xmlData;当documentRoot为空时生效。
- documentRoot:设置xml文档的顶级节点标签名称,默认为xmlData;
- record:设置单条数据记录的外层节点标签名称,默认为record;
- header:设置xml文档的头信息,如版本号和编码方式,默认为空字符串。
注意:当数据源位于客户端是不要使用XmlWriter。
Store带有记录排序的功能(译者语:经过上几节内容的学习,我想大家都清楚Store的记录排序功能其实是通过Ext.data.proxy.Proxy子类和Ext.data.Operation类实现的)。在Extjs3中我们使用sortInfo来配置排序规则,可在客户端或服务端执行排序操作。在Extjs4中每个排序规则均为一个独立的Ext.util.Sorter实例对象。我们即使在Store实例化阶段配置好了排序规则,在之后的使用中也能改变排序规则。
下面我们来看一下实例吧:
Ext.define("Book", {
extend: "Ext.data.Model",
fields: [{
name: "id",
type: "int"
}, {
name: "title",
type: "string"
}, {
name: "pages", type:"int"
}],
proxy: {
type: "ajax",
url: "data/books/books.json"
}
});
books.json文件数据如下:
[{
"id": 11,
"title": "Learning Ext JS 3.2",
"pages": 432
},{
"id": 12,
"title": "Learning Ext JS 4.2",
"pages": 333
}]
下面我们创建一个Store和为其配置排序规则:
var store = Ext.create("Ext.data.Store", {
model: "Book",
sorters: [{
property: "pages",
direction: "DESC"
},{
property: "title",
direction: "ASC"
}]
});
或者
var store = Ext.create("Ext.data.Store", {
model: "Book",
sorters: [Ext.create("Ext.util.Sorter",{
property: "pages",
direction: "DESC"
}),Ext.create("Ext.util.Sorter",{
property: "title",
direction: "ASC"
})]
});
这样在数据加载的时候就会对数据进行客户端排序
若想在其他时候对数据进行客户端排序,可如下:
store.sort("pages", "ASC");
或者
store.sort([{
property: "pages",
direction: "ASC"
},{
property: "title",
direction: "DESC"
}]);
若要执行服务端排序,则Store需要配置remoteSort属性为true(默认为false)
服务端排序,则会以GET方式向服务端发送请求,请求的URL中带排序信息查询参数。
Store带有记录过滤的功能(译者语:经过上几节内容的学习,我想大家都清楚Store的记录过滤功能其实是通过Ext.data.proxy.Proxy子类和Ext.data.Operation类实现的)。可在客户端或服务端执行数据过滤操作。在Extjs4中每个排序规则均为一个独立的Ext.util.Filter实例对象。我们即使在Store实例化阶段配置好了过滤规则,在之后的使用中也能改变过滤规则。
实例如下:
Ext.define("Book", {
extend: "Ext.data.Model",
fields: [{
name: "id",
type: "int"
}, {
name: "title",
type: "string"
}, {
name: "pages", type:"int"
}],
proxy: {
type: "ajax",
url: "data/books/books.json"
}
});
var store = Ext.create("Ext.data.Store", {
model: "Book",
filters: [{
property: "pages",
value: "23"
},{
property: "title",
value: "Extjs"
}]
});
或者
var store = Ext.create("Ext.data.Store", {
model: "Book",
filters: [Ext.create("Ext.util.Sorter",{
property: "pages",
value: "23"
}),Ext.create("Ext.util.Sorter",{
property: "title",
value: "Extjs"
})]
});
这样在数据加载的时候就会对数据进行客户端数据过滤
若想在其他时候对数据进行客户端数据过滤,可如下:
store.filter("pages", "32");
或者
store.filter([{
property: "pages",
value: "1111"
},{
property: "title",
value: "Ext"
}]);
若要执行服务端数据过滤,则Store需要配置remoteFilter属性为true(默认为false)
服务端数据过滤,则会以GET方式向服务端发送请求,请求的URL中带排序信息查询参数。
本章向大家介绍了Ext.data.Model及其相关的Ext.data.association、Ext.data.reader、Ext.data.writer包的类、Ext.data.proxy包的类、Ext.data.Field、Ext.data.Operation和Ext.data.Store等Extjs框架中关于数据存储、读写、排序、过滤的知识。希望大家已经对此有了基本的了解吧!