Jade Dungeon

Concepts

类型系统

命名规范

类名只有字母数字,大写开头。命名空间可以有多层,最外层命名空间开头大写。只有Sencha官方发布的可以用Ext作为最外层命名空间。

驼峰风格,不能有连着两个字符大写。

源代码

「源代码文件与目录」严格对应「类名与命名空间」。

方法与变量

同类,但以小写开头。

属性

同上。但要注意静态属性以大写开头:

Ext.MessageBox.YES = "Yes"
Ext.MessageBox.NO = "No"
MyCompany.alien.Math.PI = "4.13"

实践

声明

使用Ext.define定义类:

Ext.define(className, members, onClassCreated);
className
类名。
members
一组类成员的键值对。
onClassCreated
(可选)回调函数。

例子:

Ext.define( 'My.sample.Person', {
    name: 'Unknown',
    constructor: function(name) {
        if (name) {
            this.name = name;
        }
    },
    eat: function(foodType) {
        alert(this.name + " is eating: " + foodType);
    }
});

var aaron = Ext.create('My.sample.Person', 'Aaron');
aaron.eat("Salad"); // alert("Aaron is eating: Salad");

上面用Ext.create方法创建实例,而不推荐用new My.sample.Person()Ext.create是动态加载的实例。第一个参数是类名,后面的参数给类的构造函数。

配置

config成员通过Ext.Class的预处理器在类被创建前初始化:

  • config成员和类的其他成员放在一起。
  • config成员会在类创建期间自动生成没有被显式定义的getter/setter与相关方法。
  • 对每个config成员自动生成apply方法。在自动生成的setter方法设值前加入处理逻辑。

例子:

Ext.define('My.own.Window', {
   /** @readonly */
    isWindow: true,

    config: {
        title: 'Title Here',

        bottomBar: {
            enabled: true,
            height: 50,
            resizable: false
        }
    },

    constructor: function(config) {
        this.initConfig(config);
    },

    applyTitle: function(title) {
        if (!Ext.isString(title) || title.length === 0) {
            alert('Error: Title must be a valid non-empty string');
        }
        else {
            return title;
        }
    },

    applyBottomBar: function(bottomBar) {
        if (bottomBar && bottomBar.enabled) {
            if (!this.bottomBar) {
                return Ext.create('My.own.WindowBottomBar', bottomBar);
            }
            else {
                this.bottomBar.setConfig(bottomBar);
            }
        }
    }
});

调用方法:

var myWindow = Ext.create('My.own.Window', {
    title: 'Hello World',
    bottomBar: {
        height: 60
    }
});

alert(myWindow.getTitle()); // alerts "Hello World"

myWindow.setTitle('Something New');

alert(myWindow.getTitle()); // alerts "Something New"

myWindow.setTitle(null); // alerts "Error: Title must be a valid non-empty string"

myWindow.setBottomBar({ height: 100 }); // Bottom bar's height is changed to 100

静态成员

定义静态成员:

Ext.define('Computer', {
    statics: {
        instanceCount: 0,
        factory: function(brand) {
            // 'this' in static methods refer to the class itself
            return new this({brand: brand});
        }
    },

    config: {
        brand: null
    },

    constructor: function(config) {
        this.initConfig(config);

        // the 'self' property of an instance refers to its class
        this.self.instanceCount ++;
    }
});

var dellComputer = Computer.factory('Dell');
var appleComputer = Computer.factory('Mac');

alert(appleComputer.getBrand()); // using the auto-generated getter to get the value of a config property. Alerts "Mac"

alert(Computer.instanceCount); // Alerts "2"

异常处理与调试

抛异常时使用Ext.getDisplayName()取得任何方法的显示名。

throw new Error('['+ Ext.getDisplayName(arguments.callee) +'] Some message here');

任何由Ext.define()定义的类型抛出异常可以看到类名与方法名。

MVC架构

model
模型,如用户有usernamepassword
view
可以是任何组件:树形菜单、面板等。
controller
做程序运作起来的代码。

文件结构

sample

Ext.application({
	requires : [ 'Ext.container.Viewport' ],
	name : 'AM',
	appFolder : 'app',
	
	launch : function() {
		Ext.create('Ext.container.Viewport', {
			layout : 'fit',
			items : [ {
				xtype : 'panel',
				title : 'Users',
				html : 'List of users will go here'
			} ]
		});
	}
});

上面建立一个Ext.application实例,指定名称为AM。这个AM会自动成为全局变量, 注册命名空间到Ext.Loader对应appFolder指定的路径applaunch指定的回调方法创建了一个Viewport实例。里面有一个占满整个屏幕的Panel

sample

定义控制器

控制器监听事件并调用对应的动作。

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

	init : function() {
		console.log('Initialized Users! This happens '
				+ 'before the Application launch function is called');
	}
});

把上面的控制器加到application中去:

Ext.application({
    ...
    controllers: [ 'Users' ],
    ...
});

这样页面加载时控制器被调用,在浏览器控制台输出:

Initialized Users! This happens before the Application launch function is called

控制器还有一个control方法可以监听视图上的控件。下面的例子中我们在初始化方法里指定监听页面上viewport里的panel的渲染事件:

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

控制台输出:

The panel was rendered

注意指定监听对象的格式和css选择器很像。'viewport > panel'表示「所有直接属于viewportpanel」。详细规则查看文档目录下的:

docs/index.html#!/api/Ext.ComponentQuery

定义视图

一个查询结果的列表视图:

Ext.define('AM.view.user.List', {
	extend : 'Ext.grid.Panel',
	alias : 'widget.userlist',
	title : 'All Users',

	initComponent : function() {
		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);
	}
})

上面的视图类型是AM.view.user.List,别名是widget.userlist

在控制器里根据类型AM.view.user.List加上视图:

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

    views: [ 'user.List' ],

    init: ...

    onPanelRendered: ...
});

把原来applicationViewport里的panel换成我们的视图userList(根据别名):

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

sample

控制grid

注意onPanelRander还是被会调用,因为grid类还是panel的子类可以匹配viewport > panel选择器。

再给表格里的行添加双击事件,可以弹出对应记录编辑框:

Ext.define('AM.controller.Users', {
	...

	editUser : function(grid, record) {
		console.log('Double clicked on ' + record.get('name'));
	},

	init : function() {
		...

		this.control({
			'userlist' : {
				itemdblclick : this.editUser
			}
		});
	}
});

现在双击后能在控制抬输出信息。接下来要给编辑框做个视图:

Ext.define('AM.view.user.Edit', {
	extend : 'Ext.window.Window',
	alias : 'widget.useredit',

	title : 'Edit User',
	layout : 'fit',
	autoShow : true,

	initComponent : function() {
		this.items = [ {
			xtype : 'form',
			items : [ {
				xtype : 'textfield',
				name : 'name',
				fieldLabel : 'Name'
			}, {
				xtype : 'textfield',
				name : 'email',
				fieldLabel : 'Email'
			} ]
		} ];

		this.buttons = [ {
			text : 'Save',
			action : 'save'
		}, {
			text : 'Cancel',
			scope : this,
			handler : this.close
		} ];

		this.callParent(arguments);
	}
});

别忘记有了新视图要加到controller里,对应的事件里再弹出编辑框:

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

    views: [
        'user.List',
        'user.Edit'
    ],

    init: ...

    editUser: function(grid, record) {
        var view = Ext.widget('useredit');
        view.down('form').loadRecord(record);
    }
});

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

修改控制器添加stores模块把读取记录的源代码包含进来:

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

去掉视图里的硬编码记录:

Ext.define('AM.view.user.List' ,{
    extend: 'Ext.grid.Panel',
    alias: 'widget.userlist',
    title: 'All Users',

    // we no longer define the Users store in the `initComponent` method
    store: 'Users',

    initComponent: function() {
        this.columns = [
        ...
});

到目前为止我们已经把数据集合和程序分开了。但是还有可以改进的地方,因为每第记录里有哪些字段是定义在结果集里的。我们再把每个记录的结构抽象出来作为model:

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

把原来store里声明列名的地方用model代替:

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

Controller里指定model:

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

通过Model保存对象

controller里给保存按钮加上事件:

Ext.define('AM.controller.Users', {
    ...
    init: function() {
        this.control({
            'viewport > userlist': {
                itemdblclick: this.editUser
            },
            'useredit button[action=save]': {
                click: this.updateUser
            }
        });
    },
    ...
    updateUser: function(button) {
        console.log('clicked the Save button');
    }
    ...
});

从服务器读取记录

把硬编码在Store里的记录改成通过proxy从服务器上读取,注意加上autoLoad让它自动读取:

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

服务器端的程序就不写了,用个静态文件代替响应:

{
    "success": true,
    "users": [
        {"id": 1, "name": 'Ed',    "email": "ed@sencha.com"},
        {"id": 2, "name": 'Tommy', "email": "tommy@sencha.com"}
    ]
}

提交更新到服务器

提交函数中调用StoreSync()方法:

updateUser: function(button) {
    var win    = button.up('window'),
        form   = win.down('form'),
        record = form.getRecord(),
        values = form.getValues();

    record.set(values);
    win.close();
    // synchronize the store after editing the record
    this.getUsersStore().sync();
}

更新Store中的proxy,为读写方法指定不同的接口:

	proxy : {
		type : 'ajax',
		api : {
			read : 'data/users.json',
			update : 'data/updateUsers.json'
		},
		reader : {
			type : 'json',
			root : 'users',
			successProperty : 'success'
		}
	}

布局与容器

容器

更多关于组件的信息:

docs/index.html#/guide/components

容器中可以放置组件或其他容器。如我们在页面上加一个PanelPanel里再加两个子Panel

Ext.create('Ext.panel.Panel', {
    renderTo: Ext.getBody(),
    width: 400,
    height: 300,
    title: 'Container Panel',
    items: [
        {
            xtype: 'panel',
            title: 'Child Panel 1',
            height: 100,
            width: '75%'
        },
        {
            xtype: 'panel',
            title: 'Child Panel 2',
            height: 100,
            width: '75%'
        }
    ]
});

sample

布局

容器都有布局属性来描述它内部的组件。

使用布局

在上面的例子中布局是上下放的,就和HTML的DOM行为一样。现在用colum指定左右放:

Ext.create('Ext.panel.Panel', {
    renderTo: Ext.getBody(),
    width: 400,
    height: 200,
    title: 'Container Panel',
    layout: 'column',
    items: [
        {
            xtype: 'panel',
            title: 'Child Panel 1',
            height: 100,
            columnWidth: 0.5
        },
        {
            xtype: 'panel',
            title: 'Child Panel 2',
            height: 100,
            columnWidth: 0.5
        }
    ]
});

sample

布局是如何工作的

窗口的布局负责初始化所有内部组件的大小和初始位置。在内部框架调用窗口的doLayout方法,而doLayout方法是层层包含的,所以所有内部成员的doLayout方法都会被调用。

窗口大小的调整与内部组件的添加删除都会触发重新布局。虽然一般布局的调用都由框架来处理,但有些情况下我们希望等我们做好一些工作以后手动触发。这种情况下我们打开suspendLayout标记来防止自动布局。等处理完成以后再关闭suspendLayout来调用布局:

var containerPanel = Ext.create('Ext.panel.Panel', {
    renderTo: Ext.getBody(),
    width: 400,
    height: 200,
    title: 'Container Panel',
    layout: 'column',
    suspendLayout: true // Suspend automatic layouts while we do several different things that could trigger a layout on their own
});
// Add a couple of child items.  We could add these both at the same time by passing an array to add(),
// but lets pretend we needed to add them separately for some reason.
containerPanel.add({
    xtype: 'panel',
    title: 'Child Panel 1',
    height: 100,
    columnWidth: 0.5
});
containerPanel.add({
    xtype: 'panel',
    title: 'Child Panel 2',
    height: 100,
    columnWidth: 0.5
});
// Turn the suspendLayout flag off.
containerPanel.suspendLayout = false;
// Trigger a layout.
containerPanel.doLayout();

组件

所有的组件都继承自Ext.Component

组件组合结构

Container是一种特殊的组件,负责管理内部组件的生命周期与渲染工作。它包含的成员一般用下图来表示,顶部是Viewport容器。

sample

Ext.create()创建组件实例,items指定窗口内部成员:

var childPanel1 = Ext.create('Ext.panel.Panel', {
    title: 'Child Panel 1',
    html: 'A Panel'
});

var childPanel2 = Ext.create('Ext.panel.Panel', {
    title: 'Child Panel 2',
    html: 'Another Panel'
});

Ext.create('Ext.container.Viewport', {
    items: [ childPanel1, childPanel2 ]
});

sample