您的位置:首页 > Web前端 > JavaScript

【ExtJS 4.x学习教程】(2)MVC结构

2014-05-29 21:51 417 查看
作者:周邦涛(Timen)

Email:zhoubangtao@gmail.com

转载请注明出处: /article/9909405.html

1. 简介

大型客户端应用总是很难编写、组织和维护。你对一个大型客户端应用添加的功能越多,投入的开发人员越多,它就越快趋向于失控。Ext JS 4自带一个应用架构能够让你更有效的组织你的代码,并且减少工作量。

随着Model和Controller的首次引入,Ext JS 4的应用程序架构越来越像MVC模式,现在有很多种MVC架构,并且每个都不尽相同。我们看看Ext JS 4中是怎么定义MVC架构的:

Model 是多个字段以及它所存储的数据的集合(例如 一个拥有username和password两个字段的User模型)。模型通过Ext JS 4的data包实现持久化,并且可以通过关联(association)与其他模型联系起来。Model很想Ext JS 3的Record类的工作原理,并且通常跟Store一起使用将数据呈现在grid或者其他的组件中。
View 可以任何的组件(component),例如grid,tree和panel等等
Controller 是你存放应用逻辑的地方,无论是渲染视图,实例化模型还是任何其他的应用逻辑

在这篇文章中,我们将创建一个非常简单的管理用户数据应用。到最后你就会知道怎么使用Ext JS 4应用程序架构将一些简单的应用程序组装起来。

Ext JS 4应用程序架构既关注提供代码结构和一致性,也关注实际的类和框架代码。我们看一下几个重要的好处:

每个应用程序工作方式都一样,所以你只需要学习一次
在不同的应用之间共享代码很简单,因为他们的工作方式是一样的
你可以使用Ext JS提供的构建工具创建你应用的优化版本,从而使用在生产环境中

2. 文件结构

Ext JS 4应用采用统一的目录结构。你可以在【ExtJS 4.x学习教程】(0)简介一文中学习一个应用程序基本文件结构的详细解释。在MVC模式下,所有的类都位于app目录下,app目录下又包含子目录,从而形成model,view,controller和store的命名空间。下边看一个简单例子应用的目录结构:



在这个例子中,我们将整个应用包在一个目录下——‘account_manager’。Ext JS SDK的文件包在ext-4.0目录下。因此index.html的内容如下:

<html>
<head>
<title>Account Manager</title>

<link rel="stylesheet" type="text/css" href="ext-4.0/resources/css/ext-all.css">

<script type="text/javascript" src="ext-4.0/ext-debug.js"></script>

<script type="text/javascript" src="app.js"></script>
</head>
<body></body>
</html>


3. 在app.js中创建应用

每一个Ext JS 4应用都以一个Application实例为入口。这个Application包含了应用程序的全局设置(例如应用名称),并且维护者应用所使用的model、view和controller。Application还包含一个launch函数,它会在Application依赖的资源被全部加载完后自动执行。

让我们创建一个简单的账户管理应用吧。首先我们需要为我们的应用程序选一个全局的命名空间。所有的Ext JS 4应用都应该只使用一个全局变量,然后将所有的应用程序类包裹在里边。通常我们需要一个简短的全局变量,所以这里我们就使用“AM”:

Ext.application({
name: 'AM',

appFolder: 'app',

launch: function() {
Ext.create('Ext.container.Viewport', {
layout: 'fit',
items: [
{
xtype: 'panel',
title: 'Users',
html : 'List of users will go here'
}
]
});
}
});


这里做了几件事。第一件,我们调用Ext.application实例化一个Application类,并且把name配置属性赋值为“AM”。这样Ext JS 4就会为我们自动创建一个AM全局变量,并且将其与通过appFolder配置属性设置的‘app’路径组合形成的命名空间注册给Ext.Loader,另外我们还提供了一个简单的launch函数,由其创建一个包含充满全屏Panel的Viewport对象。



4. 定义一个Controller

Controller是把应用绑定到一起的胶水。它要做的事情就是监听事件(通常来自view层),然后采取一些动作。继续我们的账户管理应用,让我们创建一个Controller。创建一个叫做app/controller/Users.js然后添加以下代码:

Ext.define('AM.controller.Users', {
extend: 'Ext.app.Controller',

init: function() {
console.log('Initialized Users! This happens before the Application launch function is called');
}
});


现在让我们添加我们刚刚创建的Users控制器到Application的config中:

Ext.application({
...

controllers: [
'Users'
],

...
});


当我们在浏览器中通过访问index.html加载我们的应用的时候,Users控制器就会自动加载(因为我们在Application的配置中说明了),并且它的init方法会在Application的launch方法调用之前被调用。

Users控制器的init方法主要用来定义你的Controller怎么和view层交互,并且通常和Controller的control方法结合使用。control方法能够很方便的监听view类的事件并且财务对应的动作。下载让我们更新一下Users控制器让panel被渲染后输出一行话。

Ext.define('AM.controller.Users', {
extend: 'Ext.app.Controller',

init: function() {
this.control({
'viewport > panel': {
render: this.onPanelRendered
}
});
},

onPanelRendered: function() {
console.log('The panel was rendered');
}
});


这里我们更新了init方法,让它使用control设置view层的监听器。control方法使用新的ComponentQuery引擎快速、方便的获取页面上组件的引用。如果你还不熟悉ComponentQuery,先参考一下它的API文档(当然我会在后期的课程中进行介绍)。简单来说,它能够让我们通过传入一个CSS选择器找到页面上所有匹配的组件。

在上边的init方法中,我们使用了‘viewport > panel’,也就是“找到Viewport的所有直接子Panel”。然后我们使用一个映射每一个事件名和它对应的处理器(当然,我们这里只映射了render事件)。最终效果就是无论何时匹配我们选择器的组件触发了一个render事件,我们的onPanelRendered函数就会被调用。

当我们运行我们的应用时,我们会看到如下界面:



当然这个应用还并不令人振奋,但是它展示了开始组织代码是多么的简单。让我们在丰富一下这个应用,我们加一个grid

5. 定义View

直到现在我们的应用还只有几行长,并且只包含两个文件——app.js和app/controller/Users.js。现在我们想添加一个grid展示我们系统中的用户,是时候更好的组织我们的逻辑,并且使用view了

一个View就是一个Component,通常定一个Ext JS组件的子类。现在开始创建我们的Users表格,先创建一个app/view/user/List.js文件,然后将下边的代码贴进去:

Ext.define('AM.view.user.List' ,{
extend: 'Ext.grid.Panel',
alias : 'widget.userlist',

title : 'All Users',

initComponent: function() {
this.store = {
fields: ['name', 'email'],
data  : [
{name: 'Ed',    email: 'ed@sencha.com'},
{name: 'Tommy', email: 'tommy@sencha.com'}
]
};

this.columns = [
{header: 'Name',  dataIndex: 'name',  flex: 1},
{header: 'Email', dataIndex: 'email', flex: 1}
];

this.callParent(arguments);
}
});


我们的View类就是一个普通的类。这里我们继承了Grid组件并且设置了一个别名,这样我们就可以把它作为一个xtype使用(我们后边会更详细的解释)。我们还传入了grid渲染时需要的store和column。

下一步我们需要添加这个view到我们的Users控制器中。因为我们使用‘widget.’格式为它设置了别名,所以我们可以使用‘userlist’作为一个xtype,就像前边使用‘panel’一样。

Ext.define('AM.controller.Users', {
extend: 'Ext.app.Controller',

views: [
'user.List'
],

init: ...

onPanelRendered: ...
});


然后改变app.js中的launch方法把它在viewport中渲染出来

Ext.application({
...

launch: function() {
Ext.create('Ext.container.Viewport', {
layout: 'fit',
items: {
xtype: 'userlist'
}
});
}
});


这里还要注意的另一件事是我们这里在views数组中指明了‘user.List’。这就告诉了应用程序在自动加载List.js这个文件。这样我们就可以在launch的时候使用它。应用程序使用Ext JS 4新的动态加载系统自动化的从服务器加载对应文件。当我们刷新页面的时候,就会看到下边的效果:



6. 控制grid

注意我们的onPanelRendered函数依然会被调用。这是因为我们的grid类也符合‘viewport > panel’选择器。原因是我们的类继承自Grid,而Grid又继承自Panel。

这时,我们添加到这个选择器的监听器将会为每一个是viewport的直接子Panel或者子Panel的子类被调用,所以我们我们使用我们新的xtype以便绑定的更准确点。让我们监听grid每一行的双击事件,以便我们能够编辑对应的User:

Ext.define('AM.controller.Users', {
extend: 'Ext.app.Controller',

views: [
'user.List'
],

init: function() {
this.control({
'userlist': {
itemdblclick: this.editUser
}
});
},

editUser: function(grid, record) {
console.log('Double clicked on ' + record.get('name'));
}
});


注意我们改变了ComponentQuery选择器为‘userlist’,事件名为’itemdbclick‘,处理器‘editUser’。现在当我们双击的时候我们只记录用户的姓名,效果如下:



打日志到控制台是非常好,但是我们真的想编辑用户。就让我们开始吧,重新建一个view,app/view/user/Edit.js:

Ext.define('AM.view.user.Edit', {
extend: 'Ext.window.Window',
alias : 'widget.useredit',

title : 'Edit User',
layout: 'fit',
autoShow: true,

initComponent: function() {
this.items = [
{
xtype: 'form',
items: [
{
xtype: 'textfield',
name : 'name',
fieldLabel: 'Name'
},
{
xtype: 'textfield',
name : 'email',
fieldLabel: 'Email'
}
]
}
];

this.buttons = [
{
text: 'Save',
action: 'save'
},
{
text: 'Cancel',
scope: this,
handler: this.close
}
];

this.callParent(arguments);
}
});


我们再一次定义了一个已经存在的组件的子类,这次是Ext.window.Window。我们又一次使用initComponent去指定复杂对象items和buttons。我们使用了‘fit’布局,并且使用form作为唯一的item,这个form包含了要编辑的姓名和邮件地址。最后我们创建了两个按钮,一个用来关闭窗口,另一个用来保存修改。

我们现在要做的就是把这个view添加到控制器中,渲染它并且把User给他加载进去:

Ext.define('AM.controller.Users', {
extend: 'Ext.app.Controller',

views: [
'user.List',
'user.Edit'
],

init: ...

editUser: function(grid, record) {
var view = Ext.widget('useredit');

view.down('form').loadRecord(record);
}
});


首先我们使用便捷的方法Ext.widget创建了view,这就相当于使用Ext.create('widget.useredit')。之后我们又一次使用ComponentQuery快速获取编辑窗的form的引用。Ext JS 4中的每一个组件都有一个down方法,它接收一个ComponentQuery选择器去快速找到它的子组件。

在我们的grid中双击一行,然后你会看到下边的效果:



7. 创建Model和Store

现在我们已经有了编辑表单,是时候开始编辑和保存用户数据了。但是在开始之前我们先稍微重构一下我们的代码。

目前AM.view.user.List组件是在内部创建了一个Store。这样是工作的很好,但是我们希望有能力在应用的任何地方引用这个Store,以便我们能够更新其中的数据。我们将会把这个Store拆分到一个单独的文件中——app/store/Users.js:

Ext.define('AM.store.Users', {
extend: 'Ext.data.Store',
fields: ['name', 'email'],
data: [
{name: 'Ed',    email: 'ed@sencha.com'},
{name: 'Tommy', email: 'tommy@sencha.com'}
]
});


现在我们就做两个小变化——第一,我们让Users控制器包含这个Store:

Ext.define('AM.controller.Users', {
extend: 'Ext.app.Controller',
stores: [
'Users'
],
...
});


第二,我们更新app/view/user/List.js,让它通过Store的id去引用这个Store:

Ext.define('AM.view.user.List' ,{
extend: 'Ext.grid.Panel',
alias : 'widget.userlist',

//we no longer define the Users store in the `initComponent` method
store: 'Users',

...
});


通过添加Users控制器定义时关心的stores,它们就能就能自动被加载到页面并且被赋予一个storeId,这样就是它能够很容易的在我们的view中被引用(这个例子中只需要配置store:'Users')。

目前我们只在Store中定义了字段(‘name’和‘email’)。这样就能很好的工作,但是在Ext JS 4有一个强大的Ext.data.Model类,它能在我们编辑用户是发挥很大的功能。我们将会把字段抽取成Model,定在app/model/User.js文件中:

Ext.define('AM.model.User', {
extend: 'Ext.data.Model',
fields: ['name', 'email']
});


这就是我们定一个Model所需要的全部工作了,现在来更新Store来引用Model的名字而不是在内部提供字段名,然后让Users控制器也保有Model的一个引用:

//the Users controller will make sure that the User model is included on the page and available to our app
Ext.define('AM.controller.Users', {
extend: 'Ext.app.Controller',
stores: ['Users'],
models: ['User'],
...
});

// we now reference the Model instead of defining fields inline
Ext.define('AM.store.Users', {
extend: 'Ext.data.Store',
model: 'AM.model.User',

data: [
{name: 'Ed',    email: 'ed@sencha.com'},
{name: 'Tommy', email: 'tommy@sencha.com'}
]
});


目前的重构将会使下面的内容更简单,但是还不会影响应用程序现在的表现。如果我们重新加载页面并且双击grid的一行,用户编辑窗依然会呈现。现在是时候结束编辑功能了:



8. 使用Model保存数据

现在我们已经实现用户表格加载数据,并且当双击每一行时出现编辑窗,我们想保存用户数据的变化。用户编辑窗包含一个form(包括name和email字段)和一个保存按钮。首先我们更新Users控制器的init方法,让它监听保存按钮的点击事件:

Ext.define('AM.controller.Users', {
init: function() {
this.control({
'viewport > userlist': {
itemdblclick: this.editUser
},
'useredit button[action=save]': {
click: this.updateUser
}
});
},

updateUser: function(button) {
console.log('clicked the Save button');
}
});


我们给control方法添加第二个ComponentQuery选择器——这次叫‘useredit button[action=save]'。它跟第一个选择器的工作方式一样。它使用上边定义的’useredit‘ xtype定位到用户编辑窗,然后查找这个窗口下所有action为’save‘的按钮。当我们定义用户编辑窗是我们给保存按钮传入了{action : 'save'},这样就能让我们更容易的定位到这个按钮。

当我们点击保存按钮时,updateUser函数就会被调用:



现在我们已经看到了我们的处理器正确的绑定到了保存按钮的点击事件上,让我们给updateUser填充真正的逻辑吧。在这个函数中我们需要拿到form的数据,用它来更新用户的数据,然后再把我们创建的数据保存会Store中。看看怎么做:

updateUser: function(button) {
var win    = button.up('window'),
form   = win.down('form'),
record = form.getRecord(),
values = form.getValues();

record.set(values);
win.close();
}


我们分析一下这里都发生了什么。点击事件提供了一个用户点击的按钮的引用,但是我们真正想访问的是包含数据的form和编辑窗。为了让应用工作的更快我们这里又使用了ComponentQuery,首先使用button.up('window')得到用户编辑窗的一个引用,之后使用win.down('form')得到form。

之后我们就得到了加载到form中的数据记录,让后更新它。最后我们关闭编辑窗重新回到grid。当我们刷新应用时,改变name字段为’Ed Spencer‘,然后点击保存:



9. 保存到服务器

够简单了。现在来做些服务器端交互。目前我们给Users Store硬编码进去了两个用户记录,接下来我们换作从AJAX读取数据:

Ext.define('AM.store.Users', {
extend: 'Ext.data.Store',
model: 'AM.model.User',
autoLoad: true,

proxy: {
type: 'ajax',
url: 'data/users.json',
reader: {
type: 'json',
root: 'users',
successProperty: 'success'
}
}
});


我们移除了’data'属性,使用Proxy代替。Proxy是Ext JS 4中从Store或者Model中加载数据或者保存数据方式。有AJAX,JSON-P和HTML5 localStorage几种Proxy。这里我们简单使用AJAX Proxy,我们让它从‘data/users.json’ url中加载数据。

我们还为Proxy绑定了一个reader。reader主要负责把服务器端的相应解码成Store可以理解的数据格式。这次我们使用JSON reader,并且指定root和successProperty配置(可以看看Json Reader的API文档)。最后我们创建data/users.json,然后把刚刚的数据粘贴进去:

{
success: true,
users: [
{id: 1, name: 'Ed',    email: 'ed@sencha.com'},
{id: 2, name: 'Tommy', email: 'tommy@sencha.com'}
]
}


我们对Store做的唯一改变是设置autoLoad为true,也即Store将会要求它的Proxy立即加载数据。如果我们刷新页面,我们会看到跟之前一样的东西,只是现在我们的应用中没有硬编码了。

我们想做的最后一件事是把变化的数据发送到服务器。在这个例子中我们只是在服务器端使用静态的JSON文件,所以你不会看到任何数据库的变化,但是你能确认一切都被真确组装在一起了。首先对Proxy做个小变化,让它发送更新的数据到另一个url:

proxy: {
type: 'ajax',
api: {
read: 'data/users.json',
update: 'data/updateUsers.json'
},
reader: {
type: 'json',
root: 'users',
successProperty: 'success'
}
}


我们仍然从users.json读取数据,但是任何更新都会被发送到updateUsers.json。仅仅是为了返回一个假的相应,以便我们知道正常工作了。updateUsers.json只包含{"success" : true}。另一个改变是编辑后让Store同步它自己,就是通过在updateUser函数中添加一行:

updateUser: function(button) {
var win    = button.up('window'),
form   = win.down('form'),
record = form.getRecord(),
values = form.getValues();

record.set(values);
win.close();
this.getUsersStore().sync();
}


现在我们可以运行整个例子,并且确保一切工作正常。我们将会编辑一行,点击保存按钮,看看发送到updateUsers.json的请求是否正确



10. 部署

新引入的Sencha SDK Tools(下载)能够让Ext JS 4应用的部署变得很简单。这个工具会一个JSB3(JSBuilder文件格式)的形式生成所有依赖的清单,并且几分钟内就会创建一个你应用程序需要的最小化构建版本。

11. 总结

我们创建了一个管理用户数据,发送更新到服务器的简单应用。从简单出入手,逐步重构代码使它更加简洁更加有组织性。这点上很简单就能给我们的应用添加更多的功能,而不用写凌乱的代码。这个应用程序的整个源码在Ext JS 4 SDK的examples/app/simple文件夹下。

12. 参考资料

http://dev.sencha.com/deploy/ext-4.1.0-gpl/docs/index.html#!/guide/application_architecture

作者:周邦涛(Timen)

Email:zhoubangtao@gmail.com

转载请注明出处: /article/9909405.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: