Ext JS Architecture

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/cexo425/article/details/52278629

为了更好的创建代码和组织代码结构,改进团队合作,以及减少代码量。在Ext JS 4引入了MVC模式, 更进一步,在Ext JS 5引入了MVVM模式。

MVC and MVVM模式

首先我们学习一些基本概念

  • Model: fields(字段)和数据的集合. 它被用于存放需要被显示的数据。同我们的组件或代码一起使用。 可以参考 Ext JS Data Package
  • View: 它是与用户进行交互的视觉部分。主要为容器类型的组件 - grids, panels trees
  • Controller: 用于处理View与Model的数据交互,它包含大部分的逻辑。它就是model和view的中间层
  • ViewController: 它是一个controller, 被添加到一个特定的view实例中,并且管理这个view和它的子组件。每次创建一个View, 则ViewController的实例也将被创建。
  • ViewModel: 这个类用来管理数据对像,并且能够让我们将它的数据绑定到view中,在 angular中,称为双向绑定。类似于ViewController, 新创建的view, 也就创建了ViewModel

What is MVC?

在MVC架构中,大部分类为Models, Views 或者Controllers中的一种。 用户跟View进行交互,并且显示Models中保存的数据。View与Models的交互是通过Controller层进行,它负责更新View和Model.

MVC的目的是明确application中每个类的职责。因为每一个类都有明确的责任划分,对于大的应用来说,它拥有更好的解耦。易于测试和维护,代码更具可用性

这里写图片描述

What is MVVM?

MVVM的优点在于数据绑定。通过这种方式,model与框架有了更多的内部交互,因此可以减少操作 view的应用逻辑。尽管名字为Model-View-ViewModel, MVVM模式可能依旧使用了controllers(许多开发都称它为MVC+VM体系)

这里写图片描述

如上图所示,ViewMOdel将数据与form中的文本框进行绑定。

Building a Sample App

在我们继续之前,我们通过Sencha Cmd创建一个样例.

sencha -sdk local/path/to/ExtJS generate app MyApp MyApp
cd app
sencha app watch

Application Overview

在我们讨论MVC, MVVM, MVC+VM模式之前,让我们看看Cmd生成的文件结构

File Structure

Ext js application遵循统一的目录结构,即每个app都有相同的目录结构。我们推荐将Layout, Store, Model, ViewModel以及ViewController的类都放到app目录下(ViewModes/Controllers 放到 app/view), 如下图所示. 最佳的原则是进行逻辑分组,将ViewControllers与ViewModels相关的View保存到app/view的子目录中。如下图的app/view/main以下classic/src/view/main.

这里写图片描述

Namespace

每个类的第一行是一个分类地址,这个”address”称为命名空间,命间的命名为

<AppName>.<foldername>.<ClassAndFileName>

在我们的样例中, app的名称为”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.js将应用程序需要的所在元数据,保存在一个地方。

然后可以通过Sencha Cmd对应用程序进行编译。

通过beforeLoad部分和平台特性,可以参考 Developing for Multiple Environments and Screens guide.

app.js

当我们在之前生成了我们的application, 我们就创建了一个类(in 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 Class)作为我们的窗口。

mainView会让application创建指定的View,并且附上Viewport插件

Application.js

每一个 Ext JS都是从Application Class的一个实例开始, 这个类主要被用于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 Class. onAppUpdate方法是当应用过期时调用(浏览器缓存跟服务器的最新版本不同时). 提示用户重新加载应用。

The views

一个View只不过是一个组件,它是Ext.Component的一个子类。一个View包含了应用程序的视图要素。

如果你打开 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}'
        }
    }]
});

请注意一个view不会包含任何的应用逻辑。所有的应用逻辑应该包含在ViewController

view的两个有趣的配置是`controllerviewModel
下一个View是 “List”, classic/src/main/view/List

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 config
``controller配置允许你为 view指派一个ViewController. 当一个ViewController通过这种方式指定,它变为事件处理器和引用的引用的容器, 使得来自于这个view中的组件事件形成一对一的关系 。

了解ViewController, 可以参考文档 View Controllers

ViewModel config

viewModel配置允许你指派一个ViewModel. 这个ViewModel为这个组件和它的子View提供数据。ViewModel中包含的数据通过bind配置进行添加到组件。

在”Main” view中,你可以看到header中的title, 就绑定了ViewModel的数据。这意味着,title的值存放为data为”name”的值。这些都是通过ViewModel进行管理。如果ViewModel的数据被改变,title的值也将自动被更新。

了解更多View Model, 可以参考文档 View Models & BindingView Model Internals

Models and Stores

Models和Stores 组成了应用程序的信息门户,大部分的数据都是它们发送,接收,组件和模式化(标准化数据).

Models

Ext.data.Model表示应用程序中任何可持化的数据类型。每一个model都有字段和函数,使用应用可以”model”数据。 Models常跟Stores一起使用。Stores常被用于grids, trees, charts等组件。
我们创建一个app/model/user.js文件

Ext.define('MyApp.model.User', {
    extend: 'Ext.data.Model',
    fields: [
        {name: 'name',  type: 'string'},
        {name: 'age',   type: 'int'}
    ]
});

Stores

stores 是一个客户端记录缓存(多个model实例). Stores提供排序,过滤,查询等函数

app/store/Users.js

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'}
    ]
});

你可以在Application.js中, 通过store配置,使用上面定义的users store

stores: [
    'Users'
],

在这个例子中,store直接包含数据。在实中应该能过store中的proxy获取记录。关于更多,你可以查看 Data Guide

The controller

跟View一样,我们先看看自动生成的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文件,它监听一个select 事件。监听处理函数onItemSelected将通过父视图Main.js的controller进行配置。

当选择grid的一行,将创建一个消息框,这个消息框包含控制器自已定义的onConfirm函数。

ViewControllers的作用:

  • 使用 “listeners”和 “reference”配置,就可以在view中使用ViewController中的函数
  • 利用view的生命周期,自动管理它们相关联的 ViewController. 相同View的第二个实例,将获得自己的ViewController实例。当View被删除,相应的ViewController也将被删除.
  • 为可视化的view嵌套,提供封装

ViewModels

接下来,让我们看看app/view/main/MainModel.js 的ViewModel.

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
});

ViewModel是一个管理数据对像的类,这个类允许view, 将想要的数据绑定到 view, 并且当这些数据改变量,可以通知view. ViewModel类似于ViewController, 只属于引用它们的view. 由于ViewModels跟view相关联,所以它们也可以被View的子组件引用。

Example

自定义View

Ext.define('myApp.view.myViewport', {
    extend: 'Ext.container.Viewport',
    alias: 'widget.myviewport',
    requires: [
        'myApp.view.appZone',
        'Ext.panel.Panel'
    ],
    layout: 'border',
    items: [{
        xtype: 'panel',
        region: 'north',
        height: 76,
        itemId: 'appHeader',
        bodyPadding: 0,
        cls: 'appheaderbg',
        title: '',
        header: false,
        html: '<div class="appheader appheaderbg"><img src=
        "resources/images/myapp_logo.png"/></div>',
    },{
        xtype: 'appzone',
        region: 'center',
        itemId: 'myappZone'
    }]
});

在上面的代码中,我们创建了一个基础布局的viewport. 它将使用border layout并且包含两个组件。顶部是一个panel(region: ‘north’), 中间部分是一个类为’appzone’的组件。它是我们新创建的一个组件。所以我们在app/view文件夹下,创建一个appZone.js的文件

Ext.define('myApp.view.appZone', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.appzone',
// Alias property let us define the xtype to appzone on the
    viewport previously
    requires: [
        'myApp.store.modulesTreeDs',
        'Ext.tab.Panel',
        'Ext.tab.Tab',
        'Ext.tree.Panel',
        'Ext.tree.View'
    ],
    layout: 'border',
    header: false,
    title: '',
    items: [{
        xtype: 'tabpanel',
        region: 'center',
        itemId: 'mainZone',
        header: false,
        title: '',
        items: [{
            xtype: 'panel',
            itemId: 'startappPanel',
            title: 'Dashboard',
            bodyPadding: 5,
            html:'myApp Dashboard',
            region: 'center'
        }]
    },{
        xtype: 'panel',
        itemId: 'accessPanel',
        region: 'west',
        split: true,
        width: 180,
        layout: 'fit',
        title: 'App modules',
        items: [{
            xtype: 'treepanel',
            header: false,
            title: 'My Tree Panel',
            store: Ext.create( 'myApp.store.modulesTreeDs', {
                storeId: 'accessmodulesDs'
            }), //'modulesTreeDs'
        }]
    }]
});

在这个类中,我们创建了一个拥有 border布局的面板,它包含两个组件, 第一个是tab panel组件,用来放置我们即将创建的 module的内容。

第二个组件是一个 tree panel, 通过它,我们可以访问 application的模块。这个组件需要一个tree store和data model, 因些我们需要创建这些文件

在app/model文件中,我们创建一个modulesModel.js的文件

Ext.define('myApp.model.modulesModel', {
    extend: 'Ext.data.Model',
    requires: [
        'Ext.data.field.String',
        'Ext.data.field.Boolean',
        'Ext.data.field.Integer'
    ],
    fields: [
        {type: 'string', name: 'description'},
        {type: 'boolean', name: 'allowaccess'},
        {type: 'int', name: 'level'},
        {type: 'string', name: 'moduleType', defaultValue: ''},
        {type: 'string', name: 'moduleAlias', defaultValue: ''},
        {type: 'string', name: 'options'}
    ]
});

接着,在app/store创建 modulesTreeDs.js

Ext.define('myApp.store.modulesTreeDs', {
    extend: 'Ext.data.TreeStore',
    requires: [
        'myApp.model.modulesModel',
        'Ext.data.proxy.Ajax'
    ],
    constructor: function(cfg) {
        var me = this;
        cfg = cfg || {};
        me.callParent([Ext.apply({
            storeId: 'mymodulesTreeDs',
            autoLoad: true,
            model: 'myApp.model.modulesModel',
            proxy: {
                type: 'ajax',
                url: 'serverside/data/menu_extended.json'
            }
        }, cfg)]);
    }
});

现在我们需要在resources/images中添加logo图片(header部分使用),同时在resource/css目录中创建一个style.css

.appheader {width:100%; padding:5px;}
.appheaderbg {background-color:#CCC;}
.appheader img {width:185px;}

接着在index.html中添加这个样式文件

<!DOCTYPE HTML>
<html manifest="">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="UTF-8">
<title>myApp</title>
<!-- The line below must be kept intact for Sencha Cmd to build your
application -->
<script id="microloader" type="text/javascript" src="bootstrap.js"></
script>
<link rel="stylesheet" type="text/css" href="resources/css/style.
css">
</head>
<body></body>
</html>

接着修改app.js文件

Ext.Loader.setConfig({});
Ext.application({
name: 'myApp',
views: [
    'myViewport',
    'appZone'
],
launch: function() {
Ext.create('myApp.view.myViewport');
}
});

在Ext.application配置中, launch函数将在index.html完全加载完成调用。之前的例子中,我们都是使用 Ext.onReady方法

application的name:’myApp’ 也很重要,Ext JS将基于此来加载类所对应的文件。

这里写图片描述

Controller

现在我们的例子中有了基础部分,我们现在需要为tree panel添加交互, 为此,我们将创建一个基本的controller(MVC类型), 来控制对app模块的访问

让我们 创建一个 app/controller/app.js文件

Ext.define( 'myApp.controller.app' , {
    extend: 'Ext.app.Controller',
    requires: [
        'myApp.view.appZone',
        'myApp.view.myViewport'
    ],
    config: { },
    init: function() {
        console.log ('app controller init');
    }
});

在这段代码中,我们扩展了Ext.app.Controller类,这个类包含许多方法,这些方法将帮助我们监听和保存引用。基本上,我们都将在这里添加代码逻辑。

至于上面的init方法,这个方法在controller创建时执行。类似于类的constructor,它是我们controller首次执行的代码,所以 通常在这里创建监听器。

现在我们创建了一个空的controller, 它只在控制台显示一条消息。接下来我们需要在application定义时,添加这个控制器。 打开app.js文件

Ext.application({
    name: 'myApp',
    controllers: ['app'],
    views: [
        'myViewport','appZone'
    ],
    launch: function() {
        Ext.create('myApp.view.myViewport');
    }
});

Listening to events

一旦创建了controller,我们就可以为view添加行为。当用户双击任何子节点时(leaf: true),我们都需要打开一个模块. 现在我们需要做的是为tree panel添加itemdblclick事件.

我们需要使用Controller类中的control方法。


Ext.define('myApp.controller.app', {
    extend: 'Ext.app.Controller',
    requires:[
        'myApp.view.appZone',
        'myApp.view.myViewport'
    ],
    config:{
        refs:{
            myappzone:{
                selector:'appzone',
                xtype:'appzone',
                autoCreate:false
            }
        }
    },
    init: function() {
        console.log('app controller init');
        var me=this;
        this.control({
            'appzone #accessPanel treepanel' :{
                itemdblclick: me.handleAccess
            }
        });
    },
    handleAccess:function (cmpView, record, itemx, index, evt, eOpts ){
        console.log('handle access for: ' + record.data.text );
        var me=this, moduleData = record.data;
        if (moduleData.hasOwnProperty('moduleType')){
            var typeModule = moduleData.moduleType;
            if (typeModule==''){
                return;
            } else if (typeModule=='link'){
                me.executeLink(moduleData);
            } else if (typeModule=='window'){
                me.runWindow(moduleData);
            } else if (typeModule=='module'){
                me.addModule(moduleData);
            }
        }
    },
    addModule:function(Data){
        console.log('Adding Module: ' + Data.options);
    },
    runWindow:function(Data){
        console.log('Execute window: ' + Data.options );
    },
    executeLink:function(Data){
        console.log('launch Link: ' + Data.options );
    }
});

首先我们在config属性中,设置了refs属性,作为我们view引用的名字(appzone类), 这样我们就可以controller中通过名字myappzone,识别view.

在init 函数中,我们设置controller的control配置。这个control属性,利用一个选择器(Ext.ComponentQuery 文档,了解更多组件选择器),为引用的元素调置事件监听器. 在这里是appzone #accessPanel treepanel, 表示整个中间视图appzone下面的ID为accessPanel面板下的树形面板.

我们创建一个目录数据serverside/data/menu_extended.json

对于第一个节点, Customers的数据如下

{
    "leaf": true,
    "text": "Customers",
    "allowaccess": false,
    "description": "Customer administration",
    "level": 3,
    "moduleType": "module",
    "options": "myApp.view.modules.customers"
}

然后是Submit a ticket节点

{
    "leaf": true,
    "text": "Submit a ticket",
    "allowaccess": false,
    "description": "Submit support tickets",
    "level": 3,
    "moduleType": "window",
    "options": "myApp.view.ticket"
}

最后一步的Forum元素

{
    "leaf": true,
    "text": "Forum",
    "allowaccess": false,
    "description": "Go to Forum",
    "level": 3,
    "moduleType": "link",
    "options": "http://www.sencha.com/forum/"
}

你将看到如下的控制台输出

这里写图片描述

Opening modules

现在我们可以监听双击事件,所以我们可以利用它来打开一个模块(虽然我们还没有创建它们,但在下步我们会创建一个模块)。 所以我们在一次修改controller文件,修改addModule, runWindow, 和executeLink函数

addModule:function(data){
    console.log('Adding Module: ' + data.options);
    var me=this;
    var myZone = me.getMyappzone();
    var ModulesTab = myZone.query('tabpanel #mainZone')[0];
    var existModule= false;
    for (var i=0;i<ModulesTab.items.items.lenght;i++){
        if (ModulesTab.items.items[i].xtype==data.moduleAlias){
            existModule= true;
            break;
        }
    }
    if (existModule){
        ModulesTab.setActiveTab(i);
        return;
    } else {
        var mynewModule = Ext.create(data.options);
        ModulesTab.add(mynewModule);
        ModulesTab.setActiveTab((ModulesTab.items.items.lenght -1));
        return;
    }
},
runWindow:function(data){
    console.log('Execute window: ' + data.options );
    Ext.Msg.alert("Window module", "here we show window:<b>" +
            data.text+ "</b>");
},
executeLink:function(data){
    console.log('launch Link: ' + data.options );
    window.open(data.options);
}

创建一个模块

我们定义一个model类

Ext.define(' myApp.model.Customer',{
    extend: 'Ext.data.Model',
    requires: ['myApp.model.Contract'],
    idProperty: 'id',
    fields: [
    {name: 'id', type: 'int'},
    {name: 'name', type: 'string'},
    {name: 'phone', type: 'string'},
    {name: 'website', type: 'string'},
    {name: 'status', type: 'string'},
    {name: 'clientSince', type: 'date', dateFormat: 'Y-m-d H:i'},
    {name: 'country', type: 'string'},
    {name: 'sendnews', type: 'boolean'},
    {name: 'employees', type: 'int'},
    {name: 'contractInfo', reference: 'Contract', unique:true}
    ]
});

接着在定义Customer对应的store

Ext.define('myApp.store.Customers', {
    extend: 'Ext.data.Store',
    requires: [
        'myApp.model.Customer',
        'Ext.data.proxy.Ajax',
        'Ext.data.reader.Json'
    ],
    constructor: function(cfg) {
        var me = this;
        cfg = cfg || {};
        me.callParent([Ext.apply({
            storeId: 'Customers',
            autoLoad: true,
            model: 'myApp.model.Customer',
            proxy: {
                type: 'ajax',
                url: 'serverside/data/customers.json',
                actionMethods: {read:"POST"},
                reader: {
                    type: 'json',
                    rootProperty: 'records',
                    useSimpleAccessors: true
                }
            }
        }, cfg)]);
    }
});

现在我们创建一个 Grid panel. app/view/modules/customers.js

Ext.define('myApp.view.modules.customers', { //step 1
    extend: 'Ext.grid.Panel',
    requires: [
        'myApp.view.modules.customersController',
        'Ext.grid.column.Number',
        'Ext.grid.column.Date',
        'Ext.grid.column.Boolean',
        'Ext.view.Table',
        'Ext.button.Button',
        'Ext.toolbar.Fill',
        'Ext.toolbar.Paging'
    ],
    xtype: 'customersmodule', //step 2
    alias: 'widget.customersmodule',
    controller: 'customersmodule',
    frame: true,
    closable: true,
    iconCls: '',
    title: 'Customers...',
    forceFit: true,
    listeners: {//step 3
        'afterrender': {fn: 'myafterrender'},
        'render': {fn: 'myrenderevent'}
    },
    initComponent: function() { //step 4
        var me = this;
        me.store = me.createCustomersStore();
        me.columns = [/* columns definition here… */];
        me.dockedItems= [/* items here… */];
        me.callParent();
    },
    createCustomersStore:function(){
        return Ext.create('myApp.store.Customers');
    }
});

让我们分析一下上面的代码

  1. 首先,我们定义一个’myApp.view.modules.customers’的类。它继承自Ext.grid.Panel
  2. 然后我们定义了这个组件的xtype, alias, controller属性。是为了使得整个应用可以使用”customersmodule”来识别这个组件类型,而ViewController将是这个view使用
  3. 我们定义了一个grid监听器
listeners: { //step 3
    afterrender: {fn: 'myafterrender'},
    render: {fn: 'myrenderevent'}
},
  1. 在最后,initComponent函数中,我们定义了其它属性。通过这种方式,我们可以根据不同的配置或者特定权限来设置属性,或者根据不同条件,改变View的子组件

上面中columns的配置如下


me.columns =[{
    xtype: 'rownumberer',
    width: 50,
    align:'center'
},{
    xtype: 'numbercolumn',
    width: 70,
    dataIndex: 'id',
    text: 'Id',
    format: '0'
},{
    xtype: 'templatecolumn',
    text: 'Country',
    dataIndex: 'country',
    tpl: '<div><divclass="flag_{[values.country.toLowerCase()]}">' +
    '&nbsp</div>&nbsp;&nbsp;{country}</div>'
},{
    xtype: 'gridcolumn',
    width: 210,
    dataIndex: 'name',
    text: 'Customer name'
},{
    xtype: 'datecolumn',
    dataIndex: 'clientSince',
    width: 120,
    text: 'Client Since',
    format: 'M-d-Y',
    align:'center'
},{
    xtype: 'booleancolumn',
    dataIndex:'sendnews',
    width: 100,
    align:'center',
    text: 'Send News?',
    falseText: 'No',
    trueText: 'Yes'
}];

dockedItems如下

me.columns =[{
    xtype: 'rownumberer',
    width: 50,
    align:'center'
},{
    xtype: 'numbercolumn',
    width: 70,
    dataIndex: 'id',
    text: 'Id',
    format: '0'
},{
    xtype: 'templatecolumn',
    text: 'Country',
    dataIndex: 'country',
    tpl: '<div><divclass="flag_{[values.country.toLowerCase()]}">' +
    '&nbsp</div>&nbsp;&nbsp;{country}</div>'
},{
    xtype: 'gridcolumn',
    width: 210,
    dataIndex: 'name',
    text: 'Customer name'
},{
    xtype: 'datecolumn',
    dataIndex: 'clientSince',
    width: 120,
    text: 'Client Since',
    format: 'M-d-Y',
    align:'center'
},{
    xtype: 'booleancolumn',
    dataIndex:'sendnews',
    width: 100,
    align:'center',
    text: 'Send News?',
    falseText: 'No',
    trueText: 'Yes'
}];

ViewController

如我们之前所说,view controller需要附件到view上面,每创建一次视图,也就创建一个ViewController的实例。如果不使用ViewController,而是Controller. 那么所有的模块视图将使用同一样Controller, 则不易于代码维护

这里写图片描述

对于我们新创建的模块customers , 我们将使用view controller. 现在我们在app/view/modules文件夹下,我们将创建customerControllers.js

Ext.define('myApp.view.modules.customersController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.customersmodule',
    config: {
        control: {
// Other alternative on how to listen some events
            'customersmodule button[action=showhelp]': {
                click: 'btnactionclick'
            }
        }
    },
    init: function() {
        console.log('customers view controller init');
    },
    myrenderevent:function(cmpx, eOpts){
        console.log('Grid - customers render event');
    },
    myafterrender:function(cmpx, eOpts){
        console.log('Grid - customers afterrender event');
    },
    btnactionclick:function(btnx, evt, eOpts){
        console.log('Button clicked : ' + btnx.action);
    }
});

在上面的代码中,我们为customer视图,创建了一个ViewController. 它的名字为App.view.modules.customersController, 继承
app.ViewController, 我们使用别名为controller.customersmodule, 它将在views的controller配置中使用, controller: ‘customersmodule’, 通过它,Ext JS将view与ViewController连接在一起。

同样,你可能注意到,在customer view中,我们有一个toolbar(dockeditems), 它有三个按纽,每个按纽都设置了一个监听器处理函数。但对于help按纽,我们还没有监听器。所以在ViewController中,设置以下代码

config:{
    control: {
        // Other alternative on how to listen some events
        'customersmodule button[action=showhelp]': {
            click:'btnactionclick'
        }
    }
},

这段代码将被监听view中的Help 按纽的点击事件,然后运行btnactionclick。整个customer模块的效果如下图所示

这里写图片描述

现在我们点击新模块中的按纽,控制台输出如下结果

这里写图片描述

现在我们看到 View与ViewController正确的连接,并做出我们需要的响应。现在,让我们创建一个form, 用来添加新的customer记录。 在这个新的form中,我们将实现ViewModel, 将form的行为与model中定义的数据能过数据绑定进行连接。

ViewModel

Ext JS中的ViewModel类用来管理数据对像,它将监听ViewModel中定义的数据的变化。这个类还可以连接到父ViewModel(从components/views中继承过来), 即它允许child view继承父ViewModel中的数据

在第5章中, 组件有一个新的配置属性bind, 它允许我们关联ViewModel中定义的数据

如我们之前所说, 一旦ViewModel相关联的view实例创建时,ViewModel也将创建一个新的实例,类似于ViewController. 现在为Customer form创建一个ViewModel, 文件为app/view/forms/customerFormViewModel.js


Ext.define('myApp.view.forms.customerFormViewModel', { //step 1
    extend:'Ext.app.ViewModel',
    alias:'viewmodel.customerform',
    data:{ //step 2
        action: 'add',
        ownerCmp: null,
        rec: null
    },
    formulas:{ //Step 3
        readOnlyId:function(get){
            return (get('action')!=='add');
        },
        ownerNotNull:function(get){
            var cmpx = get('ownerCmp');
            return (cmpx!==null && cmpx!==undefined);
        },
        refName:function(get){
            var value='';
            if (get('action')!=='add'){ //Edit action
                var id = get('rec.id'), custname =get('rec.name');
                if (custname===''){ custname ='(not defined)'; }
                value = 'Editing : ' + id + ' - ' + custname + "..." ;
            } else {
                value = 'New customer...';
            }
//Step 4
            var xtypeOwner= this.getView().ownerCt.getXType();
            if (xtypeOwner=="customerwindow"){
                this.getView().ownerCt.setTitle(value);
            }
            Return value;
        }
    }
});

让我们一步步的解释下上面的代码:

  1. 我们定义了一个Ext.app.ViewModel的子类, 并且设置别名viewmodel.customerform. 所以我们可以通过customerform引用
  2. 我们设置了默认的data配置对像,它将在创建新的view时,被重写
  3. 我们设置了一个formulas属性, 它是一个对像,这个对像中指定的值,将通过函数进行管理,所以我们可以操作这些值, 比如,定义一个name的属性,它由data中的firstName 和 lastName, 则可以返回 return get("firstName") + get("lastName")。在这里,我们设置了三个新的属性,称为readOnlyId, ownerNotNull 和 refName.
  4. 如果你仔细观察了formulas.refName函数, 你会注意到,我们使用了this.getView()方法,这个方法,允许我们访问连接到的view实例,并且操作它

Binding and data binding

在Ext JS5中,组件多了一个新的配置,bind, 它允许我们关联 ViewModel中的数据,所以,使用bind, 我们可以绑定想要的数据,发生改变时,这个配置也将自动更新。

为了引用model中相应的数据,我们需要使用bind描述符

  • 直接绑定: bind:{ value: '{firstName}'}
  • 绑定模板: 我们可以像Ext.Template那样,使用自定义的字符串 bind:{ title: 'Hello {firstName} {lastName}..!'}
  • 绑定布尔值: 对于绑定一个Boolean配置非常有用,{!isAdmin.checked}

你可以查看Ext JS文档 了解更多ViewModel与binding

我们创建customers form, app/view/forms/customerForm.js

Ext.define('myApp.view.forms.customerForm', { //Step 1
    extend: 'Ext.form.Panel',
    alias: 'widget.customerform',
    xtype: 'customerform',
    requires:[
        'Ext.form.field.Number',
        'Ext.form.field.Date',
        'Ext.form.field.ComboBox',
        'Ext.toolbar.Toolbar',
        'Ext.toolbar.Fill',
        'Ext.button.Button',
        'myApp.view.forms.customerFormViewController',
        'myApp.view.forms.customerFormViewModel',
        'myApp.model.Customer'
    ],
    controller: 'customerform', //Step 2
    ViewModel: {type: 'customerform' }, //Step 2
    bodyPadding: 6,
    header: false,
    title: 'Customer...',
    bind:{ title: '{refName}' }, //Step 3
    defaults:{
        labelAlign: 'right',
        labelWidth: 80,
        msgTarget: 'side',
        anchor: '-18'
    },
    items: [{
        xtype: 'numberfield',
        fieldLabel: 'Customer ID',
        name: 'id',
        anchor: '100%',
        maxWidth: 200,
        minWidth: 200,
        hideTrigger: true,
        bind:{ value:'{rec.id}', readOnly:'{readOnlyId}'}//Step 3
    },{
        xtype: 'textfield',
        fieldLabel: 'Name',
        name: 'name',
        bind: '{rec.name}' //Step 3
    },{
        xtype: 'textfield',
        fieldLabel: 'Phone',
        name: 'phone',
        bind: '{rec.phone}' //Step 3
    },{
        xtype: 'textfield',
        fieldLabel: 'Web site',
        name: 'website',
        bind: '{rec.website}' //Step 3
    },{
        xtype: 'datefield',
        anchor: '60%',
        fieldLabel: 'Client since',
        name:'clientSince',
        submitFormat: 'Y-m-d',
        bind:'{rec.clientSince}' //Step 3
    },{
        xtype: 'combobox',
        fieldLabel: 'Country',
        name: 'country',
        store: Ext.create('Ext.data.Store', {
            fields: ['id', 'name'],
            data : [
                {"id": "USA", "name": "United States of America"},
                {"id": "Mexico", "name": "Mexico"}
            ]
        }),
        valueField: 'id',
        displayField: 'name',
        bind:'{rec.country}' //Step 3
    },{
        xtype: 'combobox',
        fieldLabel: 'Status',
        name: 'status',
        store: Ext.create('Ext.data.Store', {
            fields: ['id', 'name'],
            data: [
                {"id": "Active", "name": "Active"},
                {"id": "Inactive", "name": "Inactive"},
                {"id": "Suspended", "name": "Suspended"},
                {"id": "Prospect", "name": "Prospect"},
            ]
        }),
        valueField: 'id',
        displayField: 'name',
        bind: '{rec.status}' //Step 3
    },{
        xtype: 'numberfield',
        anchor: '60%',
        fieldLabel: '# Employees',
        name:'employees',
        bind:'{rec.employees}' //Step 3
    },{
        xtype:'checkbox',
        fieldLabel: 'Send news ?',
        boxLabel:'check if yes/uncheck if no...!',
        name:'sendnews',
        inputValue:1,
        bind:'{rec.sendnews}' //Step 3
    }],
    dockedItems: [{
        xtype: 'toolbar', dock: 'bottom',
        items: [{
            xtype: 'tbfill'
        },{
            xtype: 'button',
            iconCls: 'save-16',
            text: 'Save...', action:'savecustomer'
        },{
            xtype: 'button',
            iconCls: 'cancelicon-16',
            text: 'Close / Cancel',
            action:'closeform',
            bind:{ hidden:'{ownerNotNull}'}
        }]
    }],
    initComponent: function(){
// place your code....
        this.callParent();
    },
    listeners:{ //Step 4
        'titlechange':{
            fn:function( panelx, newtitle, oldtitle, eOpts){
                if (panelx.rendered){
                    panelx.ownerCt.setTitle(newtitle);
                }
            }
        },
        'afterrender':{
            fn:function( panelx, eOpts ){
                panelx.ownerCt.setTitle(panelx.title);
            },
            single:true
        }
    }
});
  1. 我们创建一个继承Ext.form.Panel的类
  2. 定义controller和 ViewModel {type: ‘customform’}
  3. 通过bind属性,将viewModel(customerForm)中的配置与表单的字段连接, 绑定时,可以为字符串,或者对像, 如{refName} or {rec.id}。对于许多组件,默认绑定到value属性上
  4. 注意一下,我们设置close按纽的绑定,bind: {hidden: '{ownerNotNull}'}, ownerNotNull取决于ownCmp属性,如果formPanel有一个父窗器,或者设置了ownCmp,则这个按纽出现,否则隐藏。
  5. 我们使用事件来监听title的改变,我们一开始设置了form view的 header: false. 但我们又绑定了它的title到ViewModel. 所以title改变时,我们的ownerCt组件,也将改变它的title

最后,我们在app/view/forms文件夹中,创建一个customFormViewController.js,作为customer form的ViewController


Ext.define('myApp.view.forms.customerFormViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.customerform',
    config: {
        control: {
            'customerform button[action=savecustomer]': {
                click:'saveCustomer'
            },
            'customerform button[action=closeform]': {
                click:'formClose'
            }
        }
    },
    init: function() {
        console.log('customers form view controller init');
    },
    formClose: function(cmpx, eOpts){
        console.log('Closing Form');
        var closeCmp= this.getViewModel().get('ownerCmp');
        if(closeCmp!==null && closeCmp!==undefined){
            var xtypeUsed = closeCmp.getXType();
            if (xtypeUsed ==='panel' || xtypeUsed ==='gridpanel' ||
                    xtypeUsed ==='window' || xtypeUsed ==="customerwindow"){
                closeCmp.close();
            }
        }
        return;
    },
    saveCustomer:function(btnx, evt, eOpts){
        var action= this.getView().getViewModel().get('action');
        console.log('Performing action in form : ' + btnx.action);
        if(action=='add'){
            if( this.getView().getForm().isValid() ) {
                var newCustomerData =this.getView().getForm().getValues();
                var mycustomer = Ext.create('myApp.model.Customer',
                        newCustomerData );
                this.getView().gridModule.getStore().add(mycustomer);
                Ext.Msg.alert('Ok', 'New customer added successfully..!');
                this.formClose();
            } else {
                Ext.Msg.alert('Error!', 'There are' + 'some errors in the
                form , please check' + ' the information!');
                return;
            }
        } else { //Edit action
            if ( this.getView().getForm().isValid()){
                var newCustomerData = this.getView().getForm().
                getValues();
                var Record = this.getView().gridModule.getStore().getById(
                        newCustomerData.id);
                var editResult = Record.set(newCustomerData);
                if (editResult!=null){
                    Record.commit();
                    Ext.Msg.alert('Ok', 'Customer edited successfully.!');
                    this.formClose();
                } else {
                    Ext.Msg.alert('Error.!', 'Error updating customer.!');
                    return;
                }
            } else {
                Ext.Msg.alert('Error..!', 'There are some errors in the
                form, please check the information..!');
                return;
            }
        }
    }
});

在这个控制器中,我们为表单的 save和 close按钮添加了save 和 close函数。接下来,我们创建一个新的View, 这个视图实际上是对customerForm进行了包装,app/view/forms/customerWindow.js

Ext.define('myApp.view.forms.customerWindow', { //Step 1
    extend: 'Ext.window.Window',
    alias: 'widget.customerwindow',
    xtype: 'customerwindow',
    requires: [
        'myApp.view.forms.customerWindowViewController',
        'myApp.view.forms.customerForm'
    ],
    controller: 'customerwindow', //Step 2
    height: 368,
    width: 489,
    iconCls: 'customer-16',
    layout:'fit',
    closable:true,
    minimizable:true,
    title: '',
    tools:[{ //Step 3
        type:'restore',
        tooltip: 'Restore window...',
        handler: function(event, toolEl, panelHeader) {
            var cmpx=panelHeader.up('window');
            if (cmpx.collapsed){
                cmpx.expand();
            }
        }
    }],
    initComponent: function() {
        var me=this;
//Step 4
        var myForm =Ext.create('myApp.view.forms.customerForm',{
            gridModule: me.gridModule,
            ViewModel:{
                data:{
                    action:me.action,
                    ownerCmp: me,
                    rec: me.record || null
                }
            }
        });
        me.items=[myForm];
        me.callParent(arguments);
    }
});
  1. 在第一步, 我们定义一个类
  2. 然后,定义了一个controller
  3. 创建了一个工具,当最小化时,用来还原window
  4. 在initComponent函数中,我们创建了一个customerForm的实例,即这个window将包含的表单对像。在配置对像中,我们设置了ViewModel和它的数据。它将让Ext JS创建一个customerForm的实例,并且应用这些配置中指定的数据

以下是window的controller

Ext.define('myApp.view.forms.customerWindowViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.customerwindow',
    config: {
        control:{
            'customerwindow':{
                'minimize':'mywindowMinimize',
                'expand':'myExpand'
            },
        }
    },
    mywindowMinimize:function(cmpx, eOpts){
        console.log('customerWindow minimizing..!');
        cmpx.collapse();
        cmpx.alignTo(Ext.getBody(),'tr-tr');
    },
    myExpand:function(cmpx, eOpts){
        cmpx.center();
    }
});

这个controller将控制window的 minimizes and restored. 与网页或者浏览器居中对齐。

![这里写图片描述](https://img-blog.csdn.net/20160824135256069)

Router – implementing and using

对于路由的使用,可以查看文档 Using the router

在我们的应用程序中,还可以使用路由。路由可以通过浏览器的历史,用来追踪应用程序的状态。比如Sencha的官方例子 Kitchen Sink

这里写图片描述

URL的#basic-panels部分,我们称为hash or fragment标识符。当它改变量,浏览器会触发一个hashchange事件,它可以被我们的application捕获, 我们可以在 application中使用这个hash。

所以在这个URL例子中,如果你复制了这个URL, 然后关闭浏览器,在打开这个URL, 它将打开最后的模块(视图). 在这里是打开basic-panels这个例子.

为了实现他,我们需要修改app.js文件

init:function() {
    this.setDefaultToken('');
}

如果没有指定token, 将使用setDefaultToken设置的token. 接下来,改变app/controller/app.js文件的handleAccess 函数

handleAccess: function(cmpView, record, itemx, index, evt, eOpts ){
    console.log('Action for handle access : ' + record.data.text);
    var me=this, moduleData = record.data;
    if (moduleData.hasOwnProperty('moduleType')){
        var typeModule = moduleData.moduleType;
        if (typeModule==''){
            return;
        } else if(typeModule=='link'){
            me.executeLink(moduleData);
        } else if (typeModule=='window'){
            me.runWindow(moduleData);
        } else if (typeModule=='module'){
            //Change to be made for router
            if (moduleData.options=="myApp.view.modules.customers"){
                this.redirectTo('customers', true);
                return;
            } else {
                me.addModule(moduleData);
            }
        }
    }
},

redirectTo方法将更新这个hash, 默认情况下,如果当前token与传递 过来的token相同,则不执行, 在这里,我们传递了customers参数和true. 第一个参数用于设置hash字符串, 第二个字串则用来强制更新hash, 而不管当前的token是什么. 我们在config中添加routes属性

config:{
    refs:{
        myappzone:{
            selector:'appzone',
                    xtype:'appzone',
                    autoCreate:false
        }
    },
    routes:{
        ':id': {
            action: 'handleRoute',
                    before: 'beforeHandleRoute'
        }
    }
},
beforeHandleRoute: function(id, action) {
    if (id!='customers'){
        Ext.Msg.alert("Route error", "invalid action...!");
        action.stop();
    } else {
        action.resume();
    }
},
handleRoute: function(id) {
    if (id=='customers'){
        var myStore=this.getMyappzone().query('treepanel')[0].
        getStore();
        var myRecord = myStore.findNode('text', 'Customers');
        if (myRecord!=undefined){
            this.addModule(myRecord.data);
        } else {
            Ext.Msg.alert("Route error", "error getting customers data
            access...!");
        }
    }
}

如果路径中有一个hash片段,那么会先执行beforeHandleRoute. 当所有都可以,我们需要调用action.resume()函数, 让Ext JS继续执行路由.否则调用action.stop(), 路由器什么也不做

handleRoute函数,将从tree panel中获取一个数据记录,并且调用addModule创建想要的模块(在这里为Customer)

所以,当你运行这个应用,并且打开customer module. 你会看到 URL中的hash已经被更新了。在更新完成后,重新加载页面. 如下图所示

这里写图片描述

猜你喜欢

转载自blog.csdn.net/cexo425/article/details/52278629