前言:
控制器 [Controller] 在Sencha Touch MVC中起到的是纽带作用,它控制视图 [View] 的展示逻辑,又负责以数据模型 [Model] 为基础的数据 [Data] 处理逻辑(包括数据的加载、展示、更新、增删等等)。控制器就像胶水,有了它才能够把一个Sencha Touch(后面一律简写做ST)应用程序 [Application] 的各项元素黏合在一起,使之协调工作并完成预期的任务。
文章英文原址是:http://docs.sencha.com/touch/2-0/#!/guide/controllers
原文标题是 Controllers ,直译就是控制器,不过在官方文档中也称之为Controllers Guide,我觉得用这个标题更为恰切一些。
Sencha Touch 交流QQ群213119459
Controllers Guide
控制器学习指南
控制器用来响应发生在应用程序中的事件,如果你的应用程序包含一个让用户点击的登出按钮,那么就应该有一个控制器来侦听该按钮的点击事件然后作出恰当的反应。控制器就像胶水,它控制视图类展示数据,并通过数据模型类来加载和保存数据,于是两者就这样被黏合在一起。
Relation to Ext.app.Application
控制器与Ext.app.Application的关系
所有应用程序用到的控制器都在Ext.app.Application.controllers 中进行声明,应用程序会自动实例化每一个控制器并保持对它们的引用,因此我们一般无需自己去直接实例化控制器。按照惯例,每一个控制器的命名应该对应他要处理的事物(通常是数据模型),而且常常是复数形式。例如你的应用程序叫做 “MyApp”,你需要一个用来管理产品的控制器,按照常规就应该在app/controller/Products.js 文件中创建一个MyApp.controller.Products 的类。
Refs and Control
Refs和Control(控制器构造函数中的两个参数)
控制器的核心部分是refs和control这对双生参数,它们被用于获得该应用程序所含组件的引用,并根据其触发的事件给予相应的处理,先看 refs。
Refs
Refs扩充了本已很强大的 ComponentQuery 语法,使之能够很容易的在网页上定位到组件,每个控制器都可以定义多个refs子项,例如这儿我们就定义了一个叫做“nav”的ref来引用该页面上id为“mainNav”的组件,在下面的addLogoutButton函数中我们会用到它。
Ext.define('MyApp.controller.Main', {
extend: 'Ext.app.Controller',
config: {
refs: {
nav: '#mainNav'
}
},
addLogoutButton: function() {
this.getNav().add({
text: 'Logout'
});
}
});
通常一个ref就是一个键值对,键名(本例中是“nav”)就是这个即将生成的ref引用的名称,值(本例中是”#mainNav”)就是被用来查找组件的 ComponentQuery 选择器。
后面我们创建了一个名叫addLogoutButton的简单函数,它通过一个叫做“getNav”的方法使用了这个ref,这种以get开头的函数是根据你在refs中定义的ref名称按照指定格式自动生成的,即get后面紧跟(首字母)大写的ref名称。在这个例子当中我们把nav设定成一个Toolbar工具栏,而调用addLogoutButton会给nav工具栏添加一个Logout按钮,这个ref可以识别类似下面这样的Toolbar:
Ext.create('Ext.Toolbar', {
id: 'mainNav',
items: [
{
text: 'Some Button'
}
]
});
假定这个toolbar已经被创建了(看代码,它里面已经有了一个button),我们调用'addLogoutButton'函数后,它会被加上第二个button。
Advanced Refs
Refs进阶
除了name和selector以外,Refs的值还可以接受另一对参数,那就是autoCreate和xtype,他们也几乎总是一起出现的。
Ext.define('MyApp.controller.Main', {
extend: 'Ext.app.Controller',
config: {
refs: {
nav: '#mainNav',
infoPanel: {
selector: 'tabpanel panel[name=fish] infopanel',
xtype: 'infopanel',
autoCreate: true
}
}
}
});
我们给控制器增加了第二个ref,同样名称'infoPanel'作为键,不过这次值成了一个object对象,除此之外我们还用了一个稍微复杂一点的选择器,此例中我们假定你的应用程序包含一个tabpanel并且其中之一的item的name是'fish',选择器会匹配该item当中xtype类型为'infopanel'的组件。
这次的问题在于这个infopanel并不存在于名为'fish'的panel中,而当我们在控制器中调用this.getInfoPanel方法时,它会自动创建一个infopanel。之所以能够做到这样是因为,哪怕选择器无法返回任何符合条件的对象,但由于我们提供了xtype类型,所以控制器也能够干脆自己创建一个。
Control
Refs的伴生配置属性是control(因为它们几乎总是配对出现),control是一种手段,通过它你可以侦听组件触发的事件,并且使你的控制器做出某种反应。control既可以接受ComponentQuery选择器又能接受refs名称作为它的键名,但是control的值必须是listener对象,比如下面:
Ext.define('MyApp.controller.Main', {
extend: 'Ext.app.Controller',
config: {
control: {
loginButton: {
tap: 'doLogin'
},
'button[action=logout]': {
tap: 'doLogout'
}
},
refs: {
loginButton: 'button[action=login]'
}
},
doLogin: function() {
//called whenever the Login button is tapped
},
doLogout: function() {
//called whenever any Button with action=logout is tapped
}
});
这里我们给出了两个control声明,一个针对名为loginButton的ref,另一个针对页面上所有action被设置为logout的按钮。我们给每一个声明都传入了一个事件处理程序,侦听到tap事件来自哪个按钮,哪个按钮指定的响应动作就会被执行。注意我们在control代码块中是使用字符串的方式来指定doLogin和doLogout函数的,这一点很重要。
每一个control声明中都可以按照你的需要侦听多个事件,并且允许混合使用ComponentQuery选择器和refs作为control的key。
Routes
路由
在ST2中,控制器可以直接指定哪个路由是它感兴趣的。这就使得我们可以在应用程序中提供访问历史支持,同样我们提供路由也可以实现深度链接,即任意链接至应用程序的任意部分。
假设说我们有一个负责响应登录和浏览用户资料的控制器,并且希望这些界面可以通过url直接访问到,我们可以这样做:
Ext.define('MyApp.controller.Users', {
extend: 'Ext.app.Controller',
config: {
routes: {
'login': 'showLogin',
'user/:id': 'showUserById'
},
refs: {
main: '#mainTabPanel'
}
},
//uses our 'main' ref above to add a loginpanel to our main TabPanel (note that
//当添加loginpanel到主TabPanel的时候需要用到上面定义的叫做main的ref
//'loginpanel' is a custom xtype created for this application)
//注意’loginpanel’是我们为这个应用程序自定义的一个xtype类型
showLogin: function() {
this.getMain().add({
xtype: 'loginpanel'
});
},
//Loads the User then adds a 'userprofile' view to the main TabPanel
//加载用户Model并添加一个userprofile视图到主TabPanel界面
showUserById: function(id) {
MyApp.model.User.load(id, {
scope: this,
success: function(user) {
this.getMain().add({
xtype: 'userprofile',
user: user
});
}
});
}
});
上面我们指定的routes可以很容易就把浏览器地址栏路径映射到一个与之匹配的控制器函数,routes可以是像“login”项那样的字符串,他匹配的路径是http://myapp.com/#login,也可以像'user/:id'那样包含通配符,他匹配的路径是类似http://myapp.com/#user/123 这种,当地址栏路径改变的时候,控制器就会自动调用指定的函数。
注意在showUIserById函数中,我们先加载了User Model的实例。当使用路由功能时,每个路由对应的函数都必须负责加载数据并还原状态,因为你的用户有可能把这个url发给其他人或者直接刷新了页面,这些情况下页面都会清空掉我们本已加载好的数据。关于如何在使用路由时还原状态,在应用程序架构指南里有更深入的探讨。
Before Filters
Before筛选器
控制器还在路由的上下文环境里提供一个过滤器功能,该过滤器可以设置在运行路由指定的函数之前必须先调用另一个函数。对于某些特殊操作来讲,在这里放置用户身份验证功能,或者加载一些新的类,是非常合适的。比如我们电子商务后台的商品编辑功能,显然就需要首先验证用户身份。
Ext.define('MyApp.controller.Products', {
config: {
before: {
editProduct: 'authenticate'
},
routes: {
'product/edit/:id': 'editProduct'
}
},
//this is not directly because our before filter is called first
//该函数不会直接运行因为before给它指定了一个需要优先执行的筛选器
editProduct: function() {
//... performs the product editing logic
// 在这里执行商品编辑逻辑
},
//this is run before editProduct
//该函数会在editProduct之前被调用
authenticate: function(action) {
MyApp.authenticate({
success: function() {
action.resume();
},
failure: function() {
Ext.Msg.alert('Not Logged In', "You can't do that, you're not logged in");
}
});
}
});
每当用户导航至一个这样的链接http://myapp.com/#product/edit/123,控制器的authenticate函数都会被首先调用并且被传入一个Ext.app.Action作为参数,该action其实就是在路由中指定的那个函数(如果没有before筛选器,这个函数才是应该被执行的)。这个action简单的描绘出了控制器、功能函数以及其他数据(比如url传过来的id)之间的关系。
筛选器能够以任意方式执行,同步异步均可。本例中我们使用了应用程序的authenticate方法去验证当前用户的身份。该方法将会触发一个AJAX请求来验证服务器上的用户凭证,因此它是使用同步方式执行的,验证成功后调用action.resume()将会把控制权交回给传入的action,失败则会告知用户他需要首先登陆。
Before筛选器同样可以用于在某些特定动作执行之前加载其他类文件,例如对于一些不常用的操作你可能会希望直到需用时才去加载(延迟按需加载)它的代码文件,这样应用程序可以启动更快些,为达到这个目的你可以设置一个筛选器中来使用Ext.Loader加载它们。
使用Before,你可以为每个action指定任意个数的筛选器,多个筛选器的时候只需要像这样传入一个数组:
Ext.define('MyApp.controller.Products', {
config: {
before: {
editProduct: ['authenticate', 'ensureLoaded']
},
routes: {
'product/edit/:id': 'editProduct'
}
},
//this is not directly because our before filter is called first
//该函数不会直接运行因为before给它指定了一个需要优先执行的筛选器
editProduct: function() {
//... performs the product editing logic
// 在这里执行商品编辑逻辑
},
//this is the first filter that is called
//这是被调用的第一个筛选器函数
authenticate: function(action) {
MyApp.authenticate({
success: function() {
action.resume();
},
failure: function() {
Ext.Msg.alert('Not Logged In', "You can't do that, you're not logged in");
}
});
},
//this is the second filter that is called
//这是第二个要被调用的筛选器函数
ensureLoaded: function(action) {
Ext.require(['MyApp.custom.Class', 'MyApp.another.Class'], function() {
action.resume();
});
}
});
筛选器函数会被依次调用,每一个当中都必须使用action.resume() 将程序控制权转交回来。
Profile-specific Controllers
多设备配置下的控制器
超类,控制器的共享部分(该控制器将被不同设备的控制器继承)
Ext.define('MyApp.controller.Users', {
extend: 'Ext.app.Controller',
config: {
routes: {
'login': 'showLogin'
},
refs: {
loginPanel: {
selector: 'loginpanel',
xtype: 'loginpanel',
autoCreate: true
}
},
control: {
'logoutbutton': {
tap: 'logout'
}
}
},
logout: function() {
//code to close the user's session
}
});
Phone Controller:
手机的控制器
Ext.define('MyApp.controller.phone.Users', {
extend: 'MypApp.controller.Users',
config: {
refs: {
nav: '#mainNav'
}
},
showLogin: function() {
this.getNav().setActiveItem(this.getLoginPanel());
}
});