ExtJS应用架构设计(三)
原文:http://www.sencha.com/learn/architecting-your-app-in-ext-js-4-part-3/?mkt_tok=3RkMMJWWfF9wsRonuKrLZKXonjHpfsX56uolXaS2lMI%2F0ER3fOvrPUfGjI4AT8t0dvycMRAVFZl5nR9dFOOdfQ%3D%3D
在该系列文章的前两篇文章中(一、二),我们探讨了如何使用ExtJS 4的新特性构建一个潘多拉风格的应用程序,并开始将MVC架构应用到多视图、Store和模型的复杂UI中,而且了解应用架构的基本技术,如通过控制器控制视图、在控制器中通过监听触发应用事件。在本文,将继续完成应用的MVC架构内的控制逻辑。
获取引用
在继续完成应用之前,先重温一些MVC包中的高级功能。在前一篇文章中,介绍了在Ext.application的配置项的stores和models的数组中定义的Store和模型会字段加载,而且加载后,会自动创建Store的实例,并将类名作为storeId的值。
app/Application.js
Ext.application({ ... models: ['Station', 'Song'], stores: ['Stations', 'RecentSongs', 'SearchResults'] ... });
在stores和models的数组中定义这些类,除了会自动加载和实例化这些类外,还会为 它们创建get方法。控制器和视图也是这样的。配置项stores、models、controllers和views也可以在控制器中使用,其功能与在应用实例中使用是一样的。这意味着,为了在Station控制器内获得Stations的引用,需要在其内加入stores数组:
app/controller/Station.js
... stores: ['Stations'], ...
现在在控制内的任何位置都可以使用自动生成的getStationsStore方法返回Stations的引用。这个约定是简单而明确的:
views: ['StationsList'] // creates getter named 'getStationsListView' -> returns reference to StationsList class models: ['Station'] // creates getter named 'getStationModel' -> returns reference to Station model class controllers: ['Song'] // creates getter named 'getSongController' -> returns the Song controller instance stores: ['Stations'] // creates getter named 'getStationsStore' -> returns the Stations store instance
要注意的是,视图和模型返回的是类的引用(要求实例化),而Store和控制器则是实例化后的对象。
引用视图实例
在上一节,讲述了如何定义stores、models、controllers和views配置项以自动创建get方法以返回它们的引用。方法getStationsListView将返回视图类的引用,而在应用流程中,StationsList将选择第一记录,因而,在viewport中,引用的是视图的实例而不是视图类。
在ExtJS 3,通常做法是使用Ext.getCmp方法引用页面中存在的组件实例。在ExtJS 4中,可以继续使用该方法,但不建议这样使用,这是因为使用Ext.getCmp方法需要为每一个组件定义一个唯一ID,才能在应用中引用它。在MVC包中,可以通过在控制器内使用ExtJS 4的新特性ComponentQuery对象获取视图实例的引用:
app/controller/Station.js
... refs: [{ // A component query selector: 'viewport > #west-region > stationslist', ref: 'stationsList' }] ...
在refs配置项,可以定义视图实例的引用,这样,就可以在控制器中返回和操作页面中的组件。要描述引用组件的,需要使用到ComponentQuery配置对象的selector配置项。另外一个必须的配置项是ref,它将会是refs数组内自动产生的get方法名称的一部分,例如,定义为“ref: ‘stationsList’ ”(注意大写L),会在控制内生成getStationsList方法。当然,如果你不想在控制器内定义引用,也可以继续使用Ext.getCmp方法,然而,我们希望你不要这样做,因为它需要在项目中管理组件的唯一ID,随着应用的增长一般都会出现问题。
一定要记住的是,无论视图是否已经存在页面中,都会独立的创建这些get方法。当调用get方法并通过选择符成功获取页面组件后,它会在缓存这些结果以便后续get方法的调用变得迅速。而当通过选择符找不到页面中任何视图的时候,get方法会返回null,这意味着,存在应用逻辑依赖一个视图,而该视图在页面不存在的情况,这时,必须根据应用逻辑进行排查,以确保get方法能返回结果,并正确执行应用逻辑。另外要注意的是,如果通过选择符会返回多个组件,那么,只会返回第一个组件。因此,让选择符只能返回单一的视图是好的编写方式。最后,当引用的组件被销毁的时候,调用get方法也会返回null,除非在页面中找到另外一个与选择符匹配的组件。
在launch方法内级联控制器逻辑
当应用开始运行的时候,如果希望加载用户已经存在的station,可以将逻辑放在应用的onReady方法内。而在MVC架构,提供了onLaunch方法,它会在每一个控制内,当所有控制器、模型和Store被初始化后,初始化视图被渲染前触发。这可让你清晰区分那些是全局应用逻辑,那些是控制器逻辑。
步骤1
app/controller/Station.js
... onLaunch: function() { // Use the automatically generated getter to get the store var stationsStore = this.getStationsStore(); stationsStore.load({ callback: this.onStationsLoad, scope: this }); } ...
Station控制器的onLaunch方法是调用Station的laod方法的一个完美地方。在代码中,Station加载后会执行一个回调函数。
步骤2
app/controller/Station.js
... onStationsLoad: function() { var stationsList = this.getStationsList(); stationsList.getSelectionModel().select(0); } ...
在回调函数中,通过自动产生的StationsList实例的get方法,选择第一个记录,这会触发StationsList内的selectionchange事件。
步骤3
app/controller/Station.js
... init: function() { this.control({ 'stationslist': { selectionchange: this.onStationSelect }, ... }); }, onStationSelect: function(selModel, selection) { this.application.fireEvent('stationstart', selection[0]); }, ...
当在应用中有许多控制器都需要监听同一个事件的时候,应用事件这时候非常有用。与在每一个控制器中监听相同的视图事件一样,只需要在一个控制器中监听视图的事件,就可以触发一个应用范围的事件,这样,等于其它控制器也监听了该事件。这样,就实现了控制器之间进行通信,而无需了解或依赖于对方是否存在。在onStationSelect操作中,将触发应用事件stationstart。
步骤4
app/controller/Song.js
... refs: [{ ref: 'songInfo', selector: 'songinfo' }, { ref: 'recentlyPlayedScroller', selector: 'recentlyplayedscroller' }], stores: ['RecentSongs'], init: function() { ... // We listen for the application-wide stationstart event this.application.on({ stationstart: this.onStationStart, scope: this }); }, onStationStart: function(station) { var store = this.getRecentSongsStore(); store.load({ callback: this.onRecentSongsLoad, params: { station: station.get('id') }, scope: this }); } ...
在Song控制器init方法内,定义了监听应用事件stationstart的方法。当事件触发时,需要加载station的song到RecentSongs。这将会在onStationStart方法内进行处理,通过get方法引用RecentSongs,然后调用其load方法,当加载完成后,需要指向控制器的操作。
步骤5
app/controller/Song.js
... onRecentSongsLoad: function(songs, request) { var store = this.getRecentSongsStore(), selModel = this.getRecentlyPlayedScroller().getSelectionModel(); selModel.select(store.last()); } ...
当Station的Song已加载到RecentSongs,将在RecentlyPlayedScroller中选择最后一首,这将通过调用RecentlyPlayedScroller数据视图的选择模型的select方法完成此操作,参数是RecentSongs的最后一个记录。
步骤6
app/controller/Song.js
... init: function() { this.control({ 'recentlyplayedscroller': { selectionchange: this.onSongSelect } }); ... }, onSongSelect: function(selModel, selection) { this.getSongInfo().update(selection[0]); } ...
当在RecentlyPlayedScroller选择最后一首song的时候,会触发selectionchange事件。在control方法内,需要监听该事件,并指向onSongSelect方法。这样就完成了在SongInfo视图中更新数据的应用流程。
启动一个新的station
现在,应用变得非常容易实现附加的应用流程,可以为其添加逻辑以便创建和选择一个新的station:
app/controller/Station.js
... refs: [{ ref: 'stationsList', selector: 'stationslist' }], init: function() { // Listen for the select event on the NewStation combobox this.control({ ... 'newstation': { select: this.onNewStationSelect } }); }, onNewStationSelect: function(field, selection) { var selected = selection[0], store = this.getStationsStore(), list = this.getStationsList(); if (selected && !store.getById(selected.get('id'))) { // If the newly selected station does not exist in our station store we add it store.add(selected); } // We select the station in the Station list list.getSelectionModel().select(selected); } ...
总结
通过示例,可以了解到使用高级的控制器技术和保持逻辑与视图分离,应用架构将变得易于理解和维护。在这个阶段,应用程序已经具体一定的功能。可以通过搜索和增加新的station,也可以通过选择开始station。Station的Song会自动加载,并显示song和艺术家信息。
在该系列的下一篇文章,将继续完善该应用,重点将是自定义组件创建和样式。不过,目前已经可以共享当前这些源代码。希望你喜欢并提供反馈。
源代码下载地址:http://cdn.sencha.io/img/architecting-your-app-in-ext-js-code.zip
作者:
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.