大型的应用在开发和运维上都存在着困难。应用功能的调整和开发人员的调动都会影响对项目的掌控。ExtJS4带来了一种新的应用结构。这种结构不止用于组织代码,也能有效的减小必要的代码量。javascript
此次ExtJS4的应用结构采用了MVC的形式。在这种形式下,Models和Controllers第一次被引入了ExtJS。目前已经有了许多MVC式的结构,这些结构大部分是大同小异。这里是咱们定义的MVC结构:css
在这篇文章中咱们将创建一个简单的用户数据管理应用。经过这个应用能够对ExtJS4的MVC结构有初步的了解。html
ExtJS4的MVC结构提供了一套结构性和一致性的规范。在开发中会发现,Ext的MVC应用是由自定义类和框架代码构成的。按惯例,先说下采用MVC结构开发的好处:java
使用ExtJS的MVC模式开发应用须要使用统一的文件结构 。应用全部的类都放在app目录下。按MVC结构,在app目录下须要创建model、view、store和controller等子目录。下图为咱们的一个示例应用开发完成后的目录结构:ajax
在这个例子中咱们封装了一个应用程序称为“account_manager”,ExtJS4的sdk环境置于ext-4.0目录中。以下为index.html的代码:数据库
1: <html>
2: <head>
3: <title>Account Manager</title>
4: <link rel="stylesheet" type="text/css" href="ext-4.0/resources/css/ext-all.css">
5: <script type="text/javascript" src="ext-4.0/ext-debug.js"></script>
6: <script type="text/javascript" src="app.js"></script>
7: </head>
8: <body></body>
9: </html>
每一个ExtJS4的MVC应用都是经过一个Appliaction类的实例启动。在Application中包含应用的全局定义(好比应用名称),以及应用中所使用到的模型、视图和控制器的引用。此外,Application类还有一个launch函数。launch函数在页面加载完成后执行。json
接下来咱们将建立一个account_manager应用来管理用户帐户信息。首先须要为这个应用定义一个全局的命名空间。全部ExtJS4的MVC应用应该只有一个全局命名空间,应用中全部的类都置于这个命名空间下。通常咱们会给这个命名空间定义一个较短的名称,这里咱们使用“AM”。api
1: Ext.application({
2: name: 'AM',
3:
4: appFolder: 'app',
5:
6: launch: function() {
7: Ext.create('Ext.container.Viewport', {
8: layout: 'fit',
9: items: [
10: {
11: xtype: 'panel',
12: title: 'Users',
13: html : 'List of users will go here'
14: }
15: ]
16: });
17: }
18: });
这里实现的功能很简单。首先,咱们调用 Ext.application建立了一个Application类的实例,并将之命名为“AM”。同时,这里自动建立了一个全局变量“AM”,并为Ext.Loader注册了一个命名空间。而后咱们设置appFolder属性为app目录。最后咱们设置了launch函数,并在函数中建立了包含一个panel的Viewport来填充整个屏幕。数组
能够说控制器(controller)是把整个应用绑定在一块儿的胶水。它们(控制器)执行的工做是事件监听(一般来自view)并作出相应处理。浏览器
咱们继续完成account_manager应用,接下来咱们将建立一个控制器。新建一个js文件app/controller/Users.js,代码以下:
1: Ext.define('AM.controller.Users', {
2: extend: 'Ext.app.Controller',
3:
4: init: function() {
5: console.log('Initialized Users! This happens before the Application launch function is called');
6: }
7: });
将咱们新建的控制器Users添加到应用中,为app.js添加属性以下:
1: Ext.application({
2: ...
3:
4: controllers: [
5: 'Users'
6: ],
7:
8: ...
9: });
当咱们经过index.html在浏览器中加载应用时,控制器Users也会被自动加载(由于咱们在上面的应用定义中作了设置),而后Users的init函数会被调用——在Application类的launch函数执行以前。
在控制器与视图(view)的交互中,init函数的做用十分重要,一般它会和控制器的另外一个函数control一块儿使用。使用control函数比较容易实现对视图事件的监听和响应。
咱们调整下Users控制器,经过control函数来看看panel何时被render(渲染):
1: Ext.define('AM.controller.Users', {
2: extend: 'Ext.app.Controller',
3:
4: init: function() {
5: this.control({
6: 'viewport > panel': {
7: render: this.onPanelRendered
8: }
9: });
10: },
11:
12: onPanelRendered: function() {
13: console.log('The panel was rendered');
14: }
15: });
在上面的代码中,咱们调整了users控制器的init函数,使用this.control为应用中的视图创建了监听。control函数使用新的ComponentQuery引擎简单快速的实现了对页面上的组件的引用。(关于ComponentQuery的内容请参看文档。简单地说,就是使用css式的选择器实现了对页面上组件的快速匹配)。
在上面的init函数中咱们使用了“viewport > panel”这样的语句。“viewport > panel”是一个选择器,它的含义是“找到Viewport的每一个直接子Panel”。随后咱们提供了一个对象,在这个对象中包含事件名称(上例中便是render)及相应的处理函数。实现的效果是任何一个匹配咱们定义的选择器的组件触发了render事件后即会调用onPanelRendered函数。
如今运行下应用看看效果:
并非一个很炫的应用,但它已经展现了使用MVC管理代码的好处。接下来咱们给这个应用添加一个表格,使它变得丰满些。
目前为止咱们的应用只有两个文件、极少的几行代码。如今咱们想给这个应用添加一个表格来展现系统中的用户。是时候使用视图了。
视图只是一个组件,是继承自ExtJS现有组件的类。咱们将定义一个用户信息列表类,新建文件app/view/user/List.js,代码以下:
1: Ext.define('AM.view.user.List' ,{
2: extend: 'Ext.grid.Panel',
3: alias : 'widget.userlist',
4:
5: title : 'All Users',
6:
7: initComponent: function() {
8: this.store = {
9: fields: ['name', 'email'],
10: data : [
11: {name: 'Ed', email: 'ed@sencha.com'},
12: {name: 'Tommy', email: 'tommy@sencha.com'}
13: ]
14: };
15:
16: this.columns = [
17: {header: 'Name', dataIndex: 'name', flex: 1},
18: {header: 'Email', dataIndex: 'email', flex: 1}
19: ];
20:
21: this.callParent(arguments);
22: }
23: });
如上,这个视图类也只是一个简单的普通类。咱们只是继承了Grid组件,定义了一个别名,经过这个别名咱们可使用xtype调用这个类(通常是这么用,还有别的用处)。同时咱们还添加了列表须要使用的store和columns信息。
接下来咱们须要将这个视图添加到Users控制器中。咱们已经使用'widget.'形式设置了类的别名,因此能够直接使用“userlist”做xtype,就跟之前使用“xtype: ‘panel’”同样。
1: Ext.define('AM.controller.Users', {
2: extend: 'Ext.app.Controller',
3:
4: views: [
5: 'user.List'
6: ],
7:
8: init: ...
9:
10: onPanelRendered: ...
11: });
而后咱们在应用的Viewport布局中调用这个类。这须要在app.js中修改launch函数:
1: Ext.application({
2: ...
3:
4: launch: function() {
5: Ext.create('Ext.container.Viewport', {
6: layout: 'fit',
7: items: {
8: xtype: 'userlist'
9: }
10: });
11: }
12: });
这里还有一点须要注意,在控制器的views数组中,咱们设置了'user.List'
。这告诉了应用去调用视图文件view/user/List.js,这样在应用加载时就能够直接使用这个文件了。这里使用了ExtJS4新增的的动态调用服务以从服务器中获取文件。如今再看看咱们的acount_manager应用:
请注意,代码更新后onPanelRendered函数仍然被调用了。这表名咱们自定义的列表类仍然匹配“viewport > panel”选择器。由于咱们自定义的userlist类继承自Grid类,Grid类继承自Panel类。
此时咱们添加到选择器中的监听仍然被Viewport直属的Panel类或其子类所调用,咱们能够利用这点对咱们的用户信息列表的功能作些强化。仍是使用init函数,不过要改成监听用户信息记录上的双击事件,使双击后能够对用户信息进行编辑。调整控制器:
1: views: [
2: 'user.List'
3: ],
4:
5: init: function() {
6: this.control({
7: 'userlist': {
8: itemdblclick: this.editUser
9: }
10: });
11: },
12:
13: editUser: function(grid, record) {
14: console.log('Double clicked on ' + record.get('name'));
15: }
对控制器Users咱们作了以下调整:
修改了ComponentQuery的选择器,如今只是使用‘userlist’;修改监听的事件为“itemdblclick”;调整事件处理函数为“editUser”。再次运行应用,双击记录时会在控制台上输出相应的信息:
应用运行的很好。可是咱们不会知足于简单的在控制台上输出信息,咱们但愿能真正的编辑用户信息。这里要建立一个新的视图:app/view/user/Edit.js
:
1: Ext.define('AM.view.user.Edit', {
2: extend: 'Ext.window.Window',
3: alias : 'widget.useredit',
4:
5: title : 'Edit User',
6: layout: 'fit',
7: autoShow: true,
8:
9: initComponent: function() {
10: this.items = [
11: {
12: xtype: 'form',
13: items: [
14: {
15: xtype: 'textfield',
16: name : 'name',
17: fieldLabel: 'Name'
18: },
19: {
20: xtype: 'textfield',
21: name : 'email',
22: fieldLabel: 'Email'
23: }
24: ]
25: }
26: ];
27:
28: this.buttons = [
29: {
30: text: 'Save',
31: action: 'save'
32: },
33: {
34: text: 'Cancel',
35: scope: this,
36: handler: this.close
37: }
38: ];
39:
40: this.callParent(arguments);
41: }
42: });
咱们再次定义了一个ExtJS组件的子类。此次是继承了Ext.window.Window。咱们仍须要使用initComponent函数为编辑视图Edit添加表单元素和按钮。布局采用了“fit”形式,窗体下只有一个form面板,在form面板中有两个文本框用以编辑姓名和邮件地址。最后还定义了两个按钮用以关闭窗体和保存更改。
而后咱们要作的就是把这个编辑视图添加到控制器,并将用户信息添加到编辑视图。见代码:
1: Ext.define('AM.controller.Users', {
2: extend: 'Ext.app.Controller',
3:
4: views: [
5: 'user.List',
6: 'user.Edit'
7: ],
8:
9: init: ...
10:
11: editUser: function(grid, record) {
12: var view = Ext.widget('useredit');
13:
14: view.down('form').loadRecord(record);
15: }
16: });
重点看一下editUser函数。在这个函数中,咱们使用了函数’Ext.widget’,其功能相似于Ext.create('widget.useredit')。而后咱们再次使用ComponentQuery创建了到编辑视图的映射。每一个ExtJS4的组件都有一个down函数以接收ComponentQuery的选择器并快速的查找选择器对应的直属子元素。
双击列表中的行,效果以下图:
如今咱们已经作好了编辑视图,也差很少能够进行编辑和保存了。可是在作这些工做以前,咱们还须要对咱们的代码进行一次重构。
如今的AM.view.user.List组件中,store是写在List类中。虽然如今应用也运行的很好,可是咱们更倾向于将store独立出来,这样也便于咱们对store中的数据进行处理。如今咱们开始将store中从List视图中拆分出来放到app/store/Users.js中:
1: Ext.define('AM.store.Users', {
2: extend: 'Ext.data.Store',
3: fields: ['name', 'email'],
4: data: [
5: {name: 'Ed', email: 'ed@sencha.com'},
6: {name: 'Tommy', email: 'tommy@sencha.com'}
7: ]
8: });
而后咱们还须要作两处小的调整。
首先在Users控制器中添加对store的调用:
1: Ext.define('AM.controller.Users', {
2: extend: 'Ext.app.Controller',
3: stores: [
4: 'Users'
5: ],
6: ...
7: });
而后更新app/view/user/List.js,添加对store的引用:
1: Ext.define('AM.view.user.List' ,{
2: extend: 'Ext.grid.Panel',
3: alias : 'widget.userlist',
4:
5: //we no longer define the Users store in the `initComponent` method
6: store: 'Users',
7:
8: ...
9: });
在控制器中引用store的目的是动态的调用store定义页,并为调用的store赋上一个简短的storeId。这使在视图中引用store更加方便(在上例中只是添加了一个属性 store: 'Users')。
如今咱们是将字段(name和email)定义在store中。这样也能够工做了,可是ExtJS4提供了一个颇有用的类Ext.data.Model来帮助咱们。咱们将使用Model再次重构咱们的代码,Model定义于app/model/User.js中:
1: Ext.define('AM.model.User', {
2: extend: 'Ext.data.Model',
3: fields: ['name', 'email']
4: });
完成model的定义后,须要在控制器和store中添加对Model的引用,并从store去掉对应字段的声明:
1: //the Users controller will make sure that the User model is included on the page and available to our app
2: Ext.define('AM.controller.Users', {
3: extend: 'Ext.app.Controller',
4: stores: ['Users'],
5: models: ['User'],
6: ...
7: });
8:
9: // we now reference the Model instead of defining fields inline
10: Ext.define('AM.store.Users', {
11: extend: 'Ext.data.Store',
12: model: 'AM.model.User',
13:
14: data: [
15: {name: 'Ed', email: 'ed@sencha.com'},
16: {name: 'Tommy', email: 'tommy@sencha.com'}
17: ]
18: });
咱们所作的重构工做不会立刻产生效果,不过会有助于下一环节的工做。再次刷新页面运行应用会发现并没有任何变化。不过是时候完成编辑功能了。
如今咱们有一个用户信息列表了,也能够双击记录打开编辑窗口了。接下来须要实现的任务就是保存用户编辑结果。如今咱们的用户信息编辑窗口有一个表单和一个保存按钮“save”。接下来咱们须要调整控制器的init函数,添加一个对“save”按钮的监听:
1: Ext.define('AM.controller.Users', {
2: init: function() {
3: this.control({
4: 'viewport > userlist': {
5: itemdblclick: this.editUser
6: },
7: 'useredit button[action=save]': {
8: click: this.updateUser
9: }
10: });
11: },
12:
13: updateUser: function(button) {
14: console.log('clicked the Save button');
15: }
16: });
咱们在控制器中添加了第二个ComponentQuery选择器:‘'useredit button[action=save]'’。它和第一个选择器工做模式相同,首先根据xtype‘useredit’找到咱们定义的用户信息编辑窗体,而后在窗体中查找action属性为‘save’的按钮。在咱们定义用户信息编辑窗体类时,咱们给“save”按钮设置action:’save’属性就给咱们提供了一个能够快速定位到这个按钮的方法。
咱们很高兴看到在咱们点击save按钮时updateUser函数被调用并执行了。
咱们已经将save按钮点击事件的监听与处理完成了。接下来要作的就是完善updateUser这个函数,真正实现用户信息的编辑。在
updateUser函数中咱们须要从表单中获取信息并保存到用户信息store中。让咱们看看功能是怎样实现的:
1: updateUser: function(button) {
2: var win = button.up('window'),
3: form = win.down('form'),
4: record = form.getRecord(),
5: values = form.getValues();
6:
7: record.set(values);
8: win.close();
9: }
这里须要打断一下作些说明。经过按钮的点击事件咱们获取了对‘save’按钮的引用,可是咱们真正须要的是包含数据的表单及用户信息编辑窗体。为了更快的解决问题咱们再次使用了ComponentQuery的选择器,首先使用button.up('window')获取了用户信息编辑窗体,然后使用win.down('form')获取了表单。
这以后咱们要作的工做就很简单了,获取加载到表单中的record,并用用户编辑后的信息更新record。最后关闭窗体,回到用户信息列表上。下图是应用运行后的结果,咱们将第一条记录的姓名改成了“Ed Spencer”:
刚刚的工做很简单吧。咱们还须要完成最后一步——实现与服务器端的交互。以前咱们只是生硬地将用户信息写在store中,接下来咱们将尝试使用Ajax获取用户信息:
1: Ext.define('AM.store.Users', {
2: extend: 'Ext.data.Store',
3: model: 'AM.model.User',
4: autoLoad: true,
5:
6: proxy: {
7: type: 'ajax',
8: url: 'data/users.json',
9: reader: {
10: type: 'json',
11: root: 'users',
12: successProperty: 'success'
13: }
14: }
15: });
这里咱们去掉了data属性,取而代之的是proxy属性。proxy是Extjs4中获取和保存数据的一种方式。ExtJS4中有多种形式的proxy,这里咱们使用了比较简单的ajax proxy,咱们告诉它从'data/users.json'中获取信息。
从上面的代码中看到,咱们给proxy提供了一个reader对象。reader的做用是解码服务器端反馈的数据集信息。此次咱们使用的是JSON Reader,并设置了它的root和successProperty属性。最后咱们还须要建立一个'data/users.json'文件,将咱们之前使用的数据复制进去:
1: {
2: success: true,
3: users: [
4: {id: 1, name: 'Ed', email: 'ed@sencha.com'},
5: {id: 2, name: 'Tommy', email: 'tommy@sencha.com'}
6: ]
7: }
在store中的另外一处改动是将autoLoad属性设置为true。这意味着stroe会主动向proxy发出请求加载数据。如今咱们刷新页面从新加载应用并不能看到任何变化。可是实际上获取数据的方式已经不同了。
咱们要作的最后一件事就是将对数据所作的改变返回到服务器上。在咱们这个例子中咱们是使用静态的JSON文件在服务器端存储数据,因此咱们不能看到任何数据库的变化。可是至少咱们能够验证应用是在正常工做的。首先咱们须要对store的proxy作出一点小的调整告诉它将更新信息发送到另外一个url:
1: proxy: {
2: type: 'ajax',
3: api: {
4: read: 'data/users.json',
5: update: 'data/updateUsers.json'
6: },
7: reader: {
8: type: 'json',
9: root: 'users',
10: successProperty: 'success'
11: }
12: }
咱们仍然会从users.json中获取数据,可是全部的更新都会发送到updateUsers.json。这只会返回一个虚拟的相应,让咱们知道应用在正常运行。updateUsers.json中的内容是{"success": true}
。而后还要作的就是告诉Store在编辑完成后同步一次记录,为此咱们还要在updateUser函数中添加一行代码:
1: updateUser: function(button) {
2: var win = button.up('window'),
3: form = win.down('form'),
4: record = form.getRecord(),
5: values = form.getValues();
6:
7: record.set(values);
8: win.close();
9: this.getUsersStore().sync();
10: }
如今咱们的这个示例应用已经完成了。咱们再运行这个应用一次,编辑一条记录,点击保存按钮,而后查看request是否正确的发送给了updateUser.json文件:
示例应用account_manager的源码可在ExtJS4的文档中找到,所在目录是examples/app/simple。
做者注:本文译自Extjs4.0文档中的《MVC Architecture》一文。限于在下的英文水平及对Ext的理解,因此不免有些不足之处。不过是抱着本身学习也方便你们的心思,抛砖引玉勉强翻译了全文。若是有不通之处还请及时指正。