转载自:http://www.jeeboot.com/archives/1222.html
本章探索 Ext js 中处理数据可用的工具以及服务器和客户端之间的通信。在本章结束时将写一个调用 RESTful 服务的例子。下面是本章的内容:
- 模型
- Schema
- Stores
- 代理
- 过滤和排序
- 做一个基于 RESTful 的小项目
Model(模型)
一个模型包含字段,字段类型,校验,关联和代理。它是通过扩展 Ext.data.Model 类来定义的。
其中类型,校验,关联和代理都是可选的。 当一个字段没有指定类型时,将使用默认类型 ‘auto’。 通常 代理都是设置在 store 中,但是 model 里也可以设置代理。
Field(字段)
Ext.data.field.Field 用于添加模型的属性。 这个属性字段的类型可以是预定义的或自定义类型。下列是可用的预定义类型:
- auto
- boolean
- date
- int
- number
- string
数据转换
默认当你为一个字段指定类型后,这个数据将在保存到该字段之前进行转换。转换的过程由内置的 convert 方法处理。auto 字段类型不具备 convert 方法,所以 auto 类型的字段不会发生转换。
其他所有的字段类型都有 convert 方法。如果你想避免其他字段转换来提高性能,你可以通过指定各自的转换配置为 null 。如下列 Employee 模型所示。
验证器/校验器
数据模型支持数据的校验。下列是支持的验证器:
- presence: 确保值是存在于一个字段
- format: 能够用一个正则表达式来进行验证
- length: 支持最大和最小长度的验证
- exclusion 和 inclusion: 你可以通过给出一组值来确保字段的值存在或不存在于这组值中。
下列代码示例展示一个 Employee 模型,在模型的字段上使用验证器:
- Ext.define('Employee', {
- extend: 'Ext.data.Model',
- fields: [
- { name: 'id', type: 'int', convert: null },
- { name: 'firstName', type: 'string' },
- { name: 'lastName', type: 'string' },
- { name: 'fulltime', type: 'boolean', defaultValue: true, convert: null },
- { name: 'gender', type: 'string' },
- { name: 'phoneNumber', type: 'string'},],
- validators: {
- firstName: [{
- type: 'presence'
- },{
- type: 'length',
- min: 2
- }],
- lastName:[{
- type: 'presence'
- },{
- type: 'length',
- min: 2
- }],
- phoneNumber: {
- type: 'format',
- matcher: '/^[(+{1})|(00{1})]+([0-9]){7,10}$/'
- },gender: {
- type: 'inclusion',
- list: ['Male', 'Female']
- }
- }
- });
创建一个模型的实例,使用 Ext.create ,如以下代码所示:
- var newEmployee = Ext.create('Employee', {
- id : 1,
- firstName : 'Shiva',
- lastName : 'Kumar',
- fulltime : true,
- gender: 'Male',
- phoneNumber: '123-456-7890'
- });
创建好的模型对象有 get 和 set 方法用来读取和设置字段值:
- var lastName = newEmployee.get('lastName'); newEmployee.set('gender','Female');
关系
定义实体之间的关系,使用下列关系类型之一:
One-to-one(一对一)
下列代码代表一对一关系:
- Ext.define('Address', {
- extend: 'Ext.data.Model',
- fields: [
- 'address',
- 'city',
- 'state',
- 'zipcode']
- });
- Ext.define('Employee', {
- extend: 'Ext.data.Model',
- fields: [{
- name: 'addressId',
- reference: 'Address'
- }]
- });
One-to-many(一对多)
下列代码代表一对多关系:
- Ext.define('Department', {
- extend: 'Ext.data.Model',
- fields: [{
- name: 'employeeId',
- reference: 'Employee'
- }]
- });
- Ext.define('Division', {
- extend: 'Ext.data.Model',
- fields: [{
- name: 'departmentId',
- reference: 'Department'
- }]
- });
Many-to-many(多对多)
下列代码代表多对多关系:
- Ext.define('Employee', {
- extend: 'Ext.data.Model',
- fields: [{
- name: 'empId',
- type: 'int',
- convert: null
- },{
- name: 'firstName',
- type: 'string'
- },{
- name: 'lastName',
- type: 'string'
- }],
- manyToMany: 'Project'
- });
- Ext.define('Project', {
- extend: 'Ext.data.Model',
- fields: [
- 'name'
- ],
- manyToMany: 'Employee'
- });
自定义字段类型
你也可以通过简单的扩展 Ext.data.field.Field 类来创建自定义字段类型。如以下代码所示:
- Ext.define('App.field.Email', {
- extend: 'Ext.data.field.Field',
- alias: 'data.field.email',
- validators: {
- type: 'format',
- matcher: /^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/,
- message: 'Wrong Email Format'
- }
- });
Store
一个 store 代表一个 模型的实例 的集合并用于代理获取数据。store 还定义了集合操作,例如 排序,过滤等等。它是通过扩展 Ext.data.Store 定义的。
通常,当定义一个 store ,你需要为 store 指定一个 代理。这是一个配置属性,它告诉 store 如何读取和写入数据。在本章的后面我们将会看到更多详细的不同种类的代理。
下列代码是一个 store 的例子,它使用 RESTful API 请求加载为 JSON 格式数据:
- var myStore = Ext.create('Ext.data.Store', {
- model: 'Employee',
- storeId: 'mystore',
- proxy: {
- type: 'rest',
- url: '/employee',
- reader: {
- type: 'json',
- rootProperty: 'data'
- }
- },
- autoLoad: true,
- autoSync: true
- });
这里 storeId 是 store 的唯一标示符。这个 store 有一个方法 load ,用于通过代理配置加载数据。如果你设置 autoLoad 为 true ,那么当 store 创建后将会自动调用 load 方法。如果设置 autoLoad 为 false ,那么你可以手动的调用 load 方法加载数据。
同样的,你可以设置 autoSync 为 true ,当你对 store 的记录进行修改新增和删除时将会自动发生同步。
在前面的例子中,当在 store 中执行修改,新增或删除时,它将调用 REST service API 。如果你设置这个属性为 false ,那么你可以调用 sync 方法执行同步操作。
调用 sync 方法将触发一个批量操作。所以如果你添加和删除了多条记录,那么调用 sync方法这个过程中将触发多次服务器调用。这是一个 sync 调用的例子:
- store.sync({
- callback: function (batch, options) {
-
- },
- success: function (batch, options) {
-
- },
- failure: function (batch, options) {
-
- },
- scope: this
- });
这里,当所有 sync 操作全部完成并且没有任何例外和失败时调用 success 方法。如果有一个或多个操作在 sync 过程中失败,将调用 failure 方法。callback 方法则会在同步操作完成后,不论成功失败都会被调用。
如果 failure 被调用,你可以检查 batch 异常数组来看到什么操作失败,为什么。这里 options 是 sync 方法中传递的原始参数。
这个 sync 方法调用时也可以添加一个属性 params ,它可以用于在同步的时候传递任意你想附加的参数。
内联数据 store
如果你不想绑定 store 到服务器或外部存储,例如浏览器本地存储,但还想使用一些特殊的的静态数据,那么可以直接硬编码内联数据到 store ,如以下代码所示:
- Ext.create('Ext.data.Store', {
- model: 'Employee',
- data: [{
- firstName: 'Shiva',
- lastName: 'Kumar',
- gender: 'Male',
- fulltime: true,
- phoneNumber: '123-456-7890'
- },{
- firstName: 'Vishwa',
- lastName: 'Anand',
- gender: 'Male',
- fulltime: true,
- phoneNumber: '123-456-7890'
- }]
- });
过滤和排序
store 支持本地或远程的过滤和排序。下面是本地排序的例子:
- var myStore = Ext.create('Ext.data.Store', {
- model: 'Employee',
- sorters: [{
- property: 'firstName',
- direction: 'ASC'
- }, {
- property: 'fulltime',
- direction: 'DESC'
- }],
- filters: [{
- property: 'firstName',
- value: /an/
- }]
- });
使用远程排序和远程过滤,需要设置 remoteSort 和 remoteFilter 属性为 true 。如果你设置为 true ,那么在服务器端你必须要执行过滤和排序并为客户端返回过滤或排序后的数据。
访问 store
你也许需要在应用的其他地方访问这个 store 有很多方法可以用。
使用 StoreManager 访问 store
使用 store 管理器的 lokkup 方法,你能够在程序的任何地方来访问 store。为此我们需要使用 storeId ,注意,当 store 通过一个控制器实例化的时设置 storeId,storeId 将会被覆盖,这是一个例子:
- Ext.create('Ext.data.Store', {
- model: 'Employee',
- storeId: 'mystore',
- proxy: {
- type: 'rest',
- url: '/employee',
- reader: {
- type: 'json',
- rootProperty: 'data'
-
- }
- }
- });
假设我们已经创建了 store 如上所示。现在你可以通过传递 storeId 到 store 管理器的 StoreManager.lookup 方法访问这个 store,如以下代码所示:
- Ext.data.StoreManager.lookup('myStore');
你也可以使用 Ext.getStore 方法。Ext.getStore 是 Ext.data.StoreManager.lookup 的快捷方法。
使用 Ext.app.ViewModel 访问 store
你可以通过 Ext.app.ViewModel 的 getStore 方法 访问 store 。当你是在 ViewController 中,最好使用这种方式,下面是代码示例:
- var myStore = this.getViewModel().getStore('myStore')
这个 getStore 方法也定义在 view 中,你也可以用它访问 store。
Store 事件
store 提供了很多可监听的事件。store 的一些常用事件:
- add: 当一条记录添加到 store 后调用
- beforeload: 在加载数据之前调用
- beforesync: 在进行同步操作之前调用
- datachanged: 当 store中的记录产生新增或删除时触发调用
- load: 当 store 读取一个远程数据源后触发调用
- remove: 从 store 移除一条记录触发调用
- update: 当 store 中的一条记录被更新触发调用
给出的一个 store 事件监听的例子:
- Ext.create('Ext.data.Store', {
- model: 'Employee ',
- storeId: 'mystore',
- proxy: {
- type: 'rest',
- url: '/employee',
- reader: {
- type: 'json',
- rootProperty: 'data'
- }
- },
- listeners: {
- load: function (store, records, options) {
-
- }
- }
- });
如果你想在你的控制器中监听 store 事件,你可以这样做:
- init: function() {
- this.getViewModel().getStore('myStore').on('load', this.onStoreLoad, this);
- }
在 ViewModel 中定义 store
你可以分别定义 store 和 ViewModel 或者定义到一起。通常可取的定义 store 在 ViewModel 自身。一个例子如下:
- Ext.define('MyApp.view.employee.EmployeeModel', {
- extend: 'Ext.app.ViewModel',
- alias: 'viewmodel.employee',
- stores: {
- employee: {
- fields: [{
- name: 'id',
- type: 'string'
- },{
- name: 'firstname',
- type: 'string'
- },{
- name: 'lastname',
- type: 'string'
- }],
- autoLoad: false,
- sorters: [{
- property: 'firstname',
- direction: 'ASC'
- }],
- proxy: {
- type: 'rest',
- url: 'employee',
- reader: {
- type: 'json',
- },
- writer: {
- type: 'json'
- }
- }
- }
- }
- });
代理
所有的 stores 和 models 使用 proxy 来加载和保存数据。在代理中使用这个配置你可以指定如何读取和写入数据。你也可以指定调用 URL 读取数据;你可以告诉读取器这些数据的格式,不论是 JSON 还是 XML ,等等。
有两种类型的代理:客户端代理和服务器代理。
客户端代理
客户端代理是用于处理客户端本身数据的加载和保存。这氛围三种客户端代理:内存,本地存储,和会话存储。
内存代理
内存代理是用于内存中的局部变量数据。下列代码展示了一个内存代理的例子。在这里这个数据的值没有硬编码。只要是合适的格式,可以是任何变量的数据:
- var data = {
- data: [{
- firstName: 'Shiva',
- lastName: 'Kumar',
- gender: 'Male',
- fulltime: true,
- phoneNumber: '123-456-7890'
- },{
- firstName: 'Vishwa',
- lastName: 'Anand',
- gender: 'Male',
- fulltime: true,
- phoneNumber: '123-456-7890'
- }]
- };
- var myStore = Ext.create('Ext.data.Store', {
- model: 'Employee',
- data : data,
- proxy: {
- type: 'memory',
- reader: {
- type: 'json',
- rootProperty: 'Employee'
-
- }
- }
- });
本地存储代理
这个是用于访问浏览器本地存储。它是一个键值对存储添加在 HTML5 ,所以需要浏览器支持:
- var myStore = Ext.create('Ext.data.Store', {
- model: 'Benefits',
- autoLoad: true,
- proxy: {
- type: 'localstorage',
- id: 'benefits'
- }
- });
会话存储代理
这个是用于访问浏览器会话存储。这也是一个 html5 特性,因此需要比较现代的浏览器才能支持。这些数据是当 session 超时后会被销毁:
- var myStore = Ext.create('Ext.data.Store', {
- model: 'Benefits',
- autoLoad: true,
- proxy: {
- type: 'localstorage',
- id : 'benefits'
- }
- });
服务器端代理
服务器端代理是向服务器通信来读取或保存数据。有四种代理:
- Ajax: 用于异步请求数据
- Direct: 使用 Direct 与服务器通信
- JSONP (JSON with padding): 这很有用,当你需要发送跨域请求时。而 ajax 只能请求相同的域。
- REST: 这会向服务器发送一个 ajax 请求,使用 RESTful 的风格,例如 GET,POST,PUT,和 DELETE。
在本章我们已经看到过一个 REST 代理的例子。让我们瞧一瞧 JSONP 的例子:
- var myStore = Ext.create('Ext.data.Store', {
- model: 'Products',
- proxy: {
- type: 'jsonp',
- url : 'http://domain.com/products',
- callbackKey: 'productsCallback'
- }
- });
To do 待办(RESTful 的示例项目)
现在,让我们运用本章和前面章节所学内容,创建一个 To do 待办 应用。这里我们将使用 store 的 REST 代理来连接 REST 服务。
我们来创建一个简单的 RESTful 服务,我们将使用 Go 语言,也称为 Golang 。它由 Google 开发,是静态语言,语法松散。
你不必学习 go 语言,这个项目主要集中在 Ext JS 的教学。你可以用任意的你熟悉的语言替换 Go 语言开发的 RESTful 服务代码。
这是我们将要创建的应用的设计:

让我们瞧一瞧这个项目的一些重要文件。完整的代码在这里 https://github.com/ananddayalan/extjs-by-example-todo。
下面截图展示了这个程序的目录结构:

首先,让我们先来创建 store ,我们已经在本章学习过了。这里我将 store 写在 ViewModel 中。
下列 ViewModel中有三个字段:id ,desc(描述) ,done(显示待办是否完成),代理类型为 rest 并设置 URL 地址为 tasks 。因为是 REST 代理,它将会根据不同的操作访问相应的 HTTP 服务。
例如,当你删除一条记录,这个服务请求 URL 将是 <base URL>/task/{id} 。如果你的应用部署在 localhost 端口为 9001 ,那么请求 URL 就是 http://localhost:9001/tasks/23333 ,并且 HTTP verb 是 DELETE ,你可以理解为提交的 HTTP 动作类型。这里 23333 是记录的ID。当你添加一条记录,URL 就是 http://localhost:9001/tasks ,这时 HTTP verb 就是 POST,同时要添加的记录也会以 JSON 形式发送给服务器:
- Ext.define('ToDo.view.toDoList.ToDoListModel', {
- extend: 'Ext.app.ViewModel',
- alias: 'viewmodel.todoList',
- stores: {
- todos: {
- fields: [ {
- name: 'id',
- type: 'string'
- },{
- name: 'desc',
- type: 'string'
- }],
- autoLoad: true,
- sorters: [{
- property: 'done',
- direction: 'ASC'
- }],
- proxy: {
- type: 'rest',
- url: 'tasks',
- reader: {
- type: 'json',
- },
- writer: {
- type: 'json'
- }
- }
- }
- }
- });
现在我们来创建视图,我们将创建一个 view 作为 To do 待办 列表,一个文本框用于键入新的待办,和一个添加按钮。
这个 To do 待办 列表的 UI 是基于 store 中的记录,动态的创建的,并且每当一个记录添加或移除,这个 view 都会相应的更新。Ext JS grid 组件可以实现我们的目的,但是你还没有学习过这个组件,所以这里我们不使用它来创建,而通过其他方式,你还将学习到如何处理自定义 UI。
动态创建 待办列表 UI,我把代码写在 ViewController 里。在这个 view 中,我添加了 文本框 和 按钮:
- Ext.define('ToDo.view.toDoList.ToDoList', {
- extend: 'Ext.panel.Panel',
-
-
- requires: [
- 'ToDo.view.toDoList.ToDoListController',
- 'ToDo.view.toDoList.ToDoListModel'
- ],
- xtype: 'app-todoList',
- controller: 'todoList',
-
- viewModel: {
- type: 'todoList'
- },
- items: [{
- xype: 'container',
- items: [{
- xtype: 'container',
- layout: 'hbox',
- cls: 'task-entry-panel',
- defaults: {
- flex: 1
- },
- items: [{
- reference: 'newToDo',
- xtype: 'textfield',
- emptyText: 'Enter a new todo here'
- },{
- xtype: 'button',
- name: 'addNewToDo',
- cls: 'btn-orange',
- text: 'Add',
- maxWidth: 50,
- handler: 'onAddToDo'
- }]
- }]
- }]
- });
上面的 view 代码里,我指定了两个 cls 属性分别是 btn-orange 和 task-entry-panel 。这是 CSS 类,用于添加一些 CSS 样式。在上面的 UI 截图中,你可以看到 Add 按钮并不是我们所用的主题(crisp)的默认颜色。因为我指定了 CSS 类来自定义这个按钮了。
现在我们可以创建 ViewController 。我们将通过在初始化函数中读取 store 中的记录来创建 待办列表 的UI 。这里,一旦应用加载,我们将调用 store 的 load 方法。这将使得 rest 服务调用,从服务器获取记录,并遍历结果,每一条记录我们都会创建一行在 UI 中。
提供删除功能,我会在每一条记录上添加一个删除图标。下列代码用于绑定这个图标的点击事件。待办记录 UI 是动态生成的,所以我们需要事件委派,如以下代码所示:
- Ext.getBody().on('click', function (event, target) {
- me.onDelete(event, target);
- } , null, {delegate: '.fa-times' });
如果 UI 不是动态添加的,正常的绑定点击事件,如以下代码:
- Ext.select('fa-times').on('click', function (event, target) {
- me.onDelete(event, target);
- });
以下是视图待办列表(ToDoList) 的视图控制器(ViewController)的代码。 onAddToDo 方法是添加按钮的点击事件处理,方法里通过 lookupReference 方法传递引用名称设置到 ToDoList 视图:
- Ext.define('ToDo.view.toDoList.ToDoListController', {
- extend: 'Ext.app.ViewController',
- alias: 'controller.todoList',
- views: ['ToDo.view.toDoList.ToDoList'],
- init: function () {
- var me = this;
-
- this.getViewModel().data.todos.load(function (records) {
- Ext.each(records, function (record) {
-
- me.addToDoToView(record);
- });
-
- });
-
- Ext.getBody().on('click', function (event, target) {
- me.onDelete(event, target);
- }, null, {
- delegate: '.fa-times'
- });
- },
- onAddToDo: function () {
- var store = this.getViewModel().data.todos;
-
- var desc = this.lookupReference('newToDo').value.trim();
- if (desc != '') {
- store.add({
- desc: desc
- });
- store.sync({
- success: function (batch, options) {
- this.lookupReference('newToDo').setValue('');
- this.addToDoToView(options.operations.create[0]);
- },
- scope: this
- });
- }
- },
- addToDoToView: function (record) {
- this.view.add([{
- xtype: 'container',
- layout: 'hbox',
- cls: 'row',
- items: [{
- xtype: 'checkbox',
- boxLabel: record.get('desc'),
- checked: record.get('done'),
- flex: 1
- },{
- html: '<a class="hidden" href="#"><i taskId="' + record.get('id') + '" class="fa fa-times"></i></a>',
- }]
- }]);
- },
- onDelete: function (event, target) {
- var store = this.getViewModel().data.todos;
- var targetCmp = Ext.get(target);
- var id = targetCmp.getAttribute('taskId');
- store.remove(store.getById(id));
- store.sync({
- success: function () {
- this.view.remove(targetCmp.up('.row').id)
- },
- scope: this
- });
- }
- });
最后,让我们来创建 REST 服务。Go 语言可以安装在 mac OS,Windows,Linux 等等。这里下载 https://golang.org.
Go 语言安装完成之后,你需要设置 GOROOT 环境变量为 Go 语言的安装目录。linux 应该添加下列命令到 $HOME/.profile:
- export GOROOT=/usr/local/go export PATH=$PATH:$GOROOT/bin
针对本项目,我将使用一个名为 Gorilla 的路由模块,安装这个模块使用以下命令:
- go get github.com/gorilla/mux
以下是 REST 服务的代码。
- 这段代码不会存数据到数据库里,所有的数据都是在内存中,当你关闭程序,数据将被销毁。
- package main
- import ( "fmt"
- "encoding/json"
- "net/http" "strconv"
- "github.com/gorilla/mux"
- )
- type Task struct {
- Id string `json:"id"`
- Desc string `json:"desc"`
- Done bool `json:"done"`
- }
- var tasks map[string] *Task
- func GetToDo(rw http.ResponseWriter, req * http.Request) {
- vars := mux.Vars(req)
- task := tasks[vars["id"]]
- js, err := json.Marshal(task)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- fmt.Fprint(rw, string(js))
- }
-
- func UpdateToDo(rw http.ResponseWriter, req * http.Request) {
- vars := mux.Vars(req)
- task:= tasks[vars["id"]]
- dec:= json.NewDecoder(req.Body)
- err:= dec.Decode( & task)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- task.Id = vars["id"]
- retjs, err:= json.Marshal(task)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- fmt.Fprint(rw, string(retjs))
- }
-
- func DeleteToDo(rw http.ResponseWriter, req * http.Request) {
- vars := mux.Vars(req)
- delete(tasks, vars["id"])
- fmt.Fprint(rw, "{status : 'success'}")
- }
-
- func AddToDo(rw http.ResponseWriter, req * http.Request) {
- task:= new(Task)
- dec:= json.NewDecoder(req.Body)
- err:= dec.Decode( & task)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- tasks[task.Id] = task
- retjs, err:= json.Marshal(task)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- fmt.Fprint(rw, string(retjs))
- }
-
- func GetToDos(rw http.ResponseWriter, req * http.Request) {
- v := make([]*Task, 0, len(tasks))
- for _, value := range tasks {
- v = append(v, value)
- }
- js, err:= json.Marshal(v)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- fmt.Fprint(rw, string(js))
- }
-
- func main() {
- var port = 9001
- router:= mux.NewRouter()
- tasks = make(map[string] *Task)
- router.HandleFunc("/tasks", GetToDos).Methods("GET")
- router.HandleFunc("/tasks", AddToDo).Methods("POST")
- router.HandleFunc("/tasks/{id}", GetToDo).Methods("GET")
- router.HandleFunc("/tasks/{id}", UpdateToDo).Methods("PUT")
- router.HandleFunc("/tasks/{id}", DeleteToDo).Methods("DELETE")
- router.PathPrefix("/").Handler(http.FileServer(http.Dir("../")))
- fmt.Println("Listening on port", port)
- http.ListenAndServe("localhost:" + strconv.Itoa(port), router)
- }
使用下列命令运行服务:
如果没有报错,应该显示类似于以下代码:
现在你可以访问 localhost:9001 查看应用了:

完整的代码在这里 https://github.com/ananddayalan/extjs-byexample-todo.
总结
在本章中,你学习了如何创建 model,store,代理和如何处理数据。同时也看到如何使用 Go 语言创建一个简单的 REST 服务。