ExtJS 4应用架构设计 (一)

在《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

[javascript] view plaincopy
  1. Ext.application({  
  2.     name: 'Panda',      
  3.     autoCreateViewport: true,  
  4.     launch: function() {  
  5.         // This is fired as soon as the page is ready  
  6.     }  
  7. });  

        配置项name将会创建一个命名空间。所有视图、模型和Store和控制器都会以该命名空间为命名。设置autoCreateViewport为true,框架将字段加载app/view/Viewport.js文件。在该文件内,将会定义一个名称为Panda.view.Viewport的类,类名必须使用应用中name指定的命名空间。


        The Viewport class

        在UI中的视图,都是独立的部件,因而需要使用Viewport将它们粘合起来。它会加载要求的视图及它们的定义需要的配置项,以实现应用的整体布局。我们发现,通过定义视图并将它们加载到viewprot,是创建UI基本结构最快的方法。

        重要的是,这个过程的重点在搭建视图,而不是单个视图本身,就如雕刻一样,先开始创建视图的粗糙形状,然后细化它们。

        创建构造块

        利用前文中的工作,我们现在可以开始定义视图了。


app/view/NewStation.js

[javascript] view plaincopy
  1. Ext.define('Panda.view.NewStation', {  
  2.     extend: 'Ext.form.field.ComboBox',  
  3.     alias: 'widget.newstation',  
  4.     store: 'SearchResults',  
  5.     ... more configuration ...  
  6. });  

app/view/SongControls.js
[javascript] view plaincopy
  1. Ext.define('Panda.view.SongControls', {  
  2.     extend: 'Ext.Container',  
  3.     alias: 'widget.songcontrols',  
  4.     ... more configuration ...  
  5. });  

app/view/StationsList
[javascript] view plaincopy
  1. Ext.define('Panda.view.StationsList', {  
  2.     extend: 'Ext.grid.Panel',  
  3.     alias: 'widget.stationslist',  
  4.     store: 'Stations',  
  5.     ... more configuration ...  
  6. });  

app/view/RecentlyPlayedScroller.js
[javascript] view plaincopy
  1. Ext.define('Panda.view.RecentlyPlayedScroller', {  
  2.     extend: 'Ext.view.View',  
  3.     alias: 'widget.recentlyplayedscroller',  
  4.     itemTpl: '<div></div>',  
  5.     store: 'RecentSongs',  
  6.     ... more configuration ...  
  7. });  

app/view/SongInfo.js
[javascript] view plaincopy
  1. Ext.define('Panda.view.SongInfo', {  
  2.     extend: 'Ext.panel.Panel',  
  3.     alias: 'widget.songinfo',      
  4.     tpl: '<h1>About </h1><p></p>',  
  5.     ... more configuration ...  
  6. });  

        我们只是列了一些定义,组件的具体配置将不在本文进行讨论。

        在上述配置,可看到使用了三个Store,这些Store的名称都是上一篇文章定义的。现在,我们开始创建Store。


        模型和Store

        通常,在初始阶段,以包含数据的静的json文件作为服务器端是相当有用。以后,这些静态文件可以作为实际的服务器端动态文件的参考。

        在当前应用,要使用Station和Song两个模型,还需要定义使用这两个模型并绑定到数据组件的Store。每个Store都会从服务器端加载数据,模拟的数据文件格式如下:

        静态数据

data/songs.json

[javascript] view plaincopy
  1. {  
  2.     'success'true,  
  3.     'results': [  
  4.         {  
  5.             'name''Blues At Sunrise (Live)',   
  6.             'artist''Stevie Ray Vaughan',   
  7.             'album''Blues At Sunrise',   
  8.             'description''Description for Stevie',   
  9.             'played_date''1',  
  10.             'station': 1  
  11.         },  
  12.         ...  
  13.     ]  
  14. }  

data/stations.json
[javascript] view plaincopy
  1. {  
  2.     'success'true,  
  3.     'results': [  
  4.         {'id': 1, 'played_date': 4, 'name''Led Zeppelin'},   
  5.         {'id': 2, 'played_date': 3, 'name''The Rolling Stones'},   
  6.         {'id': 3, 'played_date': 2, 'name''Daft Punk'}  
  7.     ]  
  8. }  

data/searchresults.json
[javascript] view plaincopy
  1. {  
  2.     'success'true,      
  3.     'results': [  
  4.         {'id': 1, 'name''Led Zeppelin'},   
  5.         {'id': 2, 'name''The Rolling Stones'},   
  6.         {'id': 3, 'name''Daft Punk'},  
  7.         {'id': 4, 'name''John Mayer'},   
  8.         {'id': 5, 'name''Pete Philly & Perquisite'},   
  9.         {'id': 6, 'name''Black Star'},  
  10.         {'id': 7, 'name''Macy Gray'}  
  11.     ]  
  12. }  

        模型

         ExtJS 4中的模型类似ExtJS 3中的记录,它们主要的区别是,可以在模型上定义代理、验证和关联。应用中的模型Song代码如下:

app/model/Song.js

[javascript] view plaincopy
  1. Ext.define('Panda.model.Song', {  
  2.     extend: 'Ext.data.Model',  
  3.     fields: ['id''name''artist''album''played_date''station'],  
  4.    
  5.     proxy: {  
  6.         type: 'ajax',  
  7.         url: 'data/recentsongs.json',  
  8.         reader: {  
  9.             type: 'json',  
  10.             root: 'results'  
  11.         }  
  12.     }  
  13. });  

        可以看到,在模型中定义了代理,这是一个好的方式,这样,就可以直接在模型加载和保存模型实例,而不需要经过Store。而且,当有多个Store使用该模型的时候,也不需要重新为每个Store定义一次代理。

        下面继续定义Station 模型:

app/model/Station.js

[javascript] view plaincopy
  1. Ext.define('Panda.model.Station', {  
  2.     extend: 'Ext.data.Model',  
  3.     fields: ['id''name''played_date'],  
  4.    
  5.     proxy: {  
  6.         type: 'ajax',  
  7.         url: 'data/stations.json',  
  8.         reader: {  
  9.             type: 'json',  
  10.             root: 'results'  
  11.         }  
  12.     }  
  13. });  

        Store

        在ExtJS 4,多个Store可以使用相同的数据模型,即使Store需要从不同的来源加载数据。在实例中,模型Station将会在SearchResults和Stations这两个Store中使用,而它们会从不同的位置加载数据。其中,SearchResults将返回返回搜索结果,而另一个则返回用户收藏的Station。为了实现这一点,其中一个Store需要重写模型中定义的代

理。

app/store/SearchResults.js

[javascript] view plaincopy
  1. Ext.define('Panda.store.SearchResults', {  
  2.     extend: 'Ext.data.Store',  
  3.     requires: 'Panda.model.Station',  
  4.     model: 'Panda.model.Station',  
  5.    
  6.     // Overriding the model's default proxy  
  7.     proxy: {  
  8.         type: 'ajax',  
  9.         url: 'data/searchresults.json',  
  10.         reader: {  
  11.             type: 'json',  
  12.             root: 'results'  
  13.         }  
  14.     }  
  15. });  

app/store/Stations.js
[javascript] view plaincopy
  1. Ext.define('Panda.store.Stations', {  
  2.     extend: 'Ext.data.Store',  
  3.     requires: 'Panda.model.Station',  
  4.     model: 'Panda.model.Station'  
  5. });  

         在SearchResults的定义,已经重写了Station模型中代理的定义,当调用Store的load方法时,其代理将调用Store的的代理以替换模型中定义的代理。

         当然,也可以在服务器端使用相同的接口实现返回搜索结果和用户收藏的station,这样,两个Store就可以使用模型中定义的默认代理了,只是加载数据时请求的参数不同。

         接着创建RecentSongs:

app/store/RecentSongs.js

[javascript] view plaincopy
  1. Ext.define('Panda.store.RecentSongs', {  
  2.     extend: 'Ext.data.Store',  
  3.     model: 'Panda.model.Song',  
  4.    
  5.     // Make sure to require your model if you are  
  6.     // not using Ext JS 4.0.5  
  7.     requires: 'Panda.model.Song'  
  8. });  

        要注意,在当前版本的ExtJS中,在Store中的model属性还不能自动创建一个依赖,因而需要通过requires配置项指定模型以便能够实现动态加载模型。

        另外,根据约定,Store的类名要使用复数,而模型的类名要使用单数。

        增加Store和模型到应用

        定义好模型和Store后,现在可以把它们加到应用里。打开Application.js并添加如下代码:

app/Application.js

[javascript] view plaincopy
  1. Ext.application({  
  2.     ...  
  3.     models: ['Station''Song'],      
  4.     stores: ['Stations''RecentSongs''SearchResults']  
  5.     ...  
  6. });  

         使用ExtJS 4 MVC包的另外一个好处是,应用会自动加载在stores和models中定义的Store和模型。当Store加载后,会为每个Store创建一个实例,并将其名称作为storeId。这样,就可以使用该名称将其绑定到视图中数据组件,例如“SearchResults”。

        应用粘合剂

        在开始的时候,可以将视图逐一加到Viewport,这样比较容易调试出视图错误的配置。先在Panda应用中构建Viewport:

[javascript] view plaincopy
  1. Ext.define('Panda.view.Viewport', {  
  2.     extend: 'Ext.container.Viewport',  

         通常,应用的Viewport类扩展自Ext.container.Viewport,这会让应用把浏览器的可用空间作为应用的空间。
[javascript] view plaincopy
  1. requires: [  
  2.     'Panda.view.NewStation',  
  3.     'Panda.view.SongControls',  
  4.     'Panda.view.StationsList',  
  5.     'Panda.view.RecentlyPlayedScroller',  
  6.     'Panda.view.SongInfo'  
  7. ],  

        这里要设置viewprot依赖的视图, 这样,就可以使用视图中通过alias属性定义的名称作为xtype配置项的值来定义视图。
[javascript] view plaincopy
  1. layout: 'fit',  
  2.   
  3. initComponent: function() {  
  4.     this.items = {  
  5.         xtype: 'panel',  
  6.         dockedItems: [{  
  7.             dock: 'top',  
  8.             xtype: 'toolbar',  
  9.             height: 80,  
  10.             items: [{  
  11.                 xtype: 'newstation',  
  12.                 width: 150  
  13.             }, {  
  14.                 xtype: 'songcontrols',  
  15.                 height: 70,  
  16.                 flex: 1  
  17.             }, {  
  18.                 xtype: 'component',  
  19.                 html: 'Panda<br>Internet Radio'  
  20.             }]  
  21.         }],  
  22.         layout: {  
  23.             type: 'hbox',  
  24.             align: 'stretch'  
  25.         },  
  26.         items: [{  
  27.             width: 250,  
  28.             xtype: 'panel',  
  29.             layout: {  
  30.                 type: 'vbox',  
  31.                 align: 'stretch'  
  32.             },  
  33.             items: [{  
  34.                 xtype: 'stationslist',  
  35.                 flex: 1  
  36.             }, {  
  37.                 html: 'Ad',  
  38.                 height: 250,  
  39.                 xtype: 'panel'  
  40.             }]  
  41.         }, {  
  42.             xtype: 'container',  
  43.             flex: 1,  
  44.             layout: {  
  45.                 type: 'vbox',  
  46.                 align: 'stretch'  
  47.             },  
  48.             items: [{  
  49.                 xtype: 'recentlyplayedscroller',  
  50.                 height: 250  
  51.             }, {  
  52.                 xtype: 'songinfo',  
  53.                 flex: 1  
  54.             }]  
  55.         }]  
  56.     };  
  57.   
  58.     this.callParent();  
  59. }  

         因为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

[javascript] view plaincopy
  1. Ext.define('Panda.controller.Station', {  
  2.     extend: 'Ext.app.Controller',  
  3.     init: function() {  
  4.         ...  
  5.     },  
  6.     ...  
  7. });  

app/controller/Song.js
[javascript] view plaincopy
  1. Ext.define('Panda.controller.Song', {  
  2.     extend: 'Ext.app.Controller',  
  3.     init: function() {  
  4.         ...  
  5.     },  
  6.     ...  
  7. });  

        在应用中包含控制器后,框架会在调用init方法的时候字段加载控制器。在init方法内,可以定义监听视图和应用的事件。在大型应用,有时需要在运行时加载额外的控制器,可以使用getController方法加载:

[javascript] view plaincopy
  1. someAction: function() {  
  2.     var controller = this.getController('AnotherController');  
  3.    
  4.     // Remember to call the init method manually  
  5.     controller.init();  
  6. }  

         当在运行时加载额外的控制器时,一定要记得手动调用加载的控制器的init方法。

         在实例应用,只要将控制器加到controllers配置项中的数组,让框架加载和初始化控制器就可以了。

app/Application.js

[javascript] view plaincopy
  1. Ext.application({  
  2.     ...  
  3.     controllers: ['Station''Song']  
  4. });  

         定义监听

         现在要在控制器的init方法内调用其control方法控制UI部件:

app/controller/Station.js

[javascript] view plaincopy
  1. ...  
  2. <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>  
  3.     <span class="kw1">this</span>.<span class="me1">control</span><span class="br0">(</span><span class="br0">{</span>  
  4.         <span class="st0">'stationslist'</span><span class="sy0">:</span> <span class="br0">{</span>  
  5.             selectionchange<span class="sy0">:</span> <span class="kw1">this</span>.<span class="me1">onStationSelect</span>  
  6.         <span class="br0">}</span><span class="sy0">,</span>  
  7.         <span class="st0">'newstation'</span><span class="sy0">:</span> <span class="br0">{</span>  
  8.             select<span class="sy0">:</span> <span class="kw1">this</span>.<span class="me1">onNewStationSelect</span>  
  9.         <span class="br0">}</span>  
  10.     <span class="br0">}</span><span class="br0">)</span><span class="sy0">;</span>  
  11. <span class="br0">}</span>  
  12. ...  

        方法control会通过对象的关键字查询组件。在示例中,会通过视图的xtypes配置项查询组件。然而,使用组件查询,必须确保能指向UI中的知道部件。想了解更多有关组件查询的信息,可访问这里

        每一个查询都会绑定一个监听配置对象,在每个监听配置对象内,事件名就是监听对象的关键字,而事件是由查询的目标组件提供的。在示例中,将使用Grid(在StationsList视图扩展出的)提供的selectionchange事件,及ComboBox(从NewStation视图中扩展出的)提供的select事件。要找到特定组件有那些事件,可以在API文档中查看组件的事件部分。


        监听配置对象中的值是事件触发后执行的函数。函数的作用将是控制器本身。

        现在为Song控制器添加一些监听:

app/controller/Song.js

[javascript] view plaincopy
  1. ...  
  2. init: function() {  
  3.     this.control({  
  4.         'recentlyplayedscroller': {  
  5.             selectionchange: this.onSongSelect  
  6.         }  
  7.     });  
  8.    
  9.     this.application.on({  
  10.         stationstart: this.onStationStart,  
  11.         scope: this  
  12.     });  
  13. }  
  14. ..  

        除了监听RecentlyPlayedScroller视图的selectionchange事件外,这里还需要监听应用事件。这需要使用application实例的on方法。每一个控制都可以使用this.application直接访问application实例。

        当在应用中有许多控制器都需要监听同一个事件的时候,应用事件这时候非常有用。与在每一个控制器中监听相同的视图事件一样,只需要在一个控制器中监听视图的事件,就可以触发一个应用范围的事件,这样,等于其它控制器也监听了该事件。这样,就实现了控制器之间进行通信,而无需了解或依赖于对方是否存在。

        控制器Song需要知道新的station是否已开始,以便更新song滚动条和song信息。

        现在在Station控制器内,编写当应用事件stationstart触发时的响应代码:

app/controller/Station.js

[javascript] view plaincopy
  1. ...  
  2. onStationSelect: function(selModel, selection) {  
  3.     this.application.fireEvent('stationstart', selection[0]);  
  4. }  
  5. ...  

        这里只是简单在触发stationstart事件时,将selectionchange事件中的第一个选择项作为唯一参数传递给事件。

        小结

        在这篇文章中,可以了解到应用架构的基本技术。当然,这只是其中的一小部分,在该系列的下一部分,将可以看到一些更高级的控制器技术,并继续在控制器中实现操作,以及在视图中添加细节,以完成Panda应用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章