原文:http://www.sencha.com/learn/architecting-your-app-in-ext-js-4-part-2/
在《ExtJS应用架构设计》一文,我们探讨了如何使用ExtJS构建一个潘多拉风格的应用程序。我们采用了MVC架构,并将它应用到一个比较复杂的用户界面,应用中带有多个视图和模型。在这篇文章中,我们将在架构的基础上继续探讨控制和模型的设计与代码问题,并开始使用Ext.application和Viewprot类。
现在,让我们开始编写应用。
定义应用
在ExtJS 3,Ext.onReady方法是应用程序和开发人员开始编写应用架构的入口。在ExtJS 4,我们推出了类似MVC模式,该模式可以让你在创建应用程序时遵循最佳做法。
新的MVC包要求使用Ext.application方法作为入口,该方法将创建一个Ext.app.Application实例,并在页面准备好以后触发launch方法。它取代了在Ext.onReady内添加诸如创建Viewport和设置命名空间等功能的这种写法。
app/Application.js
Ext.application({
name: 'Panda',
autoCreateViewport: true,
launch: function() {
// This is fired as soon as the page is ready
}
});
配置项name将会创建一个命名空间。所有视图、模型和Store和控制器都会以该命名空间为命名。设置autoCreateViewport为true,框架将字段加载app/view/Viewport.js文件。在该文件内,将会定义一个名称为Panda.view.Viewport的类,类名必须使用应用中name指定的命名空间。
The Viewport class
在UI中的视图,都是独立的部件,因而需要使用Viewport将它们粘合起来。它会加载要求的视图及它们的定义需要的配置项,以实现应用的整体布局。我们发现,通过定义视图并将它们加载到viewprot,是创建UI基本结构最快的方法。
重要的是,这个过程的重点在搭建视图,而不是单个视图本身,就如雕刻一样,先开始创建视图的粗糙形状,然后细化它们。
创建构造块
利用前文中的工作,我们现在可以开始定义视图了。
app/view/NewStation.js
Ext.define('Panda.view.NewStation', {
extend: 'Ext.form.field.ComboBox',
alias: 'widget.newstation',
store: 'SearchResults',
... more configuration ...
});
app/view/SongControls.js
Ext.define('Panda.view.SongControls', {
extend: 'Ext.Container',
alias: 'widget.songcontrols',
... more configuration ...
});
app/view/StationsList
Ext.define('Panda.view.StationsList', {
extend: 'Ext.grid.Panel',
alias: 'widget.stationslist',
store: 'Stations',
... more configuration ...
});
app/view/RecentlyPlayedScroller.js
Ext.define('Panda.view.RecentlyPlayedScroller', {
extend: 'Ext.view.View',
alias: 'widget.recentlyplayedscroller',
itemTpl: '<div></div>',
store: 'RecentSongs',
... more configuration ...
});
app/view/SongInfo.js
Ext.define('Panda.view.SongInfo', {
extend: 'Ext.panel.Panel',
alias: 'widget.songinfo',
tpl: '<h1>About </h1><p></p>',
... more configuration ...
});
我们只是列了一些定义,组件的具体配置将不在本文进行讨论。
在上述配置,可看到使用了三个Store,这些Store的名称都是上一篇文章定义的。现在,我们开始创建Store。
模型和Store
通常,在初始阶段,以包含数据的静的json文件作为服务器端是相当有用。以后,这些静态文件可以作为实际的服务器端动态文件的参考。
在当前应用,要使用Station和Song两个模型,还需要定义使用这两个模型并绑定到数据组件的Store。每个Store都会从服务器端加载数据,模拟的数据文件格式如下:
静态数据
data/songs.json
{
'success': true,
'results': [
{
'name': 'Blues At Sunrise (Live)',
'artist': 'Stevie Ray Vaughan',
'album': 'Blues At Sunrise',
'description': 'Description for Stevie',
'played_date': '1',
'station': 1
},
...
]
}
data/stations.json
{
'success': true,
'results': [
{'id': 1, 'played_date': 4, 'name': 'Led Zeppelin'},
{'id': 2, 'played_date': 3, 'name': 'The Rolling Stones'},
{'id': 3, 'played_date': 2, 'name': 'Daft Punk'}
]
}
data/searchresults.json
{
'success': true,
'results': [
{'id': 1, 'name': 'Led Zeppelin'},
{'id': 2, 'name': 'The Rolling Stones'},
{'id': 3, 'name': 'Daft Punk'},
{'id': 4, 'name': 'John Mayer'},
{'id': 5, 'name': 'Pete Philly & Perquisite'},
{'id': 6, 'name': 'Black Star'},
{'id': 7, 'name': 'Macy Gray'}
]
}
模型
ExtJS 4中的模型类似ExtJS 3中的记录,它们主要的区别是,可以在模型上定义代理、验证和关联。应用中的模型Song代码如下:
app/model/Song.js
Ext.define('Panda.model.Song', {
extend: 'Ext.data.Model',
fields: ['id', 'name', 'artist', 'album', 'played_date', 'station'],
proxy: {
type: 'ajax',
url: 'data/recentsongs.json',
reader: {
type: 'json',
root: 'results'
}
}
});
可以看到,在模型中定义了代理,这是一个好的方式,这样,就可以直接在模型加载和保存模型实例,而不需要经过Store。而且,当有多个Store使用该模型的时候,也不需要重新为每个Store定义一次代理。
下面继续定义Station 模型:
app/model/Station.js
Ext.define('Panda.model.Station', {
extend: 'Ext.data.Model',
fields: ['id', 'name', 'played_date'],
proxy: {
type: 'ajax',
url: 'data/stations.json',
reader: {
type: 'json',
root: 'results'
}
}
});
Store
在ExtJS 4,多个Store可以使用相同的数据模型,即使Store需要从不同的来源加载数据。在实例中,模型Station将会在SearchResults和Stations这两个Store中使用,而它们会从不同的位置加载数据。其中,SearchResults将返回返回搜索结果,而另一个则返回用户收藏的Station。为了实现这一点,其中一个Store需要重写模型中定义的代
理。
app/store/SearchResults.js
Ext.define('Panda.store.SearchResults', {
extend: 'Ext.data.Store',
requires: 'Panda.model.Station',
model: 'Panda.model.Station',
// Overriding the model's default proxy
proxy: {
type: 'ajax',
url: 'data/searchresults.json',
reader: {
type: 'json',
root: 'results'
}
}
});
app/store/Stations.js
Ext.define('Panda.store.Stations', {
extend: 'Ext.data.Store',
requires: 'Panda.model.Station',
model: 'Panda.model.Station'
});
在SearchResults的定义,已经重写了Station模型中代理的定义,当调用Store的load方法时,其代理将调用Store的的代理以替换模型中定义的代理。
当然,也可以在服务器端使用相同的接口实现返回搜索结果和用户收藏的station,这样,两个Store就可以使用模型中定义的默认代理了,只是加载数据时请求的参数不同。
接着创建RecentSongs:
app/store/RecentSongs.js
Ext.define('Panda.store.RecentSongs', {
extend: 'Ext.data.Store',
model: 'Panda.model.Song',
// Make sure to require your model if you are
// not using Ext JS 4.0.5
requires: 'Panda.model.Song'
});
要注意,在当前版本的ExtJS中,在Store中的model属性还不能自动创建一个依赖,因而需要通过requires配置项指定模型以便能够实现动态加载模型。
另外,根据约定,Store的类名要使用复数,而模型的类名要使用单数。
增加Store和模型到应用
定义好模型和Store后,现在可以把它们加到应用里。打开Application.js并添加如下代码:
app/Application.js
Ext.application({
...
models: ['Station', 'Song'],
stores: ['Stations', 'RecentSongs', 'SearchResults']
...
});
使用ExtJS 4 MVC包的另外一个好处是,应用会自动加载在stores和models中定义的Store和模型。当Store加载后,会为每个Store创建一个实例,并将其名称作为storeId。这样,就可以使用该名称将其绑定到视图中数据组件,例如“SearchResults”。
应用粘合剂
在开始的时候,可以将视图逐一加到Viewport,这样比较容易调试出视图错误的配置。先在Panda应用中构建Viewport:
Ext.define('Panda.view.Viewport', {
extend: 'Ext.container.Viewport',
通常,应用的Viewport类扩展自Ext.container.Viewport,这会让应用把浏览器的可用空间作为应用的空间。
requires: [
'Panda.view.NewStation',
'Panda.view.SongControls',
'Panda.view.StationsList',
'Panda.view.RecentlyPlayedScroller',
'Panda.view.SongInfo'
],
这里要设置viewprot依赖的视图, 这样,就可以使用视图中通过alias属性定义的名称作为xtype配置项的值来定义视图。
layout: 'fit',
initComponent: function() {
this.items = {
xtype: 'panel',
dockedItems: [{
dock: 'top',
xtype: 'toolbar',
height: 80,
items: [{
xtype: 'newstation',
width: 150
}, {
xtype: 'songcontrols',
height: 70,
flex: 1
}, {
xtype: 'component',
html: 'Panda<br>Internet Radio'
}]
}],
layout: {
type: 'hbox',
align: 'stretch'
},
items: [{
width: 250,
xtype: 'panel',
layout: {
type: 'vbox',
align: 'stretch'
},
items: [{
xtype: 'stationslist',
flex: 1
}, {
html: 'Ad',
height: 250,
xtype: 'panel'
}]
}, {
xtype: 'container',
flex: 1,
layout: {
type: 'vbox',
align: 'stretch'
},
items: [{
xtype: 'recentlyplayedscroller',
height: 250
}, {
xtype: 'songinfo',
flex: 1
}]
}]
};
this.callParent();
}
});
因为Viewport扩展自Container,而Container没有停靠项,因而必须添加一个面板作为viewport的第一个子组件,并让使用fit布局让面板与viewprot有相同的大小。
在架构方面,需要特别注意的是,不要在实际视图中定义有具体配置的布局,例如,不要定义flex、width或height等配置项,这样,就可以很容易的调整其在应用程序中某个位置的整体布局,从而增加架构的可维护性和灵活性。
应用逻辑
在ExtJS 3,通常会使用按钮的句柄、绑定监听时间到子组件或扩展时重写其方法以实现应用逻辑。然后,就像不应该在HTML标记中使用内联CSS样式一样,不应该在视图中定义应用逻辑。在ExtJS 4,通过MVC包提供的控制器,可响应视图或其它控制器中的监听事件,并执行这些事件对应的应用逻辑。这样的设计有及格好处。
第一个好处是,应用逻辑不用绑定到视图实例,这意味着在需要时,可以在视图实例化或销毁时,应用逻辑可以继续处理其它事情,如同步数据。
另外,在ExtJS 3,会有许多嵌套视图,而每一个都会添加一层应用逻辑。当将应用逻辑移动到控制器,将它们集中起来,这样就易于维护和修改。
最后,控制器基类会提供一些功能,让控制器易于执行应用逻辑。
创建控制器
现在,已经有了UI的基本架构、模型和Store的配置,是时候为应用定义控制器了。我们计划定义两个控制器,Station和Song,定义如下:
app/controller/Station.js
Ext.define('Panda.controller.Station', {
extend: 'Ext.app.Controller',
init: function() {
...
},
...
});
app/controller/Song.js
Ext.define('Panda.controller.Song', {
extend: 'Ext.app.Controller',
init: function() {
...
},
...
});
在应用中包含控制器后,框架会在调用init方法的时候字段加载控制器。在init方法内,可以定义监听视图和应用的事件。在大型应用,有时需要在运行时加载额外的控制器,可以使用getController方法加载:
someAction: function() {
var controller = this.getController('AnotherController');
// Remember to call the init method manually
controller.init();
}
当在运行时加载额外的控制器时,一定要记得手动调用加载的控制器的init方法。
在实例应用,只要将控制器加到controllers配置项中的数组,让框架加载和初始化控制器就可以了。
app/Application.js
Ext.application({
...
controllers: ['Station', 'Song']
});
定义监听
现在要在控制器的init方法内调用其control方法控制UI部件:
app/controller/Station.js
...
init: function() {
this.control({
'stationslist': {
selectionchange: this.onStationSelect
},
'newstation': {
select: this.onNewStationSelect
}
});
}
...
方法control会通过对象的关键字查询组件。在示例中,会通过视图的xtypes配置项查询组件。然而,使用组件查询,必须确保能指向UI中的知道部件。想了解更多有关组件查询的信息,可访问这里。
每一个查询都会绑定一个监听配置对象,在每个监听配置对象内,事件名就是监听对象的关键字,而事件是由查询的目标组件提供的。在示例中,将使用Grid(在StationsList视图扩展出的)提供的selectionchange事件,及ComboBox(从NewStation视图中扩展出的)提供的select事件。要找到特定组件有那些事件,可以在API文档中查看组件的事件部分。
监听配置对象中的值是事件触发后执行的函数。函数的作用将是控制器本身。
现在为Song控制器添加一些监听:
app/controller/Song.js
...
init: function() {
this.control({
'recentlyplayedscroller': {
selectionchange: this.onSongSelect
}
});
this.application.on({
stationstart: this.onStationStart,
scope: this
});
}
..
除了监听RecentlyPlayedScroller视图的selectionchange事件外,这里还需要监听应用事件。这需要使用application实例的on方法。每一个控制都可以使用this.application直接访问application实例。
当在应用中有许多控制器都需要监听同一个事件的时候,应用事件这时候非常有用。与在每一个控制器中监听相同的视图事件一样,只需要在一个控制器中监听视图的事件,就可以触发一个应用范围的事件,这样,等于其它控制器也监听了该事件。这样,就实现了控制器之间进行通信,而无需了解或依赖于对方是否存在。
控制器Song需要知道新的station是否已开始,以便更新song滚动条和song信息。
现在在Station控制器内,编写当应用事件stationstart触发时的响应代码:
app/controller/Station.js
...
onStationSelect: function(selModel, selection) {
this.application.fireEvent('stationstart', selection[0]);
}
...
这里只是简单在触发stationstart事件时,将selectionchange事件中的第一个选择项作为唯一参数传递给事件。
小结
在这篇文章中,可以了解到应用架构的基本技术。当然,这只是其中的一小部分,在该系列的下一部分,将可以看到一些更高级的控制器技术,并继续在控制器中实现操作,以及在视图中添加细节,以完成Panda应用。