您的位置:首页 > Web前端 > JavaScript

在开发 ExtJS 应用程序常犯的 10 个错误

2013-07-18 19:08 148 查看
这是CNX公司在开发ExtJS项目中总结的需要特别注意的10个地方。有时候,我们完全是自己使用ExtJS从零开始构建的新的应用程序,但有时候我们的客户会要求我们使用他们自己的代码,并且帮助他们提高性能、解决bug以及结构上的问题。同时,我们已经在ExtJS项目中作为“清洁者”的角色工作相当长的一段时间了,开始注意到项目中经常会出现一些很值得优化的代码,而这些情况不仅仅只在我们的项目中出现,也会经常出现在其他开发者的代码中。基于过去几年对我们自己工作的总结,我们想应该把这些经验分享给大家,避免大家在项目中出现类似的问题。

过度或不必要的嵌套组件结构

开发时常出现的一个问题是没有原因的使用嵌套组件。这样做会损害软件的性能,也会给软件造成像两条边界或者不正常的图层等不好看的界面。在下面的示例1A中,我们有一个包含一个简单表格的面板。在这种情况下,这个面板是不必要的。因为在示例1B中,去掉这个额外的面板依然可以工作得很好。请记住这些格式:trees,tabpanels以及grids这些全部都是继承自Panel,无论什么时候使用这些组件时,都不需要额外再添加嵌套面板的。

items:[{

xtype:'panel',

title:‘MyCoolGrid’,

layout:‘fit’,

items:[{

xtype:'grid',

store:'MyStore',

columns:[{...}]

}]

}]



示例1A.不好:这个‘panel’是不必要的。


layout:‘fit’,

items:[{

xtype:'grid',

title:‘MyCoolGrid’,

store:'MyStore',

columns:[{...}]

}]


示例1B.好:这个grid已经是一个panel了,所以直接设置panel中的属性就可以了。

因清除组件对象失败而导致内存泄露

很多开发者都遇到过这样的问题,为什么他们编写的应用程序使用过程中会变得越来越慢呢?很大的可能是因为清除不再使用的和一些无关紧要(用户导航)的组件对象失败造成的。在示例2A所示,用户每一次右击表格中某一行时,一个新的上下文菜单就被创建了。如果用户一直运行这个应用程序,并且右击很多次,那么程序中将会存在非常多的未被销毁并且永远都不会销毁的对象。对于开发者和用户来看,因为只会看到最后一个菜单,所以这个应用程序“看起来”是没问题的。其实只是因为其余创建的菜单对象暂时都被隐藏起来了。因为新的菜单创建前没有释放旧的菜单对象,应用程序所使用到的内存将会不断地增长。这样最终造成了响应变慢或浏览器的崩溃。

示例2B是更合理的,因为菜单只会在表格初始化时创建一次,并且用户每一次的右击都是使用的同一个菜单对象。但是,如果表格对象被销毁时,即这个菜单对象以后都不会再用到了,但是依然没有被销毁掉而永远都占用着内存。最好的解决方案是示例2C,因为菜单对象会随着表格对象的销毁而销毁。

Ext.define('MyApp.view.MyGrid',{

extend:'Ext.grid.Panel',

columns:[{...}],

store:‘MyStore’,

initComponent:function(){

this.callParent(arguments);

this.on({

scope:this,

itemcontextmenu:this.onItemContextMenu

});

},


onItemContextMenu:function(view,rec,item,index,event){

event.stopEvent();

Ext.create('Ext.menu.Menu',{

items:[{

text:'DoSomething'

}]

}).showAt(event.getXY());


}

});


示例2A.差:每一次右击都会创建一个菜单并且每一个菜单都不会被销毁掉.

Ext.define('MyApp.view.MyGrid',{

extend:'Ext.grid.Panel',

store:'MyStore',

columns:[{...}],

initComponent:function(){

this.menu=this.buildMenu();

this.callParent(arguments);

this.on({

scope:this,

itemcontextmenu:this.onItemContextMenu

});

},


buildMenu:function(){

returnExt.create('Ext.menu.Menu',{

items:[{

text:'DoSomething'

}]

});

},


onItemContextMenu:function(view,rec,item,index,event){

event.stopEvent();

this.menu.showAt(event.getXY());

}

});


示例2B.好一些:菜单只会在表格初始化时创建一次并且每次使用的都是同一个菜单对象.

Ext.define('MyApp.view.MyGrid',{

extend:'Ext.grid.Panel',

store:'MyStore',

columns:[{...}],

initComponent:function(){

this.menu=this.buildMenu();

this.callParent(arguments);

this.on({

scope:this,

itemcontextmenu:this.onItemContextMenu

});

},


buildMenu:function(){

returnExt.create('Ext.menu.Menu',{

items:[{

text:'DoSomething'

}]

});

},


onDestroy:function(){

this.menu.destroy();

this.callParent(arguments);

},


onItemContextMenu:function(view,rec,item,index,event){

event.stopEvent();

this.menu.showAt(event.getXY());

}

});


示例2C.最好:当表格销毁时,菜单也会跟着销毁.

庞大的控制器代码

每当我们看到一个包含了上千行的代码的巨大控制器,都会感到非常惊讶。我们倾向于喜欢通过分割成小函数的方法来打断这个巨大的控制器。例如,一个订单处理程序可能包含流水线项、载货量,用户搜索等独立的逻辑控制块。通过拆分成小的处理函数能使更查找和管理这些代码变得更加简单和方便。

一些开发者喜欢通过添加新视图代码来解决大控制器代码的问题。例如,如果一个应用程序有一个表格和一个表单,通常将会有一个控制器管理表格和另一个控制器来管理表单。当然,不存在一种“绝对正确”的方法区分你现在正在写的代码是否使用了正确的逻辑控制器代码。但只需要记住,控制器是用来与其他控制器进行交互的。在示例3A中,你可以看到怎么从另外一个控制器取回引用对象,并再次调用它的函数。

this.getController('SomeOtherController').runSomeFunction(myParm);



示例3A.获取另外一个控制器的引用并且调用它的方法.


另外,你可以触发一个应用程序级别(即所有控制器都可以监听)的事件。在示例3B和3C中,你可以看到怎么在一个控制器中触发一个应用程序级别的事件,并且在另外一个控制器监听到这个触发的事件。

MyApp.getApplication().fireEvent('myevent');


示例3B.触发一个应用程序级别的事件.


MyApp.getApplication().on({

myevent:doSomething

});


示例3C.另一个触发器在监听这个应用程序级别的事件.

注意:在ExtJS4.2版本后,使用多个控制器将变得更加容易。因为他们可以直接触发其他控制器正在监听的事件.

不合理的源代码目录结构

这不会影响到程序的性能,但是会使得在理解项目结构时会造成一些困难。因为随着项目变得越来越大,如果你能好好组织项目目录结构,会使得查找源代码以及添加新的功能和属性变得更加轻松。我们之前见过很多开发者将所有的视图层代码(甚至造成app目录包含大量的文件)放在一个文件夹下,就像示例4A所示。我们推荐通过逻辑功能划分视图层代码,正如示例4B所示。



示例4A.差:所有视图层都在一个目录下.




示例4B.好:通过逻辑功能划分很好地组织了视图层代码.

滥用全局变量

尽管大家都知道全局变量很不可取,但我们仍然经常在一些应用程序中经常看到他们的身影。使用全局变量经常会带来一些严重的问题,例如命名冲突、非常难以调试等。相对于使用全局变量,我们更推荐使用类中的一个属性来实现全局变量的功能,然后通过类中的getters和setters方法实现设置类中的属性的功能。

例如,假设你的应用程序需要记住最后选择的客户信息。你可能会在你的应用程序中,尝试定义一个像示例5A中的变量。定义这样一个变量很简单,同时也使得在你应用程序中的所有地方都很方便地使用到这个变量。

myLastCustomer=123456;

示例5A.差:通过创建一个全局变量来存储最后一位操作用户编号.

相反,通过创建一个类来保存这个属性,而不是使用全局变量,将是一个更好的尝试。在这个示例中,我们刚创建一个Runtime.js文件来用于保存可能会在应用程序执行过程中改变的一些属性。示例5B演示了怎么在目录结构中存放这个文件的位置。



示例5B.Runtime.js文件所处项目目录结构的位置.

示例5C.展现了Rumtime.js中的源代码。在示例5D中演示了怎么在你的app.js中“包含”它。你可以像示例5E和5F中一样通过"set"和"get"你的属性。

Ext.define(‘MyApp.config.Runtime’,{
singleton:true,
config:{
myLastCustomer:0//initializeto0
},
constructor:function(config){
this.initConfig(config);
}
});

示例5C.示例代码Runtime.js文件用于保存应用程序中的全局属性.


Ext.application({
name:‘MyApp’,
requires:[‘MyApp.config.Runtime’],
...
});


示例5D.在app.js文件中包含运行时已定义的配置类.


MyApp.config.setMyLastCustomer(12345);

示例5E.保存最后一位客户编号的方法.

MyApp.config.getMyLastCustomer();


示例5F.获取最后一位客户编号的方法.


使用组件"id"属性

我们不推荐在组件中使用"id"属性,因为每一个"id"都必须是唯一的。例如,不小心设置不同控件的id意义,从而造成重复DOM元素(命名冲突)。相反,如果让框架来帮你生成"id"。然后,通过ExtJS的组件查询功能,将会让你明白根本没有必要设置组件的id属性。示例6A展示了一个程序的两个代码段,其中创建了两个不同的保存按钮,两个按钮都被同一个id"savebutton"所定义,导致了一个命名冲突。尽管从下面的代码看这个错误是非常明显的,但在大型的项目开发中将很难发现这命名冲突的问题。

//herewedefinethefirstsavebutton
xtype:'toolbar',
items:[{
text:‘SavePicture’,
id:'savebutton'
}]

//somewhereelseinthecodewehaveanothercomponentwithanidof‘savebutton’
xtype:'toolbar',
items:[{
text:‘SaveOrder’,
id:'savebutton'
}]

示例6A.差:为一个组件分配重复的'id'值,将会导致命名冲突的错误.

相反,如果你想手动地定义每一个组件,你可以像示例6B一样简单地替换'id'属性为'itemId'。这样就解决命名冲突的问题,并且我们仍然可以通过itemId属性得到组件对象。有很多通过itemId取回组件对象的方式。示例6C就展示了一种办法。

xtype:'toolbar',
itemId:‘picturetoolbar’,
items:[{
text:'SavePicture',
itemId:'savebutton'
}]

//somewhereelseinthecodewehaveanothercomponentwithanitemIdof‘savebutton’
xtype:'toolbar',
itemId:‘ordertoolbar’,
items:[{
text:‘SaveOrder’,
itemId:‘savebutton’
}]

示例6B.好:通过‘itemId’创建组件对象.

varpictureSaveButton=Ext.ComponentQuery.query('#picturetoolbar>#savebutton')[1];

varorderSaveButton=Ext.ComponentQuery.query('#ordertoolbar>#savebutton')[1];

//assumingwehaveareferencetothe“picturetoolbar”aspicToolbar
picToolbar.down(‘#savebutton’);

示例6C.好:通过‘itemId’获取组件对象.

编写了不可信赖的组件对象引用代码

我们有时候看到这样的代码,通过组件所处的位置来获取组件对象的引用。因为如果添加、删除或继承不同的组件等这样新的项,就会造成整个项目的奔溃,所以应该尽量避免编写这样的代码。示例7A显示了一些容易出现的情况。

varmySaveButton=myToolbar.items.getAt(2);

varmyWindow=myToolbar.ownerCt;


示例7A.差:应该避免基于组件所处位置获取组件对象引用这种方式.


相反,如果我们使用组件查询功能(ComponentQuery),或者通过组件类(component)中的"up"或者"down"方法,来获取组件的引用对象,正如示例7B中所示一样。使用这种技术将会使得因组件顺序改变时而造成对框架破坏时的影响减少到最小。

varmySaveButton=myToolbar.down(‘#savebutton’);//searchingagainstitemId

varmyWindow=myToolbar.up(‘window’);

示例7B.好:使用ComponentQuery功能来获取相关的组件对象引用.

不恰当的命名方式

在为组件设置名字、属性、类型等时,应该遵循sencha的大小写标准。这样,能够避免给以后维护者造成理解困难和使你能保持干净整洁的代码,你应该遵循统一的命名标准。示例8A展示了几个不正确的方案。示例8B演示了恰当的变量命名方案。

Ext.define(‘MyApp.view.customerlist’,{//shouldbecapitalizedandthencamelCase
extend:‘Ext.grid.Panel’,
alias:‘widget.Customerlist’,//shouldbelowercase
MyCustomConfig:‘xyz’,//shouldbecamelCase
initComponent:function(){
Ext.apply(this,{
store:‘Customers’,
….
});
this.callParent(arguments);
}
});


示例8A.差:存在多处不恰当的大小写命名方式.


Ext.define(‘MyApp.view.CustomerList’,{
extend:‘Ext.grid.Panel’,
alias:‘widget.customerlist’,
myCustomConfig:‘xyz’,
initComponent:function(){
Ext.apply(this,{
store:‘Customers’,
….
});
this.callParent(arguments);
}
});


示例8B.好:在所有命名方式都遵循统一的规范.


另外,如果你触发任何自定义的事件,这个事件的名字都应该保持小写方式。当然,如果你不遵循这些规则,你的代码仍然可以工作。但是为什么一定要特立独行,去写一些不干净不整洁又令人讨厌的代码呢?

强制在类中配置某些属性

在示例9A中,Panel类中强制设置了‘region’属性为'center',但如果你想重新设置组件的显示方式(例如将它放到"west"区域)时,通常就会使得情况变得比较麻烦。

Ext.define('MyApp.view.MyGrid',{
extend:'Ext.grid.Panel',
initComponent:function(){
Ext.apply(this,{
store:‘MyStore’,
region:'center',
......
});
this.callParent(arguments);
}
});


示例9A.差:不应该在类的初始化函数位置强制设置‘center’属性.


相反的,就像示例9B所示,在创建一个组件对象时,来通过参数设置组件中的一些配置属性是比较好的方式。因为通过这种方式,你可以在任何你想改变的地方通过层的配置属性来重新配置这个组件,而不是采用强制方式。

Ext.define('MyApp.view.MyGrid',{
extend:'Ext.grid.Panel',
initComponent:function(){
Ext.apply(this,{
store:‘MyStore’,
......
});
}
});

//specifytheregionwhenthecomponentiscreated...
Ext.create('MyApp.view.MyGrid',{
region:'center'
});

示例9B.好:在创建组件时,设定区域属性.

就像示例9C所示,你也可以在组件上提供一个默认的region属性,这个属性可以在必要的情况下被覆盖掉。

Ext.define('MyApp.view.MyGrid',{
extend:'Ext.grid.Panel',
region:'center',//defaultregion
initComponent:function(){
Ext.apply(this,{
store:‘MyStore’,
......
});
}
});

Ext.create(‘MyApp.view.MyGrid’,{
region:‘north’,//overriddenregion
height:400
});

示例9C.也好:在类初始化时提供一个默认值并可以在需要时覆盖掉这个默认值.

编写了非必要但很复杂的代码

我们已经看过很多次在某些项目中存在不必要的但又很复杂的代码。这通常会导致每一个组件中有用的函数变得很啰嗦。一种通常出现的情况是,在加载一个数据集store时采用将数据集中的每一个选项都加载一次。示例10A显示了这种情况。

//supposethefollowingfieldsexistwithinaform
items:[{
fieldLabel:‘User’,
itemId:‘username’
},{
fieldLabel:‘Email’,
itemId:‘email’
},{
fieldLabel:‘HomeAddress’,
itemId:‘address’
}];

//youcouldloadthevaluesfromarecordintoeachformfieldindividually
myForm.down(‘#username’).setValue(record.get(‘UserName’));
myForm.down(‘#email’).setValue(record.get(‘Email’));
myForm.down(‘#address’).setValue(record.get(‘Address’));

示例10A.差:单独加载表格属性中的每一项.

相对于单独加载数据集中的每一项,通过使用loadRecord方法,仅仅一行代码就实现了来从数据集record中加载所有数据项的功能。只需要确保表格中的"name"属性和数据集record中的数据项一致即可。正如示例10B所示一样。

items:[{
fieldLabel:‘User’,
name:‘UserName’
},{
fieldLabel:‘Email’,
name:‘Email’
},{
fieldLabel:‘HomeAddress’,
name:‘Address’
}];

myForm.loadRecord(record);


示例10B.好:仅通过一行代码,使用loadRecord来加载表格中的所有属性.


这仅仅只是一个关于在实际项目中会出现很多会是代码变得比较复杂但又没必要的情况。关键点是需要了解所有组件中的方法和示例,来确保你正在使用简单和恰当的方式。

CNX

CNX组织是Sencha认证的合作伙伴。Sencha合作伙伴是一个非常有价值的Sencha专业服务团队的扩展。

CNX是定制开发商业应用程序的领导者,创建于1996年。CNX于2008年标准化了ExtJS基于浏览器的用户开发接口,于2010年定义了SenchaTouch作为移动应用程序开发标准。我们已经为世界各地各个领域客户,例如教育、财政、食物、法律、统计、制造业、出版及零售等,创建了世界级的web应用程序。我们的开发团队是在Chicago城市进行办公,并且接受任何大小的项目。CNX能够独立开发或者和你们的团队一起开发,来快速和低费用地达到项目的预期目标。可以在[http://www.cnxcorp.com]联系我们。

原文信息

Top10ExtJSDevelopmentPracticestoAvoid
https://www.sencha.com/blog/top-10-ext-js-development-practices-to-avoid/

译者的感想

开发近半年的ExtJS应用程序,确实出现了文中所提到的全局变量、强制设置组件id等这样的问题。计划慢慢地整理下代码,同时在编写新功能时尽量少出现这些不规范的代码。这篇总结得很精辟,很实用。同时也觉得,借鉴前辈的经验,能让自己在编程方面成长得更快更好。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐