视图控制器-ViewControllers
在应用架构方面,Ext JS 提供了很多增强的功能。新增了对视图实体模型(View Model)、MVVM 以及视图控制器(ViewController)的支持,增强了 MVC 架构。
应用程序控制器-Application-level Controllers
控制器(Controller)继承自 Ext.app.Controller
。这些控制器使用类似 CSS 选择器的方式(组件查询 - “Component Queries”)匹配组件和响应组件事件。也可以使用 “refs” 选择和获取组件实例。
这些控制器在应用程序加载时创建,在应用的生命周期内一直存在,并且可以视图的多个实例
挑战-Challenges
对于大型应用程序,这项技术可能带来一定的挑战。在某些情况下,视图和控制器是由不同的团队研发,然后整合到最终的产品中。确保控制器只响应其想要管理的视图可能比较困难。另外,对于研发人员开说,希望限制应用程序加载时需要创建的控制器数量。虽然通过一些方法可以实现延迟加载,但是这些控制器不能被销毁,所以即使不需要他们时,它们也会一直存在。
视图控制器-ViewControllers
Ext JS 5+ 向后兼容应用程序级别的控制器,同时引入了一种新的控制器 Ext.app.ViewController
来应对这种挑战。视图控制器以下列方式进行处理:
- 使用 “listeners” 和 “reference” 配置属性简化与视图的连接;
- 根据视图的生命周期来自动化管理其关联的控制器;
- 通过一对一的关系简化视图与视图控制器的复杂度;
- 通过封装可以提高嵌套视图的可靠性;
- 保留了对其关联视图以下级别的组件选择和事件监听及处理;
事件监听-Listeners
通过两个示例进一步了解一下视图控制器。首先是视图子组件的监听配置:
Ext.define('MyApp.view.foo.Foo', {
extend: 'Ext.panel.Panel',
xtype: 'foo',
controller: 'foo',
items: [{
xtype: 'textfield',
fieldLabel: 'Bar',
listeners: {
change: 'onBarChange' // no scope given here
}
}]
});
Ext.define('MyApp.view.foo.FooController', {
extend: 'Ext.app.ViewController',
alias: 'controller.foo',
onBarChange: function (barTextField) {
// called by 'change' event
}
});
上述示例中展示了一个命名事件处理程序 onBarChange
,并且没有指定 "scope"。 Ext JS 的事件处理系统使用该组件所属的视图控制器做为事件处理程序的作用域("scope")。
传统意义的 "listeners" 配置属性是用于组件本身的事件监听,那么如何让视图监听其自己的或者其基类触发的事件呢?我们需要显示声明作用域("scope"):
Ext.define('MyApp.view.foo.Foo', {
extend: 'Ext.panel.Panel',
xtype: 'foo',
controller: 'foo',
listeners: {
collapse: 'onCollapse',
scope: 'controller'
},
items: [{
...
}]
});
上述示例介绍了 Ext JS 的两个新功能:命名作用域(named scope)和声明式监听配置(declarative listeners)。先看看命名作用域,命名作用域有两个可选值:this
和 controller
。当构建 MVC 应用时,经常使用 controller
以使用视图的视图控制器(不是视图指定的 ViewController
,而是视图控制器的实例)。
由于视图是一种组件,所以我们可以为视图设置 xtype
属性,这样可以让其他视图像创建组件一样创建该视图。例如:
Ext.define('MyApp.view.bar.Bar', {
extend: 'Ext.panel.Panel',
xtype: 'bar',
controller: 'bar',
items: [{
xtype: 'foo',
listeners: {
collapse: 'onCollapse'
}
}]
});
在该示例中,Bar
视图创建了 Foo
视图的一个实例做为其子项。Bar
视图就像 Foo
视图一样监听 collaspe
事件。Foo
视图声明的事件监听在 Foo
的视图控制器上触发,Bar
视图声明的事件监听在 Bar
的视图控制器触发。
Reference
在编写控制器的处理逻辑时,通常面临的问题是如何获取组件的实例,从而完成指定的操作。例如:
Ext.define('MyApp.view.foo.Foo', {
extend: 'Ext.panel.Panel',
xtype: 'foo',
controller: 'foo',
tbar: [{
xtype: 'button',
text: 'Add',
handler: 'onAdd'
}],
items: [{
xtype: 'grid',
...
}]
});
Ext.define('MyApp.view.foo.FooController', {
extend: 'Ext.app.ViewController',
alias: 'controller.foo',
onAdd: function () {
// ... get the grid and add a record ...
}
});
上述示例中,如何获取 grid
组件?所有的技术方案都要求为 grid
设置一个可识别的属性并且赋予一个唯一的标识。过去的技术使用 id
配置属性(使用 getCmp
方法)或者 itemId
配置属性(使用 refs
或者一些组件查询的方法)获取对应的组件。使用 id
的优势是查找速度快,但是这些标识要求整个应用程序内唯一,包括 DOM 元素,这一点一般来说很难达到预期。使用 itemId
和组件查询灵活一些,但是必须执行搜索才能检索到预期的组件。
使用 reference
配置属性,只需要简单地为 grid
添加 reference
就可以通过 lookupReference
获取到它:
Ext.define('MyApp.view.foo.Foo', {
extend: 'Ext.panel.Panel',
xtype: 'foo',
controller: 'foo',
tbar: [{
xtype: 'button',
text: 'Add',
handler: 'onAdd'
}],
items: [{
xtype: 'grid',
reference: 'fooGrid'
...
}]
});
Ext.define('MyApp.view.foo.FooController', {
extend: 'Ext.app.ViewController',
alias: 'controller.foo',
onAdd: function () {
var grid = this.lookupReference('fooGrid');
}
});
这与给组件设置 itemId
的配置属性 fooGrid
,然后使用 this.down('#fooGrid')
的方法相似。然而底层上的差异是非常明显的。首先,reference
配置属性指导组件将其注册到所在视图(在这个示例中由 ViewController 标识)。其次,lookupReference
方法先检索其缓存是否需要刷新(缘于添加或移除组件),如果不需要刷新,直接返回缓存中的引用。伪代码如下:
lookupReference: (reference) {
var cache = this.references;
if (!cache) {
Ext.fixReferences(); // fix all references
cache = this.references; // now the cache is valid
}
return cache[reference];
}
换句话说,没有组件搜索,当添加或移除组件时可以根据需要自动更新。这种方法除了效率外还有其他的优势。
封装-Encapsulation
通过 refs
配置属性,使用选择器进行组件查询非常灵活,但同时也存在一定风险。实际上,查询选择器在组件层次结构的所有层级进行搜索,功能强大但是也有出错的可能。例如:控制器独立运行 100% 正确,但是当引入其他视图时可能就出错了,原因可能就是查询选择器对于新视图搜索出了非预期的组件。
这些问题可以遵循一些实践方法得以避免。如果使用视图控制器来绑定事件监听和组件引用,就能相对避免这些问题。原因就是事件监听和组件引用只与他们所属的视图控制器连接在一起。只要保证组件的引用值在视图内唯一,这些引用值不会暴露给视图外的对象使用。
同样,事件监听也只在它所属的视图控制器内进行解析和处理,不会像查询选择器那样,错误地将事件分发给其他控制器。但是事件监听处理还是倾向于使用查询选择器,在需要使用的场景下,将两种机制结合在一起会处理得更好。
为了实现这个模式,视图触发的事件需要被它所属的视图控制器所捕获。视图控制器有一个方法 fireViewEvent
可以实现这个目的。例如:
Ext.define('MyApp.view.foo.FooController', {
extend: 'Ext.app.ViewController',
alias: 'controller.foo',
onAdd: function () {
var record = new MyApp.model.Thing();
var grid = this.lookupReference('fooGrid');
grid.store.add(record);
this.fireViewEvent('addrecord', this, record);
}
});
这样就使得创建该视图的对象可以按照标准的事件监听模式进行处理了:
Ext.define('MyApp.view.bar.Bar', {
extend: 'Ext.panel.Panel',
xtype: 'bar',
controller: 'bar',
items: [{
xtype: 'foo',
listeners: {
collapse: 'onCollapse',
addrecord: 'onAddRecord'
}
}]
});
事件监听和事件领域-Listeners and Event Domains
在 Ext JS 4.2 版本中, 针对 MVC 的事件分发引入了事件领域 event domains
。当事件被触发时,事件领域捕获这些事件,然后根据查询选择器分发到匹配的控制器。组件的事件领域具有完整的组件查询功能,而其他领域的的组件查询有一些限制。
在 Ext JS 5+ 版本中,每个视图控制器创建一种新的类型的事件领域,称为 view
。该事件领域允许视图控制器使用标准的事件监听,当调用方法时可以缺省限制方法的作用域为所属的视图。同时也添加一个特殊的选择器 #
,匹配视图本身。
Ext.define('MyApp.view.foo.FooController', {
extend: 'Ext.app.ViewController',
alias: 'controller.foo',
control: {
'#': { // matches the view itself
collapse: 'onCollapse'
},
button: {
click: 'onAnyButtonClick'
}
}
});
从上述示例中可以发现事件监听(listeners
)和选择器(selectors
)的差别。选择器 button
将匹配视图及子视图的所有按钮,不考虑组件层级的深度。换句话说,基于选择器的事件处理不考虑封装的边界。
最后要说明一点,事件领域关注视图嵌套,并且基于视图层次结构向上进行事件冒泡。也就是说,当一个事件触发时,首先分发给标准的事件监听。然后,分发给所属的视图控制器,再沿着层次结构向父视图控制器分发。最后分发到标准的组件事件领域(“component” event domain),由继承自 Ext.app.Controller
的控制器进行处理。
生命周期-Life cycle
对于大型应用来说,常规的方法是在第一次需要的时候动态创建控制器。这可以减少应用程序的加载时间,提高应用程序性能,而不用加载所有潜在的控制器。在之前的版本中有个限制,那就是一旦创建了控制器,那么这些控制器在应用程序生命周期内一直存在,不能销毁它们以释放资源。不论视图控制器关联多少个视图,或者没有关联的视图,都不能被销毁。
但是视图控制器是在组件生命周期的早期阶段创建的,在生命周期内一直绑定到组件视图。当销毁视图时,视图控制器也同样被销毁。这意味着不必再管理视图控制器状态,不论有没有视图与其关联。
这种一对一的关系使得跟踪引用变得简单,也降低了忘记销毁组件导致内存泄露的风险。
- beforeInit - 该方法可以被重载以便在视图初始化组件(
initComponent
)之前操作视图。当控制器被创建后立即调用该方法,也就是在组件构造函数中(constructor
)调用initConfig
方法时触发。 - init - 该方法在视图初始化组件之后调用。这个时候,视图已经完成初始化,可以在此进行控制器的初始化操作。
- initViewModel - 该方法在视图实体模型(如果有的话)创建后调用。
- destroy - 该方法用于清理和释放资源,切记调用父类的方法(
callParent
)。