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

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

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

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

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 已经是一个 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 : '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. 好一些: 菜单只会在表格初始化时创建一次并且每次使用的都是同一个菜单对象.

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. 最好: 当表格销毁时,菜单也会跟着销毁.

庞大的控制器代码

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

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

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


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

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

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

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

 
MyApp.getApplication().on({
    myevent : doSomething
});

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

注意:在 ExtJS 4.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   // initialize to 0
},
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” 所定义,导致了一个命名冲突。尽管从下面的代码看这个错误是非常明显的,但在大型的项目开发中将很难发现这命名冲突的问题。

// 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 一样简单地替换 ‘id’ 属性为 ‘itemId’。这样就解决命名冲突的问题,并且我们仍然可以通过 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’)[1];

var orderSaveButton = Ext.ComponentQuery.query(‘#ordertoolbar > #savebutton’)[1];

// 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. 差: 应该避免基于组件所处位置获取组件对象引用这种方式.

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

var mySaveButton = myToolbar.down(‘#savebutton’);    // searching against itemId

var myWindow = myToolbar.up(‘window’);

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

不恰当的命名方式

在为组件设置名字、属性、类型等时,应该遵循sencha的大小写标准。这样,能够避免给以后维护者造成理解困难和使你能保持干净整洁的代码,你应该遵循统一的命名标准。示例 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 中,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’,
……
});
}
});

// specify the region when the component is created…
Ext.create(‘MyApp.view.MyGrid’,{
region : ‘center’
});

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

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

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. 也好: 在类初始化时提供一个默认值并可以在需要时覆盖掉这个默认值.

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

我们已经看过很多次在某些项目中存在不必要的但又很复杂的代码。这通常会导致每一个组件中有用的函数变得很啰嗦。一种通常出现的情况是,在加载一个数据集 store 时采用将数据集中的每一个选项都加载一次。示例 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 方法,仅仅一行代码就实现了来从数据集 record 中加载所有数据项的功能。只需要确保表格中的 “name” 属性和数据集 record 中的数据项一致即可。正如示例 10B 所示一样。

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

myForm.loadRecord(record);


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

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

CNX

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

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

原文信息

Top 10 Ext JS Development Practices to Avoid

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

译者的感想

开发近半年的 ExtJS 应用程序,确实出现了文中所提到的 全局变量、强制设置组件id 等这样的问题。计划慢慢地整理下代码,同时在编写新功能时尽量少出现这些不规范的代码。这篇总结得很精辟,很实用。同时也觉得,借鉴前辈的经验,能让自己在编程方面成长得更快更好。

标签