@(Ember)[MVVM|前端框架|HTML桌面应用]
经常有人质疑,在前端搞MV*有什么意义?也有人跟我提出这样的疑问:以AngularJS,Knockout,BackBone为代表的MV*框架,它跟jQuery有什么区别?我jQuery用得好好的,有什么必要再引入这种框架?
其实,不管我们使用的是一个类库还是一个框架,都不应该忘记我们最终目的,或许你正在为一个项目做技术选型,或许你正在为你的应用考虑代码重构,又或许你只是单纯的想做一些学术性研究,所以框架和类库的选择没有绝对只有最适合。
以jQuery为代表,针对界面上常见的DOM操作,远程请求,数据处理等作了封装,也有专注于处理数据的Underscore,而今天的主角Ember.js正是一款为构建富HTML桌面应用的理想框架。
Ember增强了简单的JavaScript对象模型,使之能够支持绑定和观察者,同时也支持一种更加强大的、基于混合的(mixin-based)代码共享途径。
作为最基本的形式,你可以使用Ember.Object
的extend
方法创建一个新的Ember类。
Person = Ember.Object.extend({
say: function(thing) {
alert(thing);
}
});
一旦你成功创建了一个新类,就可以使用create来创建类的实例了。类中定义的任何属性在实例中都是可用的。
var person = Person.create();
person.say("Hello") // alerts "Hello"
创建实例时,也可以通过传入对象来为实例增添额外的属性。
var tom = Person.create({
name: "Tom Dale",
helloWorld: function() {
this.say("Hi my name is " + this.get('name'));
}
});
tom.helloWorld() // alerts "Hi my name is Tom Dale"
由于Ember支持绑定和观察者,因此你可以随时通过get方法访问属性,也可以通过set方法设置属性。
当创建一个对象的新的实例时,也可以覆写类中定义的任何属性和方法。在本例中,作为例子,你可以覆写从Person
类继承的say
方法。
var yehuda = Person.create({
name: "Yehuda Katz",
say: function(thing) {
var name = this.get('name');
this._super(name + " says: " + thing);
}
});
你可以使用对象的_super
方法(super是JavaScript中的保留字)来调用被你覆写的原始方法。
你也可以使用extend方法为类创建子类。事实上,我们上面使用Ember.Object对象的extend方法创建新类时,即是创建了Ember.Object的子类。
var LoudPerson = Person.extend({
say: function(thing) {
this._super(thing.toUpperCase());
}
});
当创建子类时,你可以使用this._super
来调用被你覆写的方法。
无需一次性将类定义完全,你可以使用reopen
方法来重新打开(reopen)一个类并为其定义新的属性。
当使用reopen
时,你也同样可以覆写已经存在的方法并调用this._super
。
Person.reopen({
// override `say` to add an ! at the end
say: function(thing) {
this._super(thing + "!");
}
});
正如你所见,reopen
是用来为实例添加属性和方法的。而当你需要创建类的方法或为类本身添加属性时,则可使用reopenClass
。
Person.reopenClass({
createMan: function() {
return Person.create({isMan: true})
}
});
Person.createMan().get('isMan') // true
你可能经常需要一个基于其他属性计算而来的属性。Ember的对象模型可以使你很轻松地在常规的类定义中定义计算属性。
Person = Ember.Object.extend({
// these will be supplied by `create`
firstName: null,
lastName: null,
fullName: function() {
var firstName = this.get('firstName');
var lastName = this.get('lastName');
return firstName + ' ' + lastName;
}.property('firstName', 'lastName')
});
var tom = Person.create({
firstName: "Tom",
lastName: "Dale"
});
tom.get('fullName') // "Tom Dale"
当为类创建子类或者新的实例时,你可以覆写任何计算属性。
你也可以定义当设置计算属性时Ember所执行的动作。当你尝试设置计算属性时,可以使用对应的名-值(key and value)对其进行设置。
Person = Ember.Object.extend({
// these will be supplied by `create`
firstName: null,
lastName: null,
fullName: function(key, value) {
// getter
if (arguments.length === 1) {
var firstName = this.get('firstName');
var lastName = this.get('lastName');
return firstName + ' ' + lastName;
// setter
} else {
var name = value.split(" ");
this.set('firstName', name[0]);
this.set('lastName', name[1]);
return value;
}
}.property('firstName', 'lastName')
});
var person = Person.create();
person.set('fullName', "Peter Wagenet");
person.get('firstName') // Peter
person.get('lastName') // Wagenet
对于setter和getter,Ember都会调用计算属性,你可以通过检查参数的个数来确定该计算属性究竟是被getter还是setter调用的。
Ember支持观察任何属性,包括计算属性。你可以使用addObserver
方法在某个对象上设置观察者。
Person = Ember.Object.extend({
// these will be supplied by `create`
firstName: null,
lastName: null,
fullName: function() {
var firstName = this.get('firstName');
var lastName = this.get('lastName');
return firstName + ' ' + lastName;
}.property('firstName', 'lastName')
});
var person = Person.create({
firstName: "Yehuda",
lastName: "Katz"
});
person.addObserver('fullName', function() {
// deal with the change
});
person.set('firstName', "Brohuda"); // observer will fire
由于fullName
这个计算属性依赖于firstName
,所以更新firstName
也会触发fullName
上的观察者。
由于观察者的应用非常普遍,Ember提供了一种能够在类定义的内部定义观察者的方式。
Person.reopen({
fullNameChanged: function() {
// this is an inline version of .addObserver
}.observes('fullName')
});
绑定在两个属性之间创建了一个连接,这样一个属性改变时另一个也可以随之自动更新到最新的值。绑定也可以连接同一对象内的属性,或者跨越两个不同的对象。与其他大部分框架所包含的绑定实现不同的是,Ember.js的绑定可以用在任何对象上,而不仅仅用在视图和模型之间。
最简单的创建双向绑定的方法是,创建一个以Binding
字串结尾的属性,然后指定一个相对于全局作用域(global scope)的路径:
App.wife = Ember.Object.create({
householdIncome: 80000
});
App.husband = Ember.Object.create({
householdIncomeBinding: 'App.wife.householdIncome'
});
App.husband.get('householdIncome'); // 80000
// Someone gets raise.
App.husband.set('householdIncome', 90000);
App.wife.get('householdIncome'); // 90000
注意这个绑定不会立即更新,Ember会等待应用的所有代码运行完成之后再同步变更,因此你可以随意改变某个绑定的属性,而不用担心同步这些临时值所耗费的开销。
单向绑定只将变更单向传播。通常,单向绑定只是为了性能优化,你可以放心地使用更加简洁的双向绑定语法(当然,如果你总是只改变一边的话,那么双向绑定事实上也是单向绑定了)。
App.user = Ember.Object.create({
fullName: "Kara Gates"
});
App.userView = Ember.View.create({
userNameBinding: Ember.Binding.oneWay('App.user.fullName')
});
// Changing the name of the user object changes
// the value on the view.
App.user.set('fullName', "Krang Gates");
// App.userView.userName will become "Krang Gates"
// ...but changes to the view don't make it back to
// the object.
App.userView.set('userName', "Truckasaurus Gates");
App.user.get('fullName'); // "Krang Gates"
根据你的需求,有几种方式可以创建你的第一个Ember App.
如果你的需求较简单或者只是感兴趣随便玩玩,你可以下载Ember.js入门套件(Starter Kit),该入门套件基于HTML5 样板,无需任何构建工具、没有其他依赖。你也可以使用一些其他前端Scaffold工具来构建你的ember应用(例如: Yoeman )。当然你也可以根据自己的项目特点来构建自己的应用。
每个Ember app
都应该有一个Ember.Application
的实例。这个对象将会作为你的app中所有类和实例的全局可访问的命名空间(globally-accessible namespace)
。此外,它也在页面上设置了事件监听器,确保当用户与你的用户界面进行交互时你的视图可以接收到事件。
创建Ember.js应用程序的第一步是创建一个Ember.Application
类的实例化对象:
window.App = Ember.Application.create();
这里将实例化的对象命名为App,开发者可以根据应用程序的用途,选择意义相符的名字。
创建一个应用程序的实例对象非常重要,原因如下:
定义应用程序的命名空间,所有类都定义成该对象的属性(比如App.PostsView、App.PostsController)。这样做可以避免污染全局作用域。
在document上增加事件监听,并负责将事件发送给视图。
自动渲染模板,包括根模板,以及其他放入根模板的模板,都将被渲染。
基于当前URL创建路由器并开始路由。
Person.reopenClass({
createMan: function() {
return Person.create({isMan: true})
}
});
Person.createMan().get('isMan') // true
你可以随意为你的命名空间命名,不过必须以大写字母开头,这样绑定才能找到它。
如果要将Ember应用嵌入到某个现存的网站中,可以通过提供rootElement属性来将某个特定的元素作为事件监听器。
使用计算属性创建由其他属性综合决定的新属性。
计算属性不应该包含应用的行为,而且当调用时不应有任何附加影响。
除非在极少数情况下,多次调用相同的计算属性应该总是返回同样的值(当然,除了属性所依赖的属性值改变的情况。)
观察者应该包含反映其他属性变化的行为。
当你需要在绑定完成同步之后执行一些行为的时候,观察者会非常有用。
绑定通常用在确保位于不同层的对象总能保持同步。
例如,你使用Handlebars
绑定了你的视图和控制器,你也可能经常需要绑定同一层的两个对象。
例如,你可能有个App.selectedContactController
需要绑定在App.contactsController
的selectedContact
属性上