Ext JS 支持 MVC 和 MVVM 这2种应用程序架构. 这两种架构方式共享某些概念,并在逻辑层面划分程序代码. 每一种方式在选择如何分割应用程序上都有各自的优点.
本指南的目的是向您提供有关构成这些架构组件的基础知识.
什么是 MVC?
在 MVC 架构中, 大部分类都是 模型(Models), 视图(Views) 或者 控制器(Controllers). 用户和视图(Views)交互, 展示模型(Models)中的数据. 这些交互是由一个控制器监控, 然后相应这些交互并更新模型(Models)和视图(Views).
视图(View)和模型(Model) 一般互不关心,因为更新它们是控制器(Controllers) 的责任. 一般来说, 在一个 MVC 应用程序中,控制器(Controllers) 包含着应用程序的大部分逻辑. 理想状态下视图(Views)基本没有业务逻辑. 模型(Models) 主要是一个数据接口,只包含和管理数据和表现数据有关的业务逻辑.
MVC 的目标是让个各类分工明确. 在大的环境中,只有每个类都有自己的职责了, 它们才会低耦合. 这可以方便应用程序的测试和维护, 而且代码也可以复用.
什么是 MVVM?
MVC 和 MVVM 的关键区别是,MVVM 为 视图(View) 提供了一个名为 视图控制器(ViewModel) 的抽象. 视图控制器(ViewModel) 协调了模型(Model)的数据 和 视图(View)对数据的展现,使用了一种叫“数据绑定(data binding)”的技术.
结果是,模型(Model)和框架完成尽可能多的工作, 最大限度地减少或消除能够直接操纵视图的应用程序逻辑.
Returning Users
Ext JS 5 引入了 MVVM 架构支持,同时也改进了 MVC 中的 C(控制器). 我们鼓励你去研究和利用这些改进, 需要注意的是, 我们已经尽了一切努力来确保现有的 Ext JS 4 的 MVC 可以不经过修改就能继续正常运行.
MVC 和 MVVM
为了理解你的应用程序适合哪种选择, 我们应该首先进一步了解各种缩写代表什么.
模型(M) Model - 这是应用程序中的数据. 一些类 (称为 “模型”) 定义了各自数据的字段 (比如,User 这个模型包含 user-name 和 password 字段). 模型知道在数据封装过程中如何持久化存储数据,而且可以通过关系(associations)链接到其它模型.
模型(Models)一般和 Store 一起用,为表格(Grids)和其他组件提供数据. 模型(Models)里也是你存放某些业务逻辑的理想位置,比如数据验证(validation), 数据转换(conversion), 等等.
视图(V) View - 视图可以是呈现出来的任意组件. 比如, 表格(grids), 树(trees) 和 面板(panels) 都可以看做视图.
控制器(C) Controller - 控制器是用来放视图的逻辑的. 可能包括渲染视图,路由导航,实例化模型(Models), 或其他应用程序逻辑.
视图控制器(VM) ViewModel - 视图控制器是为某个特定视图(View)管理数据的.
可以将它绑定到相关组件上,并在数据发生变更时,也更新组件的呈现.
这些应用程序架构为你的框架代码提供结构性和一致性。遵循我们建议,可以有一系列重要的好处:
每个应用程序都用相同的规范, 所以你只需要学一次.
各个应用程序之间可以方便共享代码.
你可以用 Sencha Cmd 来为应用程序构建优化过的 production 版本.
构建一个示例应用程序
再继续下去之前, 我们用Sencha Cmd来构建一个示例应用程序. 首先, 下载并解压 Ext JS SDK. 然后, 执行下面的命令行代码:
sencha -sdk local/path/to/ExtJS generate app MyApp MyApp
cd app
sencha app watch
注意: 如果你不知道上面发生了什么, 请查看 快速入门.
应用程序概览
再继续讨论 MVC, MVVM, MVC+VM 模式的组成之前, 我们先来看下 Cmd 生成的应用程序的结构.
文件结构
每一个 Ext JS 应用程序都有着同样的目录结构. 我们推荐, 所有 Store, Model, ViewModel, 和 ViewController 的类都放在app
文件夹 (_然后 Models 放在model
文件夹, Stores 放在store
文件夹, ViewModels和Controllers 放在 view
文件夹_). 最好的方式是把 ViewControllers 和 ViewModels 一起放在 app/view/
目录下的一个子文件夹内,就像 视图(views)类一样 (请看下图的“app/view/main/” 和 “classic/src/view/main/” 文件夹).
命名空间(Namespace)
每个类的第一行是一个类似于地址路径的东西. 这个 “地址路径” 叫做命名空间(Namespace). 命名空间的规则是:
<应用程序名>.<文件夹名>.<类名和文件名>
在这个示例中, “MyApp”是;应用程序名, “view” 是文件夹名, “main”是子文件夹名, “Main” 是类名和文件名. 根据这些信息, 框架就会去找下面路径下的 Main.js
文件:
// Classic
classic/src/view/main/Main.js
// Modern
modern/src/view/main/Main.js
// Core
// "MyApp.view.main.MainController" 类被 Modern 和 Classic 共享,则位于:
app/view/main/MainController.js
如果文件未找到, Ext JS 会抛出异常.
应用程序
让我们来观察一下 index.html
.
<!DOCTYPE HTML>
<html manifest="">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="UTF-8">
<title>MyApp</title>
<script type="text/javascript">
var Ext = Ext || {}; // Ext namespace won't be defined yet...
// This function is called by the Microloader after it has performed basic
// device detection. The results are provided in the "tags" object. You can
// use these tags here or even add custom tags. These can be used by platform
// filters in your manifest or by platformConfig expressions in your app.
//
Ext.beforeLoad = function (tags) {
var s = location.search, // the query string (ex "?foo=1&bar")
profile;
// For testing look for "?classic" or "?modern" in the URL to override
// device detection default.
//
if (s.match(/\bclassic\b/)) {
profile = 'classic';
}
else if (s.match(/\bmodern\b/)) {
profile = 'modern';
}
else {
profile = tags.desktop ? 'classic' : 'modern';
//profile = tags.phone ? 'modern' : 'classic';
}
Ext.manifest = profile; // this name must match a build profile name
// This function is called once the manifest is available but before
// any data is pulled from it.
//
//return function (manifest) {
// peek at / modify the manifest object
//};
};
</script>
<!-- The line below must be kept intact for Sencha Cmd to build your application -->
<script id="microloader" type="text/javascript" src="bootstrap.js"></script>
</head>
<body></body>
</html>
Ext JS 使用一个叫 微加载器(Microloader)的东西来加载应用程序app.json
文件中描述的资源. 然后把这些资源添加到 index.html
. 有了 app.json
,所有的应用程序元数据信息都存在于这个文件中.
于是Sencha Cmd才可以简单高效地编译你的应用程序.
app.json
含有大量注释,提供了关于如何配置的一些信息.
更多有关 beforeLoad
部分,还有特定平台构建的内容,请看 多平台多屏幕开发.
app.js
在之前创建的应用程序中, 创建了一个类 (Application.js
里) ,并启动了它的一个实例 (app.js
里). 可以看到 app.js
有如下内容:
/*
* This file is generated and updated by Sencha Cmd. You can edit this file as
* needed for your application, but these edits will have to be merged by
* Sencha Cmd when upgrading.
*/
Ext.application({
name: 'MyApp',
extend: 'MyApp.Application',
requires: [
'MyApp.view.main.Main'
],
// The name of the initial view to create. With the classic toolkit this class
// will gain a "viewport" plugin if it does not extend Ext.Viewport. With the
// modern toolkit, the main view will be added to the Viewport.
//
mainView: 'MyApp.view.main.Main'
//-------------------------------------------------------------------------
// Most customizations should be made to MyApp.Application. If you need to
// customize this file, doing so below this section reduces the likelihood
// of merge conflicts when upgrading to new versions of Sencha Cmd.
//-------------------------------------------------------------------------
});
你可以通过指定一个容器类作为 mainView, 来将它作为 视区(Viewport). 上面的例子中, 我们把 MyApp.view.main.Main
(一个标签页面板(TabPanel) 类) 作为 视区(Viewport).
mainView 配置项 命令应用程序创建此视图,然后给它附加上一个插件(plugin)Viewport 插件(Plugin).这样就把视图和 HTML文档body 联系起来了.
Application.js
每个 Ext JS 应用程序都通过 Application 类的实例启动. 这个类本来是用来被app.js
启动的,你也可以实例化它用来测试.
当你用 Sencha Cmd 创建应用程序时,下面的Application.js
内容是自动生成的.
Ext.define('MyApp.Application', {
extend: 'Ext.app.Application',
name: 'MyApp',
stores: [
// TODO: add global / shared stores here
],
launch: function () {
// TODO - Launch the application
},
onAppUpdate: function () {
Ext.Msg.confirm('Application Update', 'This application has an update, reload?',
function (choice) {
if (choice === 'yes') {
window.location.reload();
}
}
);
}
});
Application 类 包含了你的应用程序的全局设置, 比如命名空间, 共享的 stores, 等等. 当你的应用程序版本过时(_浏览器当前缓存的版本 vs 服务器上最新版本_)的时候会调用onAppUpdate
函数. 然后会提示用户是否重新加载应用程序.
视图(Views)
视图就是一个组件(Component), 是 Ext.Component的派生类.
视图构成了应用程序的视觉部分。
打开 classic/src/view/main/Main.js
文件可以看到下面的代码.
Ext.define('MyApp.view.main.Main', {
extend: 'Ext.tab.Panel',
xtype: 'app-main',
requires: [
'Ext.plugin.Viewport',
'Ext.window.MessageBox',
'MyApp.view.main.MainController',
'MyApp.view.main.MainModel',
'MyApp.view.main.List'
],
controller: 'main',
viewModel: 'main',
ui: 'navigation',
tabBarHeaderPosition: 1,
titleRotation: 0,
tabRotation: 0,
header: {
layout: {
align: 'stretchmax'
},
title: {
bind: {
text: '{name}'
},
flex: 0
},
iconCls: 'fa-th-list'
},
tabBar: {
flex: 1,
layout: {
align: 'stretch',
overflowHandler: 'none'
}
},
responsiveConfig: {
tall: {
headerPosition: 'top'
},
wide: {
headerPosition: 'left'
}
},
defaults: {
bodyPadding: 20,
tabConfig: {
plugins: 'responsive',
responsiveConfig: {
wide: {
iconAlign: 'left',
textAlign: 'left'
},
tall: {
iconAlign: 'top',
textAlign: 'center',
width: 120
}
}
}
},
items: [{
title: 'Home',
iconCls: 'fa-home',
// The following grid shares a store with the classic version's grid as well!
items: [{
xtype: 'mainlist'
}]
}, {
title: 'Users',
iconCls: 'fa-user',
bind: {
html: '{loremIpsum}'
}
}, {
title: 'Groups',
iconCls: 'fa-users',
bind: {
html: '{loremIpsum}'
}
}, {
title: 'Settings',
iconCls: 'fa-cog',
bind: {
html: '{loremIpsum}'
}
}]
});
要注意的是这个视图不包含任何应用程序逻辑. 所有视图的逻辑都应该放在 视图控制器(ViewController), 我们将在下一部分介绍.
视图类中2个很有趣的东西就是 controller
和 viewModel
配置项.
第二个就是“List” 视图: classic/src/main/view/List
.
/**
* This view is an example list of people.
*/
Ext.define('MyApp.view.main.List', {
extend: 'Ext.grid.Panel',
xtype: 'mainlist',
requires: [
'MyApp.store.Personnel'
],
title: 'Personnel',
store: {
type: 'personnel'
},
columns: [
{ text: 'Name', dataIndex: 'name' },
{ text: 'Email', dataIndex: 'email', flex: 1 },
{ text: 'Phone', dataIndex: 'phone', flex: 1 }
],
listeners: {
select: 'onItemSelected'
}
});
controller 配置项
controller
配置项用来给视图指定一个视图控制器(ViewController). 当给视图指定了视图控制器(ViewController), 这个视图控制器就用来容纳事件处理函数和控件引用. 这就给了 视图控制器(ViewController)从组件到视图事件的一对一的关系. 我们将在下一部分讨论更多关于控制器的内容.
viewModel 配置项
viewModel
配置项用来给视图指定一个视图模型(ViewModel). 视图模型用来给组件和子视图提供数据. 视图模型中的数据一般通过配置组件的 bind,来绑定需要展现或编辑的数据.
在“Main” 视图中, 你可以看到 tabpanel 标题栏的 title
绑定到了视图模型上. 这意味着 title
会用视图模型管理的数据中的“name” 的值来填充. 如果视图模型的数据发生变化, title
的值也会自动更新. 我们稍后讨论视图模型.
控制器(Controllers)
接下来,我们看看 控制器(Controllers). 上面创建的应用程序自带的视图控制器 MainController.js
代码如下:
Ext.define('MyApp.view.main.MainController', {
extend: 'Ext.app.ViewController',
alias: 'controller.main',
onItemSelected: function (sender, record) {
Ext.Msg.confirm('Confirm', 'Are you sure?', 'onConfirm', this);
},
onConfirm: function (choice) {
if (choice === 'yes') {
//
}
}
});
如果你回头看看 List 这个视图, List.js
, 你会注意到grid
的 select 事件指定了一个处理函数. handler
映射到了一个名为onItemSelected
的函数,这个函数在父视图Main.js
的控制器中定义. 可以看到, 控制器不需要特殊的设置就可以处理这个事件了.
这使得给应用程序添加逻辑变得非常简单. 你要做的就是定义一个onItemSelected
函数,因为你的控制器和视图是一一对应的关系.
点击表格中的一行, 会创建一个消息框(MessageBox). 消息框会调用一个名为 onConfirm
的函数, 这个函数位于的是同一个控制器中.
视图控制器(ViewControllers) 被设计用来:
使用 “listeners” 和 “reference” 配置项使得控制器和视图的关系变得明显.
利用视图控制器来管理视图的生命周期. 从实例化到销毁, Ext.app.ViewController 和引用了它的组件是联系在一起的. 再次创建一个视图实例的话,它将有自己的一个视图控制器实例. 当视图销毁的时候, 相关的视图控制器实例也会被销毁.
提供封装,使得嵌套的视图更加直观.
视图模型(ViewModels)
接下来, 我们看看 视图模型(ViewModels). 打开 app/view/main/MainModel.js
文件可以看到下面的代码:
Ext.define('MyApp.view.main.MainModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.main',
data: {
name: 'MyApp',
loremIpsum: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
}
//TODO - add data, formulas and/or methods to support your view
});
视图模型(ViewModels)是一个管理数据对象的类. view可以绑定到此类的数据上面,而且可以在数据发生变化的时候通知view进行更新. 视图模型, 和视图控制器一样, 只被引用了它的view所拥有. 因为视图模型和view是关联着的, 视图模型可以链接到父视图模型(view的父容器的视图模型). 这样,子视图就可以用从父视图模型“继承”过来的数据.
“Main.js”文件中,我们使用 viewModel 这个配置项,在视图和视图模型之间建立了联系. 这种联系使得 绑定此配置项的setter访问器 能自动把 viewModel 的数据设置到 view 上. 在“MainModel.js” 例子中,数据是内嵌的. 也就是说, 你的数据可能是任何形式,来自任何地方. 数据可能是有任何 数据代理(Proxy) 提供的 (AJAX, REST, 等等).
模型(Models) 和 Stores
模型(Models) 和 Stores 组成了应用程序的信息入口. 你的大部分数据都是被这2个类所发送,获取,组织和“模拟”的.
模型(Models)
Ext.data.Model 表现了任何类型的可持久化数据. 每一个 model 都有 字段(fields) 和 函数,来模拟应用程序的数据结构. 模型(Models)经常和 Stores 一起用. Stores 之后才可以被 表格(grids), 树(trees), 和 图表(charts) 所使用.
我们的例子程序没有内置 model,所以我们来加一个:
Ext.define('MyApp.model.User', {
extend: 'Ext.data.Model',
fields: [
{name: 'name', type: 'string'},
{name: 'age', type: 'int'}
]
});
上面的 命名空间 部分我们提到过, 你应该在“app/model/”目录下创建 User.js
.
字段(Fields)
Ext.data.Model 用来描述带属性和属性值的数据记录的是 “fields”. Model 类使用fields
配置项来声明字段. 在这个例子中, name
字段被声明成了 字符串(string), age 则是 整型(integer). API 文档中还有其他 字段类型 可用.
虽然声明字段和类型有很多好处, 但是这并不是必须的. 如果你没有配置 fields, 数据会自动读取并插入到 data 对象中. 你可以按需定义 fields:
验证
默认值
数据转换函数
我们来创建一个 store,来看看这两者如何一起使用.
Stores
Store 是客户端数据记录(Model 类的实例)缓存 . Stores 提供了数据排序, 过滤 和 检索数据的功能.
这个示例应用程序没有 store, 不要担心,我们来简单定义一个 store,并设置它的 model.
Ext.define('MyApp.store.Users', {
extend: 'Ext.data.Store',
alias: 'store.users',
model: 'MyApp.model.User',
data : [
{firstName: 'Seth', age: '34'},
{firstName: 'Scott', age: '72'},
{firstName: 'Gary', age: '19'},
{firstName: 'Capybara', age: '208'}
]
});
添加上述代码到 app/store/
下的Users.js
文件中.
如果你需要一个全局的 store 实例,你可以把这个 store 添加到 Application.js
的 store配置项中. Application.js
文件的 stores 配置项如下:
stores: [
'Users'
],
在这个例子中, store直接包含了一些静态数据. 大部分实际情况,你需要通过一个设置在 model或 store 中的 数据代理(proxy) 来获取数据. 数据代理(Proxies) 作为数据提供者 和 应用程序之间的数据传输通道.
更多关于models, stores, 和数据提供者(data providers) 的内容请看数据封装.
下一步
我们已经创建了一个强大、有帮助的应用程序Ticket 应用. 这样应用程序有登录/注销会话管理, 数据绑定, 还有使用 MVC+VM 架构的时候会显示“best practice”. 这个示例里面有很多注释,所以基本上各个地方都很清楚.
我们建议你花些时间浏览下Ticket 应用,学习更多关于 MVC+VM 的应用程序架构.