MVC架构

概念

Model:定义模型的域、数据、持久化
View:视图组件
Controller:逻辑代码,包括显示组件、初始化模型、以及业务逻辑。

Data Package

Data Package In ExtJS4
Data Package In ExtJS4

工程文件结构

文件结构
文件结构

配置

Application中stores,views,models,controllers配置,相关文件会被加载,并且初始化store和controllers实例(并且调用controller的init方法,以完成事件的绑定等操作)。之后便可以使用getXXXStore和getXXXController()返回相应的实例;调用models和views相关的get方法,则只返回class。Store和Controller如果不被创建新实例,则它们为单实例。

Controler中这四个属性的配置和Application的用法完全一致。可以使用Controller模版方法onLaunch完成文件加载完后的初始化工作,如加载数据。

应用入口

Ext.application({
    requires: ['Ext.container.Viewport'],

    // This automatically sets up a global variable AM for us, and registers the namespace to Ext.Loader, 
    // with the corresponding path of 'app' set via the appFolder config option. 
    // All Ext JS 4 applications should only use a single global variable, with all of the application's classes nested inside it
    name: 'AM', 

    // 源码类相对路径(相对工程根目录)
    appFolder: 'app', 

    // 函数入口, which is run automatically when everything is loaded
    launch: function() {
        Ext.create('Ext.container.Viewport', {
            layout: 'fit',
            items: [
                {
                    xtype: 'panel',
                    title: 'Users',
                    html : 'List of users will go here'
                }
            ]
        });
    }
});

Controller

Controllers are the glue that binds an application together.

control

使用模版方法init完成Controller的初始化工作,主要包括事件绑定。使用control方法完成事件的绑定:

app/controller/Users.js:

Ext.define('AM.controller.Users', {
    extend: 'Ext.app.Controller',

    init: function() {
        // 定义监听器
        this.control({
            'viewport > panel': {
                render: this.onPanelRendered
            }
        });
    },

    onPanelRendered: function() {
        console.log('The panel was rendered');
    }
});

app.js:

Ext.application({
    ...

    controllers: [
        'Users'
    ],

    ...
});    

When we load our application by visiting index.html inside a browser, the Users controller is automatically loaded (because we specified it in the Application definition above), and its init function is called just before the Application’s launch function.

ref

ref属性会自动产生get方法,如果实例不存在,则返回null。

refs: [
        {
            ref: 'list',
            selector: 'grid'
        }
    ]

多个Controller之间的协作

为了避免多个Controller同时监听一个view中的相同组建,可以只在一个Controller中监听改组建,然后产生一个application级别的事件,this.application.fireEvent(‘stationstart’, selection[0]);其他controller都可以监听这个事件。

在Controller中,可以使用this.application获取Application实例。

在运行时创建Controller时,调用方法this.getController(‘AnotherController’);获取Controller实例,并且调用其init方法。

Views

定义Views

app/view/user/List.js:

Ext.define('AM.view.user.List' ,{
    extend: 'Ext.grid.Panel',
    // 定义别名,可以通过xtype引用。
    alias: 'widget.userlist',

    title: 'All Users',

    initComponent: function() {
        // 创建内联Store
        this.store = {
            fields: ['name', 'email'],
            data  : [
                {name: 'Ed',    email: 'ed@sencha.com'},
                {name: 'Tommy', email: 'tommy@sencha.com'}
            ]
        };

        this.columns = [
            {header: 'Name',  dataIndex: 'name',  flex: 1},
            {header: 'Email', dataIndex: 'email', flex: 1}
        ];

        this.callParent(arguments);
    }
});

Ext.define('AM.controller.Users', {
    extend: 'Ext.app.Controller',

    views: [
        'user.List'
    ],

    init: ...

    onPanelRendered: ...
});

AM.controller.Users:

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.js:

Ext.application({
    ...

    launch: function() {
        Ext.create('Ext.container.Viewport', {
            layout: 'fit',
            items: {
                xtype: 'userlist'
            }
        });
    }
});

Model

Model定义:

Ext.define('Panda.model.Station', {
extend: 'Ext.data.Model',
fields: ['id', 'name', 'played_date'],

proxy: {
        type: 'ajax',
        url: 'data/stations.json',
        reader: {
            type: 'json',
            root: 'results'
        }
    }
});

可以设置其idProperty属性来设置store中数据的唯一性,idgen用来控制id产生的机制,这在实例化新的Model的时候可以用上。

Models and Stores

Models and Stores
Models and Stores

关系

Examples:

可以通过hasMany、hasOne、belongTo来表达。框架会根据关系类型生成相应的get和set方法。

  • primaryKey 主键,默认为id。
  • foreignKey 外键,默认为关联的Model的名字的小些+_id,如user_id。

定制主键和外键

Ext.define('Product', {
    fields: [...],

    associations: [
        { type: 'belongsTo', model: 'Category', primaryKey: 'unique_id', foreignKey: 'cat_id' }
    ]
});

Model的关联的对象可以直接被加载,也可以懒加载(由后台决定)。如果是懒加载,则需要调用相应的load方法来加载关联对象,如:

// Loads User with ID 1 User's Proxy
User.load(1, {
    success: function(user) {
        console.log("User: " + user.get('name'));

        // Loads posts for user 1 using Post's Proxy
        user.posts().load({
            callback: function(posts, operation) {
                Ext.each(posts, function(post) {
                    console.log("Comments for post: " + post.get('title'));

                    post.comments().each(function(comment) {
                        console.log(comment.get('message'));
                    });
                });
            }
        });
    }
});

例子中用到的Model

User:

Ext.define('User', {
    extend: 'Ext.data.Model',
    fields: [
        {name: 'id',   type: 'int'},
        {name: 'name', type: 'string'}
    ],
    // we can use the hasMany shortcut on the model to create a hasMany association
    hasMany: {model: 'Product', name: 'products'}
});

Product:

Ext.define('Product', {
    extend: 'Ext.data.Model',
    fields: [
        {name: 'id',      type: 'int'},
        {name: 'user_id', type: 'int'},
        {name: 'name',    type: 'string'}
    ],

    associations: [
        { type: 'belongsTo', model: 'Category', primaryKey: 'unique_id', foreignKey: 'cat_id' }
    ]
});

Category:

Ext.define('Category', {
    extend: 'Ext.data.Model',
    fields: [
        { name: 'id',   type: 'int' },
        { name: 'name', type: 'string' }
    ]
});    

多对一

当数据加载上来后,数据会被缓存,第二次调用不会再与后台交互。可以设置reload来控制是否每次调用都与后台交互;setCategory(),可以传入category_id的值或者Category实例。关系的primaryKey默认为id,一般为Model中定义的idProperty。

  • 产生的Getter方法

    product.getCategory(function(category, operation) {

    // do something with the category object
    alert(category.get('id')); // alerts 20
    

    }, this);

也可以传入配置对象:

product.getCategory({
    reload: true, // force a reload if the owner model is already cached
    callback: function(category, operation) {}, // a function that will always be called
    success : function(category, operation) {}, // a function that will only be called if the load succeeded
    failure : function(category, operation) {}, // a function that will only be called if the load did not succeed
    scope   : this // optionally pass in a scope object to execute the callbacks in
});

This uses the Category’s configured proxy to load the Category asynchronously, calling the provided callback when it has loaded.

Once the getter has been called on the model, it will be cached if the getter is called a second time. To force the model to reload, specify reload: true in the options object.

  • 产生的Setter 方法
    如果只传入一个参数,则作为外键处理

    // this call…
    product.setCategory(10);

    // is equivalent to this call:
    product.set(‘category_id’, 10);

也可以传入一个对象,被解析为关联对象

product.setCategory(10, function(product, operation) {
    // the product has been saved
    alert(product.get('category_id')); //now alerts 10
});

//alternative syntax:
product.setCategory(10, {
    callback: function(product, operation), // a function that will always be called
    success : function(product, operation), // a function that will only be called if the load succeeded
    failure : function(product, operation), // a function that will only be called if the load did not succeed
    scope   : this //optionally pass in a scope object to execute the callbacks in
})    

一对多

  • Ext.data.association.HasMany

    //first, we load up a User with id of 1
    var user = Ext.create(‘User’, {id: 1, name: ‘Ed’});

    //the user.products function was created automatically by the association and returns a Store
    //the created store is automatically scoped to the set of Products for the User with id of 1
    var products = user.products();

    //we still have all of the usual Store functions, for example it’s easy to add a Product for this User
    products.add({

    name: 'Another Product'
    

    });

    //saves the changes to the store - this automatically sets the new Product’s user_id to 1 before saving
    products.sync();

生成的get方法为category.products(),加载products时,默认按id来过滤,也可以自定义过滤属性:

var store = Ext.create('Ext.data.Store', {
    model: 'Tweet',
    filters: [
        {
            property: 'query',
            value   : 'Sencha Touch'
        }
    ]
});    

枚举

// Loads User with ID 1 and related posts and comments using User's Proxy
User.load(1, {
    success: function(user) {
        console.log("User: " + user.get('name'));

        user.posts().each(function(post) {
            console.log("Comments for post: " + post.get('title'));

            post.comments().each(function(comment) {
                console.log(comment.get('message'));
            });
        });
    }
});    

一对一

生成的get和set方法如多对一中描述。

验证

  • Data Package

    Ext.define(‘User’, {

    extend: 'Ext.data.Model',
    fields: ...,
    
    validations: [
        {type: 'presence', name: 'name'},
        {type: 'length',   name: 'name', min: 5},
        {type: 'format',   name: 'age', matcher: /\d+/},
        {type: 'inclusion', name: 'gender', list: ['male', 'female']},
        {type: 'exclusion', name: 'name', list: ['admin']}
    ],
    
    proxy: ...
    

    });

常用的验证规则:

  • presence simply ensures that the field has a value. Zero counts as a valid value but empty strings do not.
  • length ensures that a string is between a min and max length. Both constraints are optional.
  • format ensures that a string matches a regular expression format. In the example above we ensure that the age field consists only of numbers.
  • inclusion ensures that a value is within a specific set of values (e.g. ensuring gender is either male or female).
  • exclusion ensures that a value is not one of the specific set of values (e.g. blacklisting usernames like ‘admin’).

验证:

// run some validation on the new user we just created
var errors = newUser.validate();

console.log('Is User valid?', errors.isValid()); //returns 'false' as there were validation errors
console.log('All Errors:', errors.items); //returns the array of all errors found on this model instance

console.log('Age Errors:', errors.getByField('age')); //returns the errors for the age field    

Model和后台交互

// Gives us a reference to the User class
var User = Ext.ModelMgr.getModel('User');
var ed = Ext.create('User', {
    name: 'Ed Spencer',
    age : 25
});
// We can save Ed directly without having to add him to a Store first because we
// configured a RestProxy this will automatically send a POST request to the url /users
ed.save({
    success: function(ed) {
        console.log("Saved Ed! His ID is "+ ed.getId());
    }
});
// Load User 1 and do something with it (performs a GET request to /users/1)
User.load(1, {
    success: function(user) {
        console.log("Loaded user 1: " + user.get('name'));
    }
});

Example of a Model that uses a Proxy directly

Store

内联数据

测试时使用本地数据可以使用data属性,当data为对象数据时,可以不设置reader属性,也就是说不需要proxy。如果数据需要处理,如数据嵌套在users中,则需要使用memery proxy中的reader属性来格式数据。如

//note how we set the 'root' in the reader to match the data structure above
var store = Ext.create('Ext.data.Store', {
    autoLoad: true,
    model: 'User',
    data : data,
    proxy: {
        type: 'memory',
        reader: {
            type: 'json',
            root: 'users'
        }
    }
});

使用内联Model定义Store

Ext.define('AM.store.Users', {
    extend: 'Ext.data.Store',
    fields: ['name', 'email'],
    data: [
        {name: 'Ed',    email: 'ed@sencha.com'},
        {name: 'Tommy', email: 'tommy@sencha.com'}
    ]
});

使用Model类定义Store

AM.model.User:

Ext.define('AM.model.User', {
    extend: 'Ext.data.Model',
    fields: ['name', 'email']
});    

AM.store.Users:

Ext.define('AM.store.Users', {
    extend: 'Ext.data.Store',
    model: 'AM.model.User',

    data: [
        {name: 'Ed',    email: 'ed@sencha.com'},
        {name: 'Tommy', email: 'tommy@sencha.com'}
    ]
});

Registering with StoreManager

Any Store that is instantiated with a storeId will automatically be registered with the StoreManager. This makes it easy to reuse the same store in multiple views:

//this store can be used several times
Ext.create('Ext.data.Store', {
    model: 'User',
    storeId: 'usersStore'
});

new Ext.List({
    store: 'usersStore',
    //other config goes here
});

new Ext.view.View({
    store: 'usersStore',
});   

和服务器交互

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

其中proxy.url表示所有的操作(CRUD)都使用该地址,如果需要配置不同的操作使用不同的url,可以使用Proxy.api属性:

api: {
        read: 'data/users.json',
        update: 'data/updateUsers.json'
    }

动态加载

store.load({
    params: {
        group: 3,
        type: 'user'
    },
    callback: function(records, operation, success) {
        // do something after the load finishes
    },
    scope: this
});        

分组和排序

例子

可以使用分组和排序静态配置sorters和filters,也可以调用filte和sort方法动态分组和排序。

  • 静态配置

    Ext.create(‘Ext.data.Store’, {

    model: 'User',
    
    sorters: ['name', 'id'],
    filters: {
        property: 'name',
        value   : 'Ed'
    },
    groupField: 'age',
    groupDir: 'DESC'
    

    });

  • 动态分组和排序
    Calling filter adds another filter to the Store and automatically filters the dataset (calling filter with no arguments simply re-applies all existing filters). Note that by default sortOnFilter is set to true, which means that your sorters are automatically reapplied if using local sorting.

    store.filter(‘eyeColor’, ‘Brown’);
    store.sort(‘height’, ‘ASC’);

  • 远程分组和排序

    remoteSort : true
    remoteFilter : true

在Controller中引用Store

AM.controller.Users:

Ext.define('AM.controller.Users', {
    extend: 'Ext.app.Controller',
    stores: ['Users'],
    models: ['User'],
    ...
});

By including the stores that our Users controller cares about in its definition they are automatically loaded onto the page and given a storeId, which makes them really easy to reference in our views (by simply configuring store: ‘Users’ in this case).

Proxy

here are two main types of Proxy - Client and Server. The Client proxies save their data locally and include the following subclasses:

  • LocalStorageProxy - saves its data to localStorage if the browser supports it
  • SessionStorageProxy - saves its data to sessionStorage if the browsers supports it
  • MemoryProxy - holds data in memory only, any data is lost when the page is refreshed

The Server proxies save their data by sending requests to some remote server. These proxies include:

  • Ajax - sends requests to a server on the same domain
  • JsonP - uses JSON-P to send requests to a server on a different domain
  • Rest - uses RESTful HTTP methods (GET/PUT/POST/DELETE) to communicate with server
  • Direct - uses Ext.direct.Manager to send requests

AjaxProxy

可以使用ationMethods来定义请求方法,默认读取使用GET,写使用POST。AjaxProxy不能跨域访问,如果需要跨域访问,需要使用JsonPProxy。与后台交互时,默认会使用proxy的api属性定义的请求路径,如果该路径未找到,则用url指定的路径。可以定义通过sorters和filters定义排序器和过滤器,并且设置传递给后台的参数名和参数格式,例子参看AjaxProxy文档。

Proxy可以定义在Model中,也可以定义在Store中,如:

Ext.define('User', {
    extend: 'Ext.data.Model',
    fields: ['id', 'name', 'age', 'gender'],
    proxy: {
        type: 'rest',
        url : 'data/users',
        reader: {
            type: 'json',
            root: 'users'
        }
    }
});

Proxy定义在Model中有如下好处:

首先,不同的Store都可以使用Model中配置的Proxy,而不需要重新配置。其次,Model可以脱离Store,直接和后台发生数据交互。当store中proxy和其Model的proxy不一样时,可以重定义Store的proxy,以覆盖Model中定义的proxy。