原文地址:http://www.sencha.com/blog/top-10-ext-js-development-practices-to-avoid/
作者:Sean Lanktree
Sean is an Ext JS Professional Services Lead at CNX Corporation.
在CNX,尽管大多数的Ext JS开发工作需要从0开始创建新的应用程序,偶尔会有客户让我们帮他们解决内部工作上的性能问题、臭虫和结构性问题。我们以“清洁工”这种角色进行工作已经有很长一段时间了,在我们审查过的应用程序中,我们注意到,有一些共同的不明智的编码方法经常会出现。基于过去几年的审查工作,我们列出了十个我们建议的,在Ext JS应用程序中应该避免的开发方法。
开发人员最常见的错误之一是没理由的嵌套组件。这样做,会影响性能和也会造成应用程序的不美观,如爽边框火意外的布局行为。在下面的示例1A,在面板内只包含了一个Grid。在这种情况下,该面板是不必要的。如示例1B所示,额外的面板可以取消。要记住的是,表单面板、树面板、标签面板和Grid面板都是从面板扩展的,隐藏,在使用这些组件的时候,应该特别注意不要的嵌套情况。
items: [{ xtype : 'panel', title: ‘My Cool Grid’, layout: ‘fit’, items : [{ xtype : 'grid', store : 'MyStore', columns : [{...}] }] }]
示例1A 不好的:面板(panel)是不必要的
layout: ‘fit’, items: [{ xtype : 'grid', title: ‘My Cool Grid’, store : 'MyStore', columns : [{...}] }]
示例1B 好:Grid已经是面板,因而可以直接在Grid中使用任何面板属性
许多开发人员不知道为什么他们的应用程序随着使用时间越长越来越慢。在用户浏览整个应用程序期间清理未使用组件失败是最大的一个原因。在下面的实例2A中,每次用户右键单击Grid的行,都会创建一个新的右键菜单。如果用户保持应用程序处于打开状态并右键单击行上百次,那么,就会有上百个永远不会被摧毁的右键菜单。对于开发人员和用户来说,应用程序“看上去”显示是争取的是因为只有最后一个被创建的菜单能显示在页面上,而且与的则是隐藏的。由于没有创建新菜单并没有清理旧的,应用程序的内存利用率就会不断增长,这最终将导致较慢的操作或浏览器崩溃。
示例2A就很好,由于右键菜单只在Grid初始化时创建一次,并在用户每次右键单击行时重复使用。不过,如果Grid被销毁,右键菜单一直存在,尽管它不再需要。最好的方式是示例2C,在Grid销毁的时候,把右键菜单也销毁。
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 : 'Do Something' }] }).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(){ return Ext.create('Ext.menu.Menu',{ items : [{ text : 'Do Something' }] }); }, onItemContextMenu : function(view,rec,item,index,event){ event.stopEvent(); this.menu.showAt(event.getXY()); } });
示例2B 较好:菜单会在Grid创建时被创建,且每次可重用
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(){ return Ext.create('Ext.menu.Menu',{ items : [{ text : 'Do Something' }] }); }, onDestroy : function(){ this.menu.destroy(); this.callParent(arguments); }, onItemContextMenu : function(view,rec,item,index,event){ event.stopEvent(); this.menu.showAt(event.getXY()); } });
示例2C 最好:Grid被销毁时,右键菜单也被销毁
当看到应用程序拥有一个上千行代码的超级控制器的时候,我们不知道震惊了多少次。我们更倾向于根据应用程序功能拆分控制器。例如,订单处理应用程序可能会有划分条目、出货量、客户查找等控制器。这将使导航和维护代码更容易。
一些开发人员喜欢根据视图拆分控制器。例如,如果一个应用程序有一个Grid和表单,这将会使用一个控制器来管理Grid和使用一个控制器来管理表单。并没有一个“正确”的方式来划分控制器逻辑,只要是一致的就行。要记住,控制器可以与其他控制器进行通信。在示例3A,可以看到如何检索其他的控制器并调用其中的方法。
this.getController('SomeOtherController').runSomeFunction(myParm);
示例3A 获取另一个控制器的引用并调用它的方法。
作为替代,也可以触发任何控制器可以监听到的应用程序层事件。在示例3B和3C,可以看到如何在一个控制器触发一个应用程序层事件并在另外一个控制器监听它。
MyApp.getApplication().fireEvent('myevent');
示例3B 触发一个应用程序层事件
MyApp.getApplication().on({ myevent : doSomething });
示例3C 在另一个控制器监听应用程序层事件
注意: 自从Ext JS 4.2开始,使用多个控制器变得更容易了——他们可以触发其他控制器可以直接监听的事件
这虽然不影响性能和操作,但会让找跟进应用程序结构变得困难。随着应用程序的增长,如果源代码很有组织,那么寻找源代码以及增加特性或功能就会很容易。我们常常看到许多开发人员会将所有视图(即使是很大的应用程序)如示例4A一样放在一个目录。我们建议如示例4B所示通过逻辑功能来组织视图。
示例4A 不好:所有视图都处于同样层次
示例4B 好:视图根据逻辑功能进行组织
即使全局变量是不好的已经广为人知,但仍然会在我们审查过的一些应用程序中看到他们的身影。使用全局变量的应用程序可能会有名称冲突等重大问题,并且很难去调试。不使用全局变量,可以在类中定义“属性”,并引用这些属性的getter和setter。
例如,假设应用程序需要记录最后选择的客户。一般情况下会如示例5A那样在应用程序中定义一个变量,这很容易且值可以很便利的被应用程序的其他部分使用。
myLastCustomer = 123456;
示例5A 不好:创建全局变量来存储最后的客户编号
作为替代,好的做法是创建一个用来保存属性的类来代替全局变量。在当前情况下,可以创建一个名为Runtime.js来保存应用程序中要使用到的运行属性。示例5B显示了Runtime.js在源代码结构中的位置。
示例5B Runtime.js文件的位置
示例5C显示了Runtime.js文件的内容,而示例5D显示了如何在app.js中“请求(require)”它。在应用程序中,就可以如5E或5F那样在任何地方“设置(set)”或“获取(get)”属性。
Ext.define(‘MyApp.config.Runtime’,{ singleton : true, config : { myLastCustomer : 0 // initialize to 0 }, constructor : function(config){ this.initConfig(config); } });
示例5C 用来为应用程序保存全局属性的Runtime.js文件示例
Ext.application({ name : ‘MyApp’, requires : [‘MyApp.config.Runtime’], ... });
示例5D 在app.js文件中请求Runtime类
MyApp.config.setMyLastCustomer(12345);
示例5E 设置最后客户的方式
MyApp.config.getMyLastCustomer();
示例5F 获取最后客户的方式
我们不建议在组件上使用id,因为每一个id必须是唯一的。而这样很容易导致不小时使用了相同的id,从而引起重复的DOM id(名称冲突)。相反,应该让框架来处理id的生成。使用Ext JS的组件查询,没有任何理由要为Ext JS组件指定一个id。示例6A显示了一个应用程序中的两段代码,在这里创建了两个不同的保存按钮,而这两个保存按钮的id都为“savebutton”,从而导致了名称冲突。显而易见,在下面的代码很容易找出名称冲突,但在一个大型应用程序中,要确定名称冲突会很困难。
// here we define the first save button xtype : 'toolbar', items : [{ text : ‘Save Picture’, id : 'savebutton' }] // somewhere else in the code we have another component with an id of ‘savebutton’ xtype : 'toolbar', items : [{ text : ‘Save Order’, id : 'savebutton' }]
示例6A 不好:为组件分配了重复id将造成名称冲突
替代方法是,如果需要手动确定每一个组件的,可以如示例6B那样使用itemid代替id。这就可以解决命名冲突,并且仍然可以通过itemid来引用组件。通过itemid有许多方法来获取组件的引用。示例6C列出了部分方法。
xtype : 'toolbar', itemId : ‘picturetoolbar’, items : [{ text : 'Save Picture', itemId : 'savebutton' }] // somewhere else in the code we have another component with an itemId of ‘savebutton’ xtype : 'toolbar', itemId: ‘ordertoolbar’, items : [{ text : ‘Save Order’, itemId: ‘savebutton’ }]
示例6B 好:使用itemid来创建组件
var pictureSaveButton = Ext.ComponentQuery.query('#picturetoolbar > #savebutton')[0]; var orderSaveButton = Ext.ComponentQuery.query('#ordertoolbar > #savebutton')[0]; // assuming we have a reference to the “picturetoolbar” as picToolbar picToolbar.down(‘#savebutton’);
示例6C 好:使用itemid引用组件
有时候会看到代码利用组件位置来引用组件。应该避免出现这样的情况,因为有任何条目被增加、移除或嵌入不同的组件,就会导致错误。示例7A显示了几种常见情况。
var mySaveButton = myToolbar.items.getAt(2); var myWindow = myToolbar.ownerCt;
示例7A 不好:避免基于组件位置来获取组件引用
替代方法是,如示例7B那样使用组件查询或者最佳的up或down方法来返回引用。使用这种技术,代码就很少会因以后的结构或组件位置变化而导致错误。
var mySaveButton = myToolbar.down(‘#savebutton’); // searching against itemId var myWindow = myToolbar.up(‘window’);
示例7B 好:使用组件查询来返回相关引用
Sencha在为组件、属性或xtype等等命名的时候,会遵循某些大写/小写标准。为了避免混淆,保持代码清洁,应对遵循相同的标准。示例8A显示几个不正确的情形。示例8B显示了在相同情况下,使用正确的大写/小写命名约定的情形。
Ext.define(‘MyApp.view.customerlist’,{ // should be capitalized and then camelCase extend : ‘Ext.grid.Panel’, alias : ‘widget.Customerlist’, // should be lowercase MyCustomConfig : ‘xyz’, // should be camelCase 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,面板总是会有“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”区域不应该放在这里
代替方法是,如示例8B那样,在创建组件的时候才指定布局配置。这样,就可以将组件重用到任何你希望的地方,切不受布局配置的约束。
Ext.define('MyApp.view.MyGrid',{ extend : 'Ext.grid.Panel', initComponent : function(){ Ext.apply(this,{ store : ‘MyStore’, ...... }); } }); // specify the region when the component is created... Ext.create('MyApp.view.MyGrid',{ region : 'center' });
示例9B 好:在创建组件的时候才知道区域
如示例9C所示,也可以为组件提供一个默认的区域,如果需要,可以重写它。
Ext.define('MyApp.view.MyGrid',{ extend : 'Ext.grid.Panel', region : 'center', // default region initComponent : function(){ Ext.apply(this,{ store : ‘MyStore’, ...... }); } }); Ext.create(‘MyApp.view.MyGrid’,{ region : ‘north’, // overridden region height : 400 });
示例9C 也很好:指定默认区域,在需要的时候重写
有很多时候,我们会看到比所需更复杂的代码。这通常是由于对每个组件的可用方法不熟悉造成的。最常见的一种情况是,为每一个表单字段单独从数据记录中加载数据。示例10A就显示这种情况。
// suppose the following fields exist within a form items : [{ fieldLabel : ‘User’, itemId : ‘username’ },{ fieldLabel : ‘Email’, itemId : ‘email’ },{ fieldLabel : ‘Home Address’, itemId : ‘address’ }]; // you could load the values from a record into each form field individually myForm.down(‘#username’).setValue(record.get(‘UserName’)); myForm.down(‘#email’).setValue(record.get(‘Email’)); myForm.down(‘#address’).setValue(record.get(‘Address’));
示例10A 不好:为表单字段单独的从记录中加载数据
替代单独加载每一个值的方法是,使用loadRecord方法从记录中为所有字段的数据到表单字段,这只需要一行代码。如示例10B所示,这里的关键是确保表单字段的name属性和记录的字段名称是一样的。
items : [{ fieldLabel : ‘User’, name : ‘UserName’ },{ fieldLabel : ‘Email’, name : ‘Email’ },{ fieldLabel : ‘Home Address’, name : ‘Address’ }]; myForm.loadRecord(record);
示例10B 好:使用loadRecord方法只需要一行代码就可加载所有表单字段
这只是比必需的更复杂的代码的其中一个例子。而当中的重点是要审视组件的所有方法和和示例,以确保正在使用简单和适当的技术。
CNX公司是Sencha认证选择合作伙伴。Sencha合作伙伴网络是Sencha专业服务器团队的宝贵扩展。
自从1996年以来,CNX一直处于自定义商业应用程序开发的先行者。在2008年,CNX在Ext JS上对它的基于浏览器的用户界面开发进行了标准化,在2010年,又添加了Sencha Touch作为移动开发的标准。我们已经在世界各地,为教育、金融、食品、法律、物流、制造、出版和零售等许多行业的客户创建了一流的Web应用程序。我们的开发团队以芝加哥市中心的公司办公室为基地,可以处理任何规模的项目。CNX可以独立工作或与您的团队一起,以快速、经济高效的方式去实现项目目标。请查阅我们的网站http://www.cnxcorp.com。