2.7 Ext JS 4语法
1.配置对象
Ext JS的基本语法就是使用树状的配置对象来定义界面,其格式如下:
{
config_options1:value1,
config_options1:value2,
…
config_optionsn:valuen,
layout:{},
items:[
{},//配置对象
{}//配置对象
…
],
listeners:{
//定义事件(根据需要而定)
click:function(){},
dblclick:function(){}
…
}
}
格式中从config_options1、config_options2到config_optionsn都是API文档中对象的配置项(config options)。很多初学者会错误地认为API不全,找不到配置项。事实上API是完整的,只是有些布局隐含了面板或其他部件,但都在同一配置对象内定义。例如在使用Accordion布局的时候,想消除布局标题栏右边的小图标,需要使用hideCollapseTool属性,而该属性是在Panel对象里的。在这方面,Ext JS 4的API已经做了调整了,增加了一个其他配置项(Other Configs)的列表。
属性layout可以是对象,也可以是字符。该属性表示在当前容器内使用什么布局来填充子控件。如果没有特殊要求,直接使用布局的别名作为值,例如,2.3节的示例中Viewport使用了Fit布局来放置子控件。如果有特殊要求,则要使用对象来定义该值。例如,如果使用Hbox布局,布局内的子控件需要居中对齐,则定义如下:
layout:{
type:'hbox',
align:'middle'
}
属性items是一个数组,可以在里面定义当前控件的子控件,里面可以是1个或多个配置项,根据你的需要而定。例如2.3节的示例,在Viewport下使用了一个Panel面板作为其面板。属性listeners是一个对象,可以在里面绑定事件,对象的属性就是事件名称,属性值就是要执行的函数。
2.关于xtype
在使用Ext JS编写应用时,很多时候通过定义xtype来指定该位置使用什么组件,例如2.3节的示例中的“xtype:"panel"”,这里指定使用面板作为当前位置的组件。这样做的主要目的是简化代码。如果没有xtype,就得先使用变量指向一个组件,然后将其加入父组件中,或者直接在父组件的定义中加入一堆由new关键或Ext.Create方法创建的组件。这不但影响了书写代码的效率,还影响了代码的可读性。有xtype存在,就不用担心这些问题了。
在某些组件下,会默认其内部组件为某些组件,因而也就不需要书写xtype语句。例如,在2.3节的示例中把xtype去掉,代码也能正常运行,因为面板是Viewport内默认的组件。一般来说,没特别声明,使用的都是Panel组件。
定义xtype,一般使用组件的别名。可在API文档的组件说明文档的顶部或Component对象的说明中找到组件的xtype值。
- 使用new关键字创建对象
在Ext JS 4版本之前,一直使用new关键字创建对象,其语法如下:
new classname([config])
其中calssname是指类名;config是可选参数,为类的配置对象(config options),类型为JSON对象。在2.3节的示例中,Ext.Viewport就是使用该方法创建的。
4.使用Ext.create方法创建对象
Ext.create方法是新增的创建对象的方法,其语法如下:
Ext.create(classname,[config])
其中classname可以是类的全名、别名或备用名;config是可选参数,为类的配置对象(config options),类型为对象。将2.3节的示例中的以下代码:
new Ext.Viewport({
修改为:
Ext.create('Ext.Viewport',{
效果是一样的。那为什么要增加这样一个方法呢?我们来研究一下create方法的源代码。
在ClassManger.js文件中,可以找到以下代码:
Ext.apply(Ext, {
create: alias(Manager, 'instantiate'),
...
})
从代码可知create方法其实是Ext.ClassManager类的instantiate方法的别名,其代码如下:
instantiate: function() {
var name = arguments[0],
nameType = typeof name,
args = arraySlice.call(arguments, 1),
alias = name,
possibleName, cls;
if (nameType != 'function') {
if (nameType != 'string' && args.length === 0) {
args = [name];
name = name.xclass;
}
//省略调试代码
cls = this.get(name);
}
else {
cls = name;
}
if (!cls) {
possibleName = this.getNameByAlias(name);
if (possibleName) {
name = possibleName;
cls = this.get(name);
}
}
if (!cls) {
possibleName = this.getNameByAlternate(name);
if (possibleName) {
name = possibleName;
cls = this.get(name);
}
}
if (!cls) {
//省略调试代码
Ext.syncRequire(name);
cls = this.get(name);
}
//省略调试代码
return this.getInstantiator(args.length)(cls, args);
},
首先将name变量指向从参数中获取的类名,然后判断name是不是函数,如果不是,且其不是字符串,则从xclass属性中获取类名。接着使用get方法获取对象,并将cls变量指向对象。
get方法的源代码如下:
get: function(name) {
var classes = this.classes;
if (classes[name]) {
return classes[name];
}
var root = global,
parts = this.parseNamespace(name),
part, i, ln;
for (i = 0, ln = parts.length; i < ln; i++) {
part = parts[i];
if (typeof part != "string") {
root = part;
} else {
if (!root || !root[part]) {
return null;
}
root = root[part];
}
}
return root;
},
代码中的classes对象包括了Ext JS的所有类,因而代码首先根据name从classes中寻找类,如果存在,则返回对象;如果不存在,则说明是用户自定义的类,需要从Ext.global中寻找。
查找工作首先要做的是使用parseNamespace方法拆解类名,其代码如下:
parseNamespace: function(namespace) {
//省略调试代码
var cache = this.namespaceParseCache;
if (this.enableNamespaceParseCache) {
if (cache.hasOwnProperty(namespace)) {
return cache[namespace];
}
}
var parts = [],
rewrites = this.namespaceRewrites,
root = global,
rewrite, from, to, i, ln;
for (i = 0, ln = rewrites.length; i < ln; i++) {
rewrite = rewrites[i];
from = rewrite.from;
to = rewrite.to;
if (namespace === from || namespace.substring(0, from.length) === from) {
namespace = namespace.substring(from.length);
if (typeof to !== "string") {
root = to;
} else {
parts = parts.concat(to.split("."));
}
break;
}
}
parts.push(root);
parts = parts.concat(namespace.split("."));
if (this.enableNamespaceParseCache) {
cache[namespace] = parts;
}
return parts;
},
代码中namespaceParseCache对象的作用是使用类名作为关键字并指向拆解后的类名数组,这样,当该类被多次使用时,就可以直接从namespaceParseCache对象中获取拆解的类名数组,而不需要再拆解一次,从而加快运行速度。
通过enableNamespaceParseCache属性可配置是否开启namespaceParseCache对象的缓存功能,默认是开启的。如果在enableNamespaceParseCache中已存在类名的拆解结果,则返回结果。
属性namespaceRewrites的定义如下:
namespaceRewrites: [{
from: 'Ext.',
to: Ext
}],
在其他文件中找不到为namespaceRewrites添加数据的代码,因而在循环中,如果类名是以“Ext.”开头的,root会指向Ext对象;如果不是,root就是初始值,指向Ext.global。
接着,将root指向的对象存入parts数组,再将拆分类名产生的数组与parts数组合并。如果开启了缓存功能,在namespaceParseCache对象中,将以类名为关键字指向parts数组,最后返回parts数组。数组返回后,通过循环的方式来查找类定义。因为返回数组(parts)的第1个数据不是Ext对象就是Ext.golbal对象,因而变量root在第一次循环时,会指向Ext对象或Ext.golbal对象。在后续循环中,会根据拆分的类名在Ext或Ext.gdbal对象中逐层找下去。如果期间“root[part]”不是对象,说明不存在该类,返回null。如果找到了,返回root指向的对象。
如果cls不是指向对象,则尝试使用别名方法查找对象。查找时,首先使用getName-ByAlias方法将别名转换为类名,其代码如下:
getNameByAlias: function(alias) {
return this.maps.aliasToName[alias] || '';
},
在maps属性中保存了以下3个对象:
maps: {
alternateToName: {},
aliasToName: {},
nameToAliases: {}
},
简单来说,maps中的对象就是一个对照表,通过它就可轻松地找到类名和别名。对象alternateToName的作用是通过备用名获得类名,它以备用名作为关键字、类名作为值;对象aliasToName的作用是通过别名获取类名,它以别名作为关键字、类名作为值;对象nameToAliases的作用是通过类名获取别名,它以类名作为关键字、别名作为值。在创建类的时候,会把类名、别名和备用名写入对照表。
如果getNameByAlias方法返回的不是空字符串,说明变量name保存的是别名,需将name修改为类名,然后通过get方法获取对象。如果使用别名也找不到对象,则可尝试使用备用名来查找对象,执行代码与使用别名查找的方式类似。如果还没有找到,则可尝试使用syncRequire方法下载对象,其代码如下:
syncRequire: function() {
this.syncModeEnabled = true;
this.require.apply(this, arguments);
this.refreshQueue();
this.syncModeEnabled = false;
},
在上面的代码中,syncModeEnabled方法可控制Loader对象的require方法通过同步的方式去下载对象。
如果实在是找不到,就会抛出异常。最后使用getInstantiator方法实例化对象,其代码如下:
getInstantiator: function(length) {
if (!this.instantiators[length]) {
var i = length,
args = [];
for (i = 0; i < length; i++) {
args.push('a['+i+']');
}
this.instantiators[length] = new Function('c', 'a', 'return new c('+args.join(',')+')');
}
return this.instantiators[length];
},
代码中的数组instantiators起缓存作用,因为同样的参数长度产生的匿名函数是一样,没必要每次都重新创建一次。最后返回的匿名函数如下:
(function anonymous(c, a) {return new c(a[0], a[1], … ,a[n]);})
代码中的n就是参数的长度,例如length的值为5,则匿名函数如下:
(function anonymous(c, a) {return new c(a[0], a[1], a[2], a[3], a[4]);})
匿名函数返回后会立即执行,参数c指向对象的cls,a指向实例化对象时的参数。也就是说,对象是在匿名函数中实例化的,这样可以保证对象的作用域是安全的。
从以上的分析可以了解到,创建对象的新方法不但可以实现动态加载,而且可以保证作用域的安全,应该优先使用。
5.使用Ext.widget或Ext.createWidget创建对象
Ext.widget的作用是使用别名来创建对象,其语法与2.7.2节介绍的Ext.create是一样的,只是classname使用的是对象的别名。Ext.createWidget是Ext.widget的另外一种使用方法而已,在源代码中的定义如下:
Ext.createWidget = Ext.widget;
Ext.widget的代码如下:
widget: function(name) {
var args = slice.call(arguments);
args[0] = 'widget.' + name;
return Manager.instantiateByAlias.apply(Manager, args);
},
也就是说,它使用ClassManager对象的instantiateByAlias方法创建对象,其代码如下:
instantiateByAlias: function() {
var alias = arguments[0],
args = slice.call(arguments),
className = this.getNameByAlias(alias);
if (!className) {
className = this.maps.aliasToName[alias];
//省略调试代码
Ext.syncRequire(className);
}
args[0] = className;
return this.instantiate.apply(this, args);
},
代码先是根据别名寻找类名,如果找不到就抛出异常或尝试使用syncRequire方法加载类文件,最后调用instantiate方法创建对象。
6.使用Ext.ns 或 Ext.namespace定义命名空间
在使用C#或Java做开发时,很多时候都会使用命名空间来组织相关类和其他类型。在JavaScript中并没有提供这样的方式,不过可以通过定义一个全局对象的方式来实现,譬如你要定义一个“MyApp”的命名空间,你可以这样:
MyApp ={};
这里要注意,不要使用var语句去定义全局对象。
在Ext JS中,使用Ext.namespace方法可创建命名空间,其语法如下:
Ext.namespace(namespace1,namespace2,…,namespacen)
其中namespace1、namespace2和namespacen都是字符串数据,是命名空间的名字,例如:
//推荐用法
Ext.namespace("MyApp","MyApp.data","MyApp.user");
Ext.ns("MyApp","MyApp.data","MyApp.user");
//或者,不建议使用
Ext.namespace(,"MyApp.data","MyApp.user");
Ext.ns("MyApp.data","MyApp.user");
Ext.ns只是Ext.namespace的简单写法。
在ClassManager.js中可找到Ext.namespace的createNamespace方法的实现代码:
createNamespaces: function() {
var root = global,
parts, part, i, j, ln, subLn;
for (i = 0, ln = arguments.length; i < ln; i++) {
parts = this.parseNamespace(arguments[i]);
for (j = 0, subLn = parts.length; j < subLn; j++) {
part = parts[j];
if (typeof part !== 'string') {
root = part;
} else {
if (!root[part]) {
root[part] = {};
}
root = root[part];
}
}
}
return root;
},
从上面代码可以看到,Ext.namespace创建的命名空间是保存在global对象里的。注意循环结构,数组长度都是先保存到一个局部变量再使用的,原因是JavaScript与C#、Java等语言不同,每次循环都要计算一次数组长度,这样会降低性能。在第一个循环下,使用了parseNamespace方法拆分命名空间的字符串。
在Ext JS初始化时,this指向的是window对象,因而global指向的是window对象。
数组返回到createNamespace方法后,开始执行完循环,最后在window对象下生成了以下结构的全局对象:
{
MyApp:{
Data:{}
}
}
虽然生成的是全局对象,但是在作用域链上,它可以直接在Ext JS的作用域链内搜索对象,而不需要到最外层的全局作用域链搜索对象,这是最大的不同。
在任何编程语言里都不提倡使用全局变量,尤其是在JavaScript里,全局对象在每一层的作用域链里都搜索不到时,才会在全局作用域链里搜索,效率相当低。因此,在JavaScript里不仅不推荐使用全局变量,而且建议当有一个变量不是在当前作用域定义的,并要多次使用时,建议将该变量保存在局部变量中,使用局部变量来进行操作,以避免在域链中多次搜索对象而降低性能。
因此,建议的方法是在Ext对象下创建命名空间,譬如创建以下的命名空间:
Ext.ns("Ext.MyApp","Ext.MyApp.data","Ext.MyApp.user");
最好的方法是使用Ext已定义的命名空间“Ext.app”,如果是扩展或插件则使用“Ext.ux”。
7.使用Ext.define定义新类
在Ext JS 3中,定义新类使用的是Ext.extend方法,因为Ext JS 4的类系统进行了重新架构,所以定义新类的方法也修改为了Ext.define方法,其语法如下:
Ext.define(classname,properties,callback);
其中:
classname:要定义的新类的类名。
properties:新类的配置对象,对象里包含了类的属性集对象,表2-1列出了常用的属性及其说明。
callback:回调函数,当类创建完后执行该函数。
代码中,initConfig方法执行后就会为congfig的属性创建set和get方法,这样创建类的示例后,就可以直接通过set和get方法读写属性的值,譬如例子中用setWidth设置了配置中width的值,使用getHeight获取height的值
定义静态方法,例如:
Ext.define("subclass",{
statics:{
method:function(args){
return new this(args);
}
statics },
constructor:function(config){
this.initConfig(config);
…
},
…
});
var myclass=subclass.method("class");
类的创建过程将在第4章讲解,下面我们来实践一下。在Firefox中打开2.3节的示例,然后打开Firebug,在控制台中输入以下代码:
Ext.define("Calculator",{
constructor:function(){
return this;
},
plus:function(v1,v2){
return v1+v2;
},
minus:function(v1,v2){
return v1-v2;
},
multiply:function(v1,v2){
return v1*v2;
},
divid:function(v1,v2){
return v1/v2
}
});
var cal=new Calculator();
console.log(cal.plus(87,28)); //115
console.log(cal.minus(87,28)); //59
console.log(cal.multiply(7,8)); //56
console.log(cal.divid(28,7)); //4
代码中定义了一个名称为“Calculator”的类,它包含加、减、乘、除4个方法。运行后,将Firebug标签页切换到DOM标签,可看到如图2-2所示的Calculator类。
继续创建一个继承自Calculator类的新类NewCalculator,新类添加了十进制转换为十六进制的方法,代码如下:
Ext.define('NewCalculator',{
extend:'Calculator',
hex:function(v1){
return v1.toString(16);
}
});
var ncal=new NewCalculator();
console.log(ncal.hex(28)); //1c
代码中extend属性的值为Calculator,表示NewCalculator将继承自Calculator类。方法hex是新增的进制转换方法。
运行后可在DOM树中看到如图2-3所示的NewCalculator类。
比较一下图2-3与图2-2,可看到Calculator类的超类(superclass)是Ext.Base,而NewCalculator类的超类是Calculator,这说明,没有使用extend属性定义的类会默认从Ext.Base中继承。
继续我们的实践,这次要做的是将HEX、BIN、OCT这3个实现不同进制转换的类混合到NewCalculator类中。第一步要做的是定义3个实现进制转换的类:
Ext.define('HEX',{
hex:function(v1){
return v1.toString(16);
}
});
Ext.define('BIN',{
bin:function(v1){
return v1.toString(2);
}
});
Ext.define('OCT',{
oct:function(v1){
return v1.toString(8);
}
});
然后将HEX、BIN和OCT三个功能混合到继承自Calculator类的NewCalculator类中,这样NewCalculator除了实现加减乘除功能外,还能实现十进制到二进制、八进制和十六进制的转换,代码如下:
Ext.define('NewCalculator',{
extend:'Calculator',
mixins:{
Hex:'HEX',
Bin:'BIN',
Oct:'OCT'
},
convert:function(value,type){
switch(type){
case 2:
return this.bin(value)
break;
case 8:
return this.oct(value)
break;
default:
return this.mixins.Hex.hex.call(this,value);
break;
}
}
});
var ncal=new NewCalculator();
console.log(ncal.convert(25,2)); //11001
console.log(ncal.convert(25,8)); //31
console.log(ncal.convert(25,16)); //19
在代码中,使用convert方法可进行进制转换,第2个参数type表示要转换的进制类型。
调用mixins属性定义的混合功能有两种方法:第一种是二进制和八进制中使用的直接调用的方法,第二种是十六进制中使用call方法调用的方法。
运行后,在DOM中可看到如图2-4所示的NewCalculator类。
在图2-4中的原型(prototype)节点里,可看到mixins对象中的3个属性都指向了对应的BIN、HEX和OCT对象,而这三个对象中的方法也直接附加到原型上了,这也就解释了为什么可以使用两种方法调用混合功能的方法。要注意的是,如果存在同名方法,譬如将BIN、HEX和OCT定义的方法都修改为convert,那么使用直接调用的方法就会出现错误,因为在JavaScript中,存在同名函数,前面的定义会被最后定义的函数覆盖,所以在不确定是否有同名方法的情况下,建议还是使用第二种call的方法。
定义3个进制转换类实在麻烦,在一个Convert类中,预设好一个type参数,然后根据type定义的类型进行转换就方便多了,代码如下:
Ext.define("Convert",{
config:{
type:"hex"
},
type_num:16,
constructor:function(config){
this.initConfig(config);
return this;
},
applyType:function(type){
this.type_num= type=="hex" ? 16 : ( type=="oct" ? 8 : 2);
return type;
},
convert:function(v){
return v.toString(this.type_num);
}
});
var cv=new Convert();
console.log(cv.convert(29)); //1d
cv.setType("oct");
console.log(cv.convert(29)); //35
cv.setType("bin");
console.log(cv.convert(29)); //11101
在Convert类中,type属性在initConfig执行后会自动生成applyType、setType、getType和resetType这4个方法。当type的值发生改变时,会触发applyType方法,因而可以重写applyType方法以实现所需的功能。在applyType方法中,根据type的值修改了type_num的值,这样在convert方法中就可以使用type_num值直接进行进制转换了。
代码运行后在DOM中可看到如图2-5所示的Convert类。
在图2-5的原型中,可看到与Type有关的4个方法。实现这么简单的功能,每次都要使用new关键字对象太麻烦了。如果是静态类就好了,所以修改代码如下:
Ext.define("Convert",{
statics:{
hex:function(v){
return v.toString(16);
},
oct:function(v){
return v.toString(8);
},
bin:function(v){
return v.toString(2);
}
},
constructor:function(config){
return this;
}
});
console.log(Convert.hex(29)); //1d
console.log(Convert.oct(29)); //35
console.log(Convert.bin(29)); //11101
在Convert类中,3个进制转换方法都定义在statics属性中。运行后在DOM树中可看到如图2-6所示的Convert类。
在图2-6中,3个进制转换方法不在原型里,而是直接挂在类节点下。现在大家应该清楚静态类和其他类的区别了。