我们之前在之前的 Ext JS Architecture介绍的Ext JS 6的应用程序的结构,简单介绍了MVC, 和MVVM两种体系,这里包括Controller, Model, View, ViewModel, ViewController等相关的知识。因为Ext JS 6中,同样支持MVC结构,而在之前的文章中,并没有详细的讲到MVC, 更多的是新特性MVVM结构,本指南将详细的讲解MVC, Ext JS 4只有此结构,所以在Ext JS 4的文档中找到此文章.
当前Ext JS 6中的Ext.app.Application中,关于application architecture的链接是错误的,它应该是此链接,在此纠正一下 http://docs.sencha.com/extjs/4.1.3/#!/guide/application_architecture
大型的客户端程序都是很难写,难以组织和维护。随着功能和开发人员的增加,更加难以控制。Ext JS 4提供了一个新的应用程序架构,不仅能够更好的组织你的代码,还能减少代码量。
我们的应用程序架构遵循 MVC-like模式,首次引入了Controller和Models. 当前有许多的MVC结构,其中大部分的定义都有略微的不同,以下是我们对MVC的定义:
在本指南中,我们将创建一个非常简单的应用,来管理用户数据。
应用程序架构主要是为实际的类和框架代码提供结构,并且保持代码的一致性,它有以下作用:
Ext JS 4应用遵循统一的目录结构,在MVC结构中,所有的类都存放在app/目录下. 如下图所示
在这个例子中,我们将整个应程程序封装在’account_manager’目录中。 所需要的Ext JS 4 SDK文件,则保存在ext-4/目录下。因此index.html文件的内容如下
<html>
<head>
<title>Account Managertitle>
<link rel="stylesheet" type="text/css" href="ext-4/resources/css/ext-all.css">
<script type="text/javascript" src="ext-4/ext-debug.js">script>
<script type="text/javascript" src="app.js">script>
head>
<body>body>
html>
Ext JS 应用通常是使用Ext.container.Viewport创建的单页面应用。
一个应用由一个或者多个Views组件,一个view的行为由相应的Ext.app.ViewController和Ext.app.ViewModel进行管理。
每一个 Ext JS 4 application都是从 Applcation类的实例开始, Application类包含你应用的全局设置(比如app的name, 创建一个全局的命名空间), 还包含app中要使用到的其它类的引用,比如models, views, controllers. 一个Application还包含一个launch 函数, 它在整个页面都加载完成时,自动运行
让我们创建一个简单的Account Manager app,用来帮助我们管理用户帐号。首先我们需要为application选择一个namespace. 所有的Ext JS 4应用都应该使用单个的全局变量,这样,application中的所有类都在这个全局变量之下。
通常我们需要一个简短的全局变量,我们在这里使用 “AM”
Ext.application({
requires: ['Ext.container.Viewport'],
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’. 它会为我们自动创建一个全局变量AM, 并且在Ext.Loader中注册这个namespace. 并且通过appFolder设置与namespace相应的文件夹路径。我们也简单的提供了一个launch函数,它仅创建一个 Viewport, Viewport创建好后,可以自动的渲染到doucment body。 一个页面,只能创建一个Viewport.
Controller相当于应用程序中的胶水,将应用程序中的元素(views, models)绑定到一起. 它真正做的是,监听事件(监听views中的事件),并做出相应的处理。接下来,我们创建一个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配置中, 修改 app.js的以下代码:
Ext.application({
...
controllers: [
'Users'
],
...
});
当我们在浏览器中查看index.html, Users controller会自动的被加载(因为我们在application中有指定它), 并且它的init函数在application的launch之前被调用.
init函数是设置你的控制器与view交互的一个好地方,在init函数中,经常使用this.control函数,用来监听view类的事件,并作出相应的行为。接下来,我们更新Users controller的代码,告诉我们,panel什么时候被渲染
Ext.define("AM.controller.Users", {
extend: "Ext.app.Controller",
init: function(){
console.log("'Initialized Users! This happens before the Application launch function is called ")
this.control({
'viewport > panel': { //use Ext.ComponentQuery class
render: this.onPanelRendered
}
})
},
onPanelRendered: function(){
console.log('The panel was render');
}
})
我们使用this.control来设置application中视图的监听器。control函数使用新的ComponentQuery 引擎来快速获取一个页面中组件的引用(xtype)。如果你还不熟悉ComponentQuery, 可以查看Component Query文档, 简单来说,它允许我们使用类似于CSS选择器,来匹配一个页面的组件(不是html节点, 而是Ext JS的Component, 可以是xtype, ID)
在我们的init函数中,我们使用了’viewport > panel’,它表示”viewport 下的直接Panel组件, 它返回一个数组,包含每个组件的引用”. 然后我们指定一个对像,对像中的键映射为匹配到组件的事件名称,它的值映射为每个事件的处理函数。即,为所有匹配到的组件,注册事件监听函数。
##Defining a View
接下来,我们想添加一个grid,用来显示我们系统中的users.
其实View就是一个组件,它是Ext JS Component的一个子类。我们创建一个app/view/user/List.js文件,使用一个grid来显示用户列表
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: '[email protected]'},
{name: 'Tommy', email: '[email protected]'}
]
};
this.columns = [
{header: 'Name', dataIndex: 'name', flex: 1},
{header: 'Email', dataIndex: 'email', flex: 1}
];
this.callParent(arguments);
}
});
我们的View Class就是一个普通的类。在这里,我们让它继承于Grid 组件,并且设置它的别名,这样我们就可以用xtype的方式来使用它。我们也配置了一个store和columns, 这两个配置是grid渲染时,需要使用到的数据
接下来,我们将view添加到User controller中。 因为我们设置别名时,使用了特殊的’widget.’格式,所以可以使用’userlist’作为xtype. 类似于之前使用的’panel’
接下来,我们需要在Users controller中添加这个view, 否则没有办法在app.js中使用 xtype对它渲染。
Ext.define('AM.controller.Users', {
extend: 'Ext.app.Controller',
views: [
'user.List'
],
init: ...
onPanelRendered: ...
});
然后,我们可以在main viewport中,对它进行浸染
Ext.application({
...
launch: function() {
Ext.create('Ext.container.Viewport', {
layout: 'fit',
items: {
xtype: 'userlist'
}
});
}
});
需要注意的一个事件是,我们在views数组中,指定了’user.List’, 它是告诉应用程序自动加载这些文件,这样我们才能在application launch中使用这些组件。application使用了Ext JS 4中新的动态加载系统,可以自动从服务器上加载。
当前onPanelRendered函数依然可以执行,这是因为grid类 继承于 Panel类,符合’viewport > panel’选择器。
所以我们需要使用一个新的xtype,用来缩窄选择的范围。我们可以添加以下代码,用来监听grid中每一行的双击事件
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'));
}
});
效果如下图所示
接下来,我们创建一个app/view/user/Edit.js,用来真真的编辑用户信息,而不是简单的console log
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)的一个子类. 布局使用 ‘fit’,它适用于一个组件的容器,它的子组件,随着容器大小的改变而改变。
接下来,我们在controller中添加新创建的view. 并且渲染它
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创建了一个视图。它等于 Ext.create(‘widget.useredit’); 然后,我们利用了ComponentQuery,来查找form 组件。每一个Component都有一个down函数,它接收一个ComponentQuery选择器,可以快迅的查找一个子组件。
当前我们的是以内联的方法创建的store. 虽然这样也可以,但我们想要在应用程序的任何地方引用这个store, 并更新它的数据,那就需要将它提取出来,存放到app/store/Users.js中
Ext.define('AM.store.Users', {
extend: 'Ext.data.Store',
fields: ['name', 'email'],
data: [
{name: 'Ed', email: '[email protected]'},
{name: 'Tommy', email: '[email protected]'}
]
});
然后修改controller,
Ext.define('AM.controller.Users', {
extend: 'Ext.app.Controller',
stores: [
'Users'
],
...
});
然后,我们就可以在app/view/user/List.js中,通过id来引用这个store.
Ext.define('AM.view.user.List' ,{
extend: 'Ext.grid.Panel',
alias: 'widget.userlist',
title: 'All Users',
//我们不需要initComponent中定义User store.
store: 'Users',
initComponent: function() {
this.columns = [
...
});
我们在Users controller中包含stores,是用来告诉应用程序自动加载这个store文件,并且根据store类的定义,给定一个storeid, 这样就可以在view中,通过id引用相应的store.
当前我们只在store中定义了’name’和’email’字段。虽然这也可以满足我们的需求,但在Ext JS 4中,我们有一个更强大的Ext.data.Model类
Ext.define('AM.model.User', {
extend: 'Ext.data.Model',
fields: ['name', 'email']
});
接下来,我们替换store中的内联field
Ext.define('AM.store.Users', {
extend: 'Ext.data.Store',
model: 'AM.model.User',
data: [
{name: 'Ed', email: '[email protected]'},
{name: 'Tommy', email: '[email protected]'}
]
});
然后,我们也需要告诉User controller获得User model的引用
Ext.define('AM.controller.Users', {
extend: 'Ext.app.Controller',
stores: ['Users'],
models: ['User'],
...
});
首先让我们监听save button
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');
}
...
});
现在我们可以看到,点击save后,将日志出输到控制台,接下来,我们更新User
updateUser: function(button) {
var win = button.up('window'),
form = win.down('form'),
record = form.getRecord(),
values = form.getValues();
record.set(values);
win.close();
}
Client 事件给了我们一个button的引用,但我们实际上是需要访问 form 组件(它包含数据) 和window 组件。所以我们再次使用了ComponentQuery,先通过button.up(‘window’)获得Edit User window, 然后通过win.down(‘form’)获得 form.
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代替。 Proixs可以用于store和model中,用来加载和保存数据,类型有AJAX, JSON-P 以及HTML5 localStorage. 在这里,我们使用 AJAX代理,加载服务器的’data/users.json’.
我们在代理中定义了一个Reader, 它用于解析服务器响应到Store可以理解的格式。这里,我们使用了JSON Reader, 并且指定了JSON中数据保存的位置root, 以及successProperty. 以下是
data/user.json文件
{
"success": true,
"users": [
{"id": 1, "name": 'Ed', "email": "[email protected]"},
{"id": 2, "name": 'Tommy', "email": "[email protected]"}
]
}
在store中,我们还有一个autoLoad=true, 它的意思是Store将要求Proxy立即加载数据。如果我们刷新网页,将看到跟之前的效果是一样的。但现在我们却可以不用将数据硬编码在application。
接下来,我们需要将改变的数据返回到服务器。
proxy: {
type: 'ajax',
api: {
read: 'data/users.json',
update: 'data/updateUsers.json'
},
reader: {
type: 'json',
root: 'users',
successProperty: 'success'
}
}
我们依然从user.json中读取数据,但是任何的更新,都将发送到updateUser.json.
接下来,我们需要告诉Store 跟服务器同步
updateUser: function(button) {
var win = button.up('window'),
form = win.down('form'),
record = form.getRecord(),
values = form.getValues();
record.set(values);
win.close();
// synchronize the store after editing the record
this.getUsersStore().sync();
}