【翻译】Architecting Your App in Ext JS 4, Part 2
发表日期:2011-08-01 | 作者:Tommy Maintz | 类别:教程 | 难度:中级
这个教程基于Ext JS 4.x版本。
在上一篇中,我们探索了如何用Ext JS设计一个Pandora风格的应用。我们研究了MVC架构,以及如何把它应用到一个相对复杂的有多个视图和模型的UI应用中。这篇文章中,我们将跳出应用设计的视觉部分,转而从Ext.application 和Viewport类开始,探索如何编写控制器和模型。
Defining our application
定义应用
在Ext JS 3中,Ext.onReady方法是你应用程序的入口点,开发者都必须以此开始应用设计。在Ext JS 4中,我们引入了MVC风格的模式。这个模式能帮助你在创建应用时遵循最佳实践。
新的MVC包中的应用入口点需要你使用Ext.application方法。这个方法将创建一个Ext.app.Application实例,并且一旦页面准备好了,就为你触发launch事件。这个本质上取代了当添加新功能时使用Ext.onReady 的需求,比如自动创建一个视口或设置你的命名空间。
app/Application.js
Ext.application({
name: 'Panda',
autoCreateViewport: true,
launch: function() {
// This is fired as soon as the page is ready
}
});
这里的name 属性将创建一个新的命名空间。所有我们的视图、模型、存储和控制器都存在于这个命名空间之中。通过设置autoCreateViewport 属性为true,框架将按约定引入app/view/Viewport.js文件。在这个文件中,必须定义一个名为Panda.view.Viewport的类,以匹配应用配置中的name属性。
The Viewport class
视口类
当我们思考UI中所需的视图时,我们要将注意力集中到那些独立的部分。应用视口就像胶水一样把那些独立部分粘合在一起,它加载所需的视图并且定义所需的配置来完成应用的总体布局。我们逐渐发现,定义你的视图,然后把他们添加到视口是创建你UI结构的最快的方式。
Creating the building blocks
生成构建块
运用上一篇文章中的成果,我们能一次性定义许多视图:
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: '',
store: 'RecentSongs',
... more configuration ...
});
app/view/SongInfo.js
Ext.define('Panda.view.SongInfo', {
extend: 'Ext.panel.Panel',
alias: 'widget.songinfo',
tpl: 'About
',
... more configuration ...
});
我们在这里去除了一些配置信息,因为组件配置不属于此文的讨论范畴。 在上面的配置中,你可能注意到,我们配置了三个存储。这些映射到存储的名字在上一篇文章中已经提及了。让我们将继续存储的创建:
The models and stores
模型和存储
通常,用一些静态的json模拟数据来扮演服务器端很有用。往后,我们能用这些静态文件,作为动态服务器实际实现时的参考。
我们决定过在应用中使用两个模型:Station 和 Song,并且决定了这两种模型绑定数据组件时需要的三种存储。每种存储将从服务器加载它自已的数据。这些模拟数据看起来像下面表示的。
Static data
静态数据
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'}
]
}
Models
模型
Ext JS 4中的模型很像Ext JS 3中的记录(Records)。一个关键的不同是你可以在模型上指定一个代理(proxy)、以及验证(validations)和关联(assocaition)。这个应用的Song模型在Ext JS 4看起来是这样的:
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'
}
}
});
如你所见,我们在模型上定义了代理。这样做通常是一个好的实践,它允许你不需要一个存储就能加载和保存这个模型实例。同样,当多个存储使用同一个模型时,你不须要为它们每个都重新定义你的代理。
现在开始定义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'
}
}
});
Stores
存储
在Ext JS 4中,多个存储能使用同一个数据模型,甚至是这些存储是从不同的数据源来加载它们的数据。在我们的例子中,Station模型将被用于SearchResults 和Stations存储,这两者都从不同的地方加载数据。一个返回搜索结果,另一个返回用户喜爱的电台。为了完成这个,我们的存储其中一个要覆写模型上定义过的代理。
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模型上定义过的代理。当存储的 load 方法调用时,就会使用这个存储的代理,而不是使用模型本身的代理。
注意,你可以在服务器端实现一个API同时检索搜索结果和用户喜爱的电台。这种情形下,存储都使用模型上定义的代理,只是当加载存储时请求传递不同的参数。
最后,我们创建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'
});
还要注意,当前的Ext JS版本中,“模型”在存储上的属性不会自动创建依赖。那就是为什么我们必须指定requires属性,以使模型能够动态加载。 同样,为了方便着想,我们总是会使用存储名称的复数形式。这能突出模型的名称。
Adding the stores and models to our application
添加存储和模型到应用
现在我们已经定义好了模型和存储,该把它们添加到应用中了。再看一下Application.js文件:
app/Application.js
Ext.application({
...
models: ['Station', 'Song'],
stores: ['Stations', 'RecentSongs', 'SearchResults']
...
});
新的Ext JS 4 MVC包中另一个优势是,应用能自动加载定义在stores和model属性中的存储和模型。然后,它将为每一个加载好的存储创建实例,分配一个和它的名称相同的storeId。这让我们在任何时候绑定到数据组件都能使用这个名称,就像我们在视图中做过的,比如:“SearchResults”。
Applying the glue 粘合到一起
粘合到一起
现在,我们有了视图、模型和存储。是时候合并了。先把视图一个接着一个添加到视口中。这让调试错误的视图配置更加容易。
让我们检查Panda应用的视口:
Ext.define('Panda.view.Viewport', {
extend: 'Ext.container.Viewport',
你的视口类通常继承自 Ext.container.Viewport,它使你的应用占据浏览器窗口的所有可用空间。
requires: [
'Panda.view.NewStation',
'Panda.view.SongControls',
'Panda.view.StationsList',
'Panda.view.RecentlyPlayedScroller',
'Panda.view.SongInfo'
],
然后设置视口中所有的视图依赖。这让我们使用它们的xtypes属性,即早先在视图中用alias属性配置过的。
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
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();
}
});
因为视口继承自容器(Container)类,并且容器还没已停靠的项目,我们已经添加了一个面板(Panel)作为视口的单独项。我们通过设置布局属性为“fit”让这个面板的大小和视口具有同样的尺寸。
基于架构的实施考虑,最重要的一点是注意到此时在实际视图中,我们还没有定义一个具体布局配置。通过不在视图中设置诸如 flex、width和width这样的属性,我们能简单地在一个地方调整应用整体布局。这能增强架构的可维护性和灵活性。
Application logic
应用逻辑
在Ext JS 3中,我们经常添加应用逻辑到视图,它们本身使用按钮上的处理函数、绑定监听函数到子组件,还有当继承它们时覆写方法。尽管如此,就像你不应该在HTML标记中写内联CSS样式,从视图定义分离应用逻辑是更可取的做法。在Ext JS 4中,我们在MVC 包中提供了控制器。它们负责监听视图或其他控制器触发的事件,并且在那些事件实现应用逻辑。
这样设计有几点好处:
其一就是是你的应用逻辑不再绑定到视图实例,这意味着当应用逻辑持续处理其他事情的时候,比如同步数据,我们能按需销毁和实例化视图。
此外,在Ext JS 3中,你可能有过许多嵌套的视图,每个都添加了一层应用逻辑。通过把应用逻辑转移到控制器,这样就中心化了,使得应用更加容易维护和改变。
最后,控制器基类提供给你许多功能,让你处理应用逻辑更简单。
Creating our Controllers
创建控制器
现在我们有了UI的基本架构,模型和存储都搭建好了,轮到应用控制了。我们打算用两个控制器: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']
});
Setting up listeners
设置监听器
现在,在控制器init函数内部使用control 方法来控制一部分UI:
app/controller/Station.js
...
init: function() {
this.control({
'stationslist': {
selectionchange: this.onStationSelect
},
'newstation': {
select: this.onNewStationSelect
}
});
}
...
control 方法被传递到一个键为组件查询(component queries)的对象。我们的例子中,组件查询用是的视图中的xtypes属性。尽管如此,使用组件查询,你还是能非常具体地指定UI部分。要了解更多关于组件查询的信息,可以考虑 API 文档。 每个查询都绑定到一个监听器配置。在每个监听器配置内部,我们可以用事件名做键来监听事件。这些可用的事件是你的查询所指向的组件所提供的。在此例中,我们使用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控制器关心的是新电台的启动,因为它需要更新歌曲卷动组件还有歌曲的信息。
app/controller/Station.js
...
onStationSelect: function(selModel, selection) {
this.application.fireEvent('stationstart', selection[0]);
}
...
我们简单的获取由selectionchange 事件提供的单个选中的项目,并且当stationstart 事件触发时把它当作一个单独的参数。
Conclusion
结论
在此文中,我们考虑了设计应用的基本技巧。当然,还有很多没有谈到。这系列的下一篇中,我们将考虑一些更高级的控制器技巧,以及继续通过实现控制器行为和添加更多细节到视图,来编写Panda应用。