应用程序架构介绍-Introduction to Application Architecture
Ext JS 提供对 MVC 和 MVVM 应用架构的支持。两种架构方法都是按照逻辑划分应用层次,每种方法在划分应用层次方面又有各自的优势。下面介绍一下基础知识。
MVC
在 MVC 架构中,主要的类是:实体模型(Model)、视图(Views)和控制器(Controllers)。用户于视图进行交互,视图用于显示模型中的数据。控制器监控交互操作,根据需要进行实体模型和视图的变更以响应用户的交互。
由于控制器主要负责变更,所以视图和实体模型之间不能相互感知变化。总的来说,在 MVC 架构中,控制器包含了主要的逻辑。根据需要视图也可以包含一点逻辑。实体模型主要是数据的接口,包括管理数据变更的逻辑。
MVC 架构的目标是清晰定义应用中各个类的职责。职责清晰有助于将这些类从复杂的环境中分离出来,方便应用的测试、维护和代码重用。
MVVM
MVC 与 MVVM 的主要差异是 MVVM 抽象出一个视图实体模型(View Model)。视图实体模型利用数据绑定(Data Binding)技术,协调实体模型数据和视图展示间的变更。
这种架构方法使得实体模型和框架可以实现尽可能多的逻辑,减少或者消除直接维护视图的逻辑。
MVC & MVVM
在Ext JS框架下,如何选择更合适的应用架构,我们需要进一步了解两种架构中每个角色的差异:
(M) Model- 应用的数据。定义数据结构的一系列实体模型(Models)(例如: user 实体包括 user-name 和 password 等字段)。实体模型通过数据包(data package)持久化数据,可以通过关联关系(Association)链接到其他的实体模型。
实体模型通常与 store 一起使用,为 grid 等组件提供数据。实体模型也可以负责一些数据逻辑(领域逻辑),例如:数据合法性验证、数据转换等。
(V) View- 可视化展示的组件。例如:grids、trees 和 panels 等。
(C) Controller- 维护视图的逻辑。负责视图渲染、路由、实体模型实例化以及其他应用逻辑。
(VM) ViewModel- 管理与视图相关的数据。负责维护使用该视图实体模型的组件的数据绑定,以及当数据记录发生变更时组件的更新。
这些应用架构为应用提供了清晰的结构和一致性。遵循这些约定可以获得如下收益:
每个应用以同样的方式运行,只需要学习一次;
可以方便不同应用间的代码共享;
可以使用 Sencha Cmd 优化应用的发布版本。
构建一个简单的应用
下载和安装 Sencha CMD 以及 Ext JS,使用如下命令创建一个应用:
sencha -sdk local/path/to/ExtJS generate app MyApp ./MyApp
cd MyApp
sencha app watch
应用程序简介-Application Overview
文件结构-File Structure
Ext JS 应用程序包含统一的目录结构。建议将所有的 Store 、 Model 、 ViewModel 和 ViewController 分别放置在 app
文件夹下的对应目录中(store
、实体模型-model
、视图实体模型和控制器view
)。 建议将 VIewController 和 ViewModel 根据逻辑进行划分,分别放置在 app/view/
下的不同的子目录中,文件夹也以逻辑功能命名。例如:app/view/main/
和 classic/src/view/main/
目录)。
命名空间-Namespace
每个类的第一行是定位该类的地址。这个地址称为命名空间,其格式为:
<AppName>.<foldername>.<ClassAndFileName>
在上述示例中, "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" shared between toolkits would be located at:
app/view/main/MainController.js
应用程序-Application
我们看看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
文件的方式,应用的所有元数据(meta-data)都可以集中维护在同一个地方。
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
通知应用程序创建指定的视图,并且渲染到文档对象(document 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)包括了应用的全局配置,例如:命名空间、共享 Store等等。当应用程序过期时(浏览器缓存的版本相对于服务器端的最新版本)将调用 onAppUpdate
方法,提示用户重新加载以便获得最新版本。
视图-Views
一个视图就是一个组件,是 Ext.Component
的子类,是应用的可视化部分。
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
。视图还有两个重要的配置属性: controller
和 viewModel
,稍后介绍。
controller 配置
配置属性 controller
用于配置视图的控制器。当通过这种方式设置了视图控制器,控制器就成为事件处理(event handlers)和引用(references)的容器(container),建立起组件和事件处理的对应关系。
viewModel 配置
配置属性 viewModel
用于配置视图的实体模型。视图模型为组件及其子视图提供数据,一般情况下,这些数据绑定到组件,显示和编辑这些数据。
在 "Main" 视图中,tabpanel
的 title
属性绑定到视图模型,这表示 title
属性的值来自于视图模型的 data
的 name
属性值。如果视图模型的 data
方式变化,title
的值自动更新。
控制器-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.js
,设计了一个函数来处理 grid 的选择事件 select event
。由于该视图未指定控制器,所以这个事件处理程序映射到父视图 Main.js
的控制器的 onItemSelected
方法。 不需要额外的处理,控制器就能处理对应的事件。
这意味着可以非常容易地为应用程序添加逻辑。由于控制器与视图具有一对一的关系,唯一需要的就是定义具体的处理方法。
视图控制器用于:
通过 “listeners” 和 “reference” 配置显式地连接到视图;
视图在生命周期中自动管理其关联的控制器。从实例化到销毁,
Ext.app.ViewController
依附于引用它的视图。同一个视图的另一个实例拥有自己的视图控制器实例。当视图被销毁时,其关联的控制器也将被销毁;为嵌套视图提供封装;
视图实体模型-ViewModels
再来看看视图实体模型:
Ext.define('MyApp.view.main.MainModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.main',
data: {
name: 'MyApp',
loremIpsum: 'Lorem ipsum dolor sit amet ...'
}
//TODO - add data, formulas and/or methods to support your view
});
视图实体模型管理数据对象。允许视图绑定该数据,并且在数据发生变化后通知视图。视图实体模型与视图控制器一样,属于引用它的视图。由于视图实体模型关联到一个视图,它也可以连接到组件层次结构中上级组件的视图实体模型。所以允许子视图实体模型简单继承父视图实体模型。
在上述示例的 Main.js
文件中,通过配置属性 viewModel
建立起视图与视图实体模型之间的链接。这个链接以声明式(declarative fashion)定义了数据绑定,自动将实体模型的数据设置到视图上。数据定义在 MainModel.js
文件中,数据可以是来自任何地方的任何数据。可以通过各种代理获取数据(例如 AJAX、 REST 等)。
Models & Stores
Models 和 Stores 构成了应用的信息接口。数据的发送、接收、组织以及模型化(modeled)都是通过这两个类实现的。
Models
Ext.data.Model
表示应用中可持久化的数据类型。每个实体模型包括字段和方法,实现应用数据的模型化。 通常,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'}
]
});
根据上述命名空间的约定,应该使用 User.js
文件存储,并且放置在 app/model/
目录中。
Fields
Ext.data.Model 描述了数据记录包含的值或属性,称为 fields
。 实体模型类通过配置属性 fields
定义这些字段。建议定义字段和字段的数据类型,但不是必须的。如果未定义 fields
配置属性,将自动读取数据并添加到数据对象中。下列情况下,需要定义字段:
- 合法性验证(Validation)
- 缺省值(Default values)
- 转换函数(Convert functions)
Stores
Store 是客户端的数据记录(Model 的实例)缓存。Store 提供了数据的排序、筛选和查询等功能。Store 的示例如下:
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'}
]
});
上述的文件应存储在 Users.js
文件中,放置在 app/store/
目录下。
如果想要全局更新 Store 实例,可以将该 Store 添加到 Application.js
的 stores
配置属性中:
stores: [
'Users'
],