原文: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
- ...
- <SPAN class=me1>init</SPAN><SPAN class=sy0>:</SPAN> <SPAN class=kw2>function</SPAN><SPAN class=br0>(</SPAN><SPAN class=br0>)</SPAN> <SPAN class=br0>{</SPAN>
- <SPAN class=kw1>this</SPAN>.<SPAN class=me1>control</SPAN><SPAN class=br0>(</SPAN><SPAN class=br0>{</SPAN>
- <SPAN class=st0>'stationslist'</SPAN><SPAN class=sy0>:</SPAN> <SPAN class=br0>{</SPAN>
- selectionchange<SPAN class=sy0>:</SPAN> <SPAN class=kw1>this</SPAN>.<SPAN class=me1>onStationSelect</SPAN>
- <SPAN class=br0>}</SPAN><SPAN class=sy0>,</SPAN>
- <SPAN class=st0>'newstation'</SPAN><SPAN class=sy0>:</SPAN> <SPAN class=br0>{</SPAN>
- select<SPAN class=sy0>:</SPAN> <SPAN class=kw1>this</SPAN>.<SPAN class=me1>onNewStationSelect</SPAN>
- <SPAN class=br0>}</SPAN>
- <SPAN class=br0>}</SPAN><SPAN class=br0>)</SPAN><SPAN class=sy0>;</SPAN>
- <SPAN class=br0>}</SPAN>
- ...
方法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应用。
作者: Tommy Maintz
Tommy Maintz is the original lead of Sencha Touch. With extensive knowledge of Object Oriented JavaScript and mobile browser idiosyncracies, he pushes the boundaries of what is possible within mobile browsers. Tommy brings a unique view point and an ambitious philosophy to creating engaging user interfaces. His attention to detail drives his desire to make the perfect framework for developers to enjoy.