Ember JS提供一套自己的类系统,普通的JavaScript
标准类不能自动更新属性值,Ember JS的类会自动触发观察者,自动更新属性值、自动刷新模板上的属性值。如果一个类是Ember JS提供的可以看到前缀命名空间是Ember.Object
。Ember
类定义使用extend()
方法,创建类实例使用create()
方法,可以在方法传入参数,但是参数要以hash
列表方式传入。
Ember JS重写了标准JavaScript
的数组类Array
,并且为了与标准JavaScript
类区别命名为Ember.Enumerable
(API介绍)
Ember JS还扩展了String
属性的特性,提供了一系列特有的处理方法,API介绍。
关于类的命名规则在此不做介绍,自己网上找一份Java
的命名规则的教材看看即可。
开始之前先做好准备工作,首先创建一个HTML文件,并引入Ember JS所必须的文件(后面会介绍一种更加简单的方法去搭建EmberJS
的项目方法,当然如果你有时间也可以提前去了解,这种方式是使用Ember CLI
搭建EmberJS
的项目)。
Ember.js • Guides // 在这里编写Ember代码
上面代码是一个简单的HTML
文件,所需的Ember
库直接使用CDN
。
下面定义一个Person
类,定义方式如下:
Person = Ember.Object.extend({ say(thing) { alert(name); }});
上面代码定义了一个Person
类,并且在类里面还定义了一个方法say
,方法传入一个参数thing
。方法体仅仅是打印了传入的参数。
在子类重写父类的方法,并在方法里调用_super()
方法来调用父类中对应的方法触发父类方法的行为。
Person = Ember.Object.extend({ say(thing) { var name = this.get('name'); alert(name + " says: " + thing); }});Soldier = Person.extend({ say(thing) { // this will call the method in the parent class (Person#say), appending // the string ", sir!" to the variable `thing` passed in this._super(thing + ", sir!"); }});var yehuda = Soldier.create({ name: "Yehuda Katz"});yehuda.say("Yes"); // alerts "Yehuda Katz says: Yes, sir!"
运行代码,刷新浏览器,可以看到如下结果:
结果正确了,但是我们还不知道类是怎么初始化的,它初始化的顺序又是怎么样的呢?其实每个类都有一个默认的初始化方法,555……别急,接着往下看。
要获取一个类的实例只需要调用类的create()
方法即可。
Person = Ember.Object.extend({ show() { console.log("My name is " + this.get('name')); }});var person = Person.create({ name: 'ubuntuvim'});person.show(); // My name is ubuntuvim var person2 = Person.create({ pwd: 'ubuntuvim'});// 由于创建person2的时候没有设置name的值,默认是undefinedperson2.show(); // My name is undefined
注意:处于性能的考虑在使用create()
方法创建实例的时候,不允许新定义、重写计算属性,也不推荐新定义、重写普通方法,Ember
推荐在使用create()
方法时只是传递简单的参数,比如上述代码的{name: 'ubuntuvim'}
。如果你需要新地定义、重写方法请新建一个子类来实现。
在create()
方法内定义计算属性,运行后会直接报如下图的报错信息。
Person = Ember.Object.create({ show() { console.log("My name is " + this.get('name')); }, fullName: Ember.computed(function() { console.log("computed properties."); })});
前面提过,我们在类继承的时候到底类是怎么初始化,这节就介绍类的初始化,Ember
定义了一个init()
方法,此方法在类被实例化的时候自动调用。
Parent = Ember.Object.extend({ init() { console.log("parent init..."); }, show() { console.log("My name is " + this.get('name')); }, others() { console.log("the method in parent class.."); }});//parent = Parent.create({// name: 'parent'//}); Child = Parent.extend({ init() { console.log("child init..."); }, show() { this._super(); }});child = Child.create({ name: 'child'}); child.show();child.others();
注意:init()
方法只有在类的create()
方法被调用的时候才会被自动调用,上面的例子中,如果只是child.others()
这个方法父类并不会调用init()
方法,只有执行Parent.create()
这个调用的时候才会执行init()
方法。上面代码如果把Parent.create()
这几句代码注释掉得到的结果如下:
可见父类的init()
方法没有被调用,然后修改代码,注释掉child.others()
这句,再把Parent.create()
这几句的注释去掉。得到如下结果
可以看到父类的init()
方法被调用了!由此可见init()
方法是在调用create()
方法的时候才调用的。在项目中有可能你需要继承Ember
提供的组件,比如继承Ember.Component
类,此时你就要注意了,在你继承Ember
的组件的时候你必须显式的调用父类方法this._super()
否则你继承得到的类无法获取Component
提供的行为或者得到无法预知的结果。
Ember
建议访问类的属性使用get、set
方法。如果你直接使用obj.prop
这种方式访问也是可以得到类的属性值,但是如果你不是使用访问器操作的就会导致很多问题:计算属性不能被重新计算、无法察觉对象属性的变化、模板也不能自动更新。
Person = Ember.Object.extend({ name: 'ubuntuvim'});// Ember 推荐的访问方式var person = Person.create();console.log("My name is " + person.get('name'));person.set('name', "Tobias Funke");console.log("My name is " + person.get('name')); console.log("---------------------------");// 不推荐的方式var person2 = Person.create(); console.log("My name is " + person2.name);person2.name = "Tobias Funke";console.log("My name is " + person2.name);
Ember为我们封装了get、set
实现细节,开发者直接使用即可。
最后感谢唯獨莪靑睐的指正。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用在github项目上给我个star
吧。您的肯定对我来说是最大的动力!!
reopen
不知道怎么翻译好,如果按照reopen
翻译过来应该是“重新打开”,但是总觉得不顺,所以就译成扩展
了,如果有不妥请指正。
当你想扩展一个类你可以直接使用reopen()
方法为一个已经定义好的类添加属性、方法。如果是使用extend()
方法你需要重新定义一个子类,然后在子类中添加新的属性、方法。前一篇所过,调用create()
方法时候不能传入计算属性并且不推荐在此方法中新定义、重写方法,但是使用reopen()
方法可以弥补create()
方法的补足。与extend()
方法非常相似,下面的代码演示了它们的不同之处。
Parent = Ember.Object.extend({ name: 'ubuntuvim', fun1() { console.log("The name is " + this.name); }, common() { console.log("common method..."); }}); // 使用extend()方法添加新的属性、方法Child1 = Parent.extend({ // 给类Parent为新增一个属性 pwd: '12345', // 给类Parent为新增一个方法 fun2() { console.log("The pwd is " + this.pwd); }, // 重写父类的common()方法 common() { //console.log("override common method of parent..."); this._super(); }}); var c1 = Child1.create();console.log("name = " + c1.get('name') + ", pwd = " + c1.get('pwd')); c1.fun1();c1.fun2(); c1.common();console.log("-----------------------"); // 使用reopen()方法添加新的属性、方法Parent.reopen({ // 给类Parent为新增一个属性 pwd: '12345', // 给类Parent为新增一个方法 fun2() { console.log("The pwd is " + this.pwd); }, // 重写类本身common()方法 common() { console.log("override common method by reopen method..."); //this._super(); }, // 新增一个计算属性 fullName: Ember.computed(function() { console.log("compute method..."); })});var p = Parent.create(); console.log("name = " + p.get('name') + ", pwd = " + p.get('pwd')); p.fun1();p.fun2(); p.common();console.log("---------------------------"); p.get('fullName'); // 获取计算属性值,这里是直接输出:compute method...// 使用extend()方法添加新的属性、方法Child2 = Parent.extend({ // 给类Parent为新增一个属性 pwd: '12345', // 给类Parent为新增一个方法 fun2() { console.log("The pwd is " + this.pwd); }, // 重写父类的common()方法 common() { //console.log("override common method of parent..."); this._super(); }}); var c2 = Child2.create();console.log("name = " + c2.get('name') + ", pwd = " + c2.get('pwd')); c2.fun1();c2.fun2(); c2.common();
从执行结果可以看到如下的差异:
同点: 都可以用于扩展某个类。
异点:
extend
需要重新定义一个类并且要继承被扩展的类;reopen
是在被扩展的类本身上新增属性、方法,可以扩展计算属性(相比create()
方法); 到底用那个方法有实际情况而定,reopen
方法会改变了原类的行为(可以想象为修改了对象的原型对象的方法和属性),就如演示实例一样在reopen
方法之后调用的Child2
类的common
方法的行为已经改改变了,在编码过程忘记之前已经调用过reopen
方法就有可能出现自己都不知道怎么回事的问题!如果是extend
方法会导致类越来越多,继承树也会越来越深,对性能、调试也是一大挑战,但是extend
不会改变被继承类的行为。
使用reopenClass()
方法可以扩展static
类型的属性、方法。
Parent = Ember.Object.extend(); // 使用reopenClass()方法添加新的static属性、方法Parent.reopenClass({ isPerson: true, username: 'blog.ddlisting.com' //,name: 'test' //这里有点奇怪,不知道为何不能使用名称为name定义属性,会提示这个是自读属性,使用username却没问题!!估计name是这个方法的保留关键字});Parent.reopen({ isPerson: false, name: 'ubuntuvim'});console.log(Parent.isPerson);console.log(Parent.name); // 输出空console.log(Parent.create().get('isPerson'));console.log(Parent.create().get('name')); // 输出 ubuntuvim
对于在reopenClass
方法中使用属性name
的问题下面的地址有解释
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用在github项目上给我个star
吧。您的肯定对我来说是最大的动力!!
简单地来说,计算属性就是将函数声明为属性,就类似于调用了一个函数,Ember
会自动调用这个函数。计算属性最大的特点就是能自动检测变化,及时更新数据。
Person = Ember.Object.extend({ firstName: null, lastName: null, // fullName 就是一个计算属性 fullName: Ember.computed('firstName', 'lastName', function() { return this.get('firstName') + ", " + this.get('lastName'); })});// 实例化同时传入参数var piter = Person.create({ firstName: 'chen', lastName: 'ubuntuvim'});console.log(piter.get('fullName')); // output >> chen, ubuntuvim
计算属性其实就是一个函数,如果你接触过就jQuery、Extjs
相信你会非常熟悉,在这两个框架中函数就是这么定义的。只不过在Ember
中,把这种函数当做属性来处理,并且可以通过get获取函数的返回值。
在Ember
程序中,计算属性还能调用另外一个计算属性,形成计算属性链,也可以用于扩展某个方法。在上一实例的基础上增加一个description()
方法。
Person = Ember.Object.extend({ firstName: null, lastName: null, age: null, county: null, // fullName 就是一个计算属性 fullName: Ember.computed('firstName', 'lastName', function() { return this.get('firstName') + ", " + this.get('lastName'); }), description: Ember.computed('fullName', 'age', 'county', function() { return this.get('fullName') + " age " + this.get('age') + " county " + this.get('county'); })});// 实例化同时传入参数var piter = Person.create({ firstName: 'chen', lastName: 'ubuntuvim', age: 25, county: 'china'});console.log(piter.get('description')); // output >> chen, ubuntuvim
当用户使用set
方法改变firstName
的值,然后再调用get('description')
得到的值也是更新后的值。
注意要把重写的属性作为参数传入computed
方法,要区别计算属性的定义方法,定义的时候computed
方法的最后一个参数是一个function
,而重写的时候最后一个参数是一个hash
。
// 重写计算属性的get、set方法Person = Ember.Object.extend({ firstName: null, lastName: null, // 重写计算属性fullName的get、set方法 fullName: Ember.computed('firstName', 'lastName', { get(key) { return this.get('firstName') + "," + this.get('lastName'); }, set(key, value) { // 这个官方文档使用的代码,但是我运行的时候出现 Uncaught SyntaxError: Unexpected token [ 这个错误,不知道是否是缺少某个文件,后续会补上;// console.log("value = " + value);// var [ firstName, lastName ] = value.split(/s+/); var firstName = value.split(/s+/)[0]; var lastName = value.split(/s+/)[1]; this.set('firstName', firstName); this.set('lastName', lastName); } }),// 对于普通的属性无法重写get、set方法// firstName: Ember.computed('firstName', {// get(key) {// return this.get('firstName') + "@@";// },// set(key, value) {// this.set('firstName', value);// }// })}); var jack = Person.create(); jack.set('fullName', "james kobe");console.log(jack.get('firstName'));console.log(jack.get('lastName'));
我们经常会遇到这种情况:某个计算属性值是依赖某个数组或者其他对象的,比如在Ember
的todos
这个例子中有这样的一段代码。
export default Ember.Controller.extend({ todos: [ Ember.Object.create({ isDone: true }), Ember.Object.create({ isDone: false }), Ember.Object.create({ isDone: true }) ], remaining: Ember.computed('todos.@each.isDone', function() { var todos = this.get('todos'); return todos.filterBy('isDone', false).get('length'); })});
计算属性remaining
的值于依赖数组todos
。在这里还有个知识点:在上述代码computed()
方法里有一个todos.@each.isDone
这样的键,里面包含了一个特别的键@each
(后面还会看到更特别的键[]
)。需要注意的是这种键不能嵌套并且是只能获取一个层次的属性。比如todos.@each.foo.name
(获取多层次属性,这里是先得到foo再获取name
)或者todos.@each.owner.@each.name
(嵌套)这两种方式都是不允许的。
在如下4种情况Ember
会自动更新绑定的计算属性值:<br>1.在todos
数组中任意一个对象的isDone
属性值发生变化的时候;2.往todos
数组新增元素的时候;3.从todos
数组删除元素的时候;4.在控制器中todos
数组被改变为其他的数组的时候;
比如下面代码演示的结果;
Task = Ember.Object.extend({ isDone: false // 默认为false}); WorkerLists = Ember.Object.extend({ // 定义一个Task对象数组 lists: [ Task.create({ isDone: false }), Task.create({ isDone: true }), Task.create(), Task.create({ isDone: true }), Task.create({ isDone: true }), Task.create({ isDone: true }), Task.create({ isDone: false }), Task.create({ isDone: true }) ], remaining: Ember.computed('lists.@each.isDone', function() { var lists = this.get('lists'); // 先查询属性isDone值为false的对象,再返回其数量 return lists.filterBy('isDone', false).get('length'); })});// 如下代码使用到的API请查看:http://emberjs.com/api/classes/Ember.MutableArray.htmlvar wl = WorkerLists.create();// 所有isDone属性值未做任何修改console.log('1,>> Not complete lenght is ' + wl.get('remaining')); // output 3var lists = wl.get('lists'); // 得到对象内的数组// ----- 演示第一种情况: 1. 在todos数组中任意一个对象的isDone属性值发生变化的时候;// 修改数组一个元素的isDone的 值var item1 = lists.objectAt(3); // 得到第4个元素 objectAt()方法是Ember为我们提供的// console.log('item1 = ' + item1);item1.set('isDone', false);console.log('2,>> Not complete lenght is ' + wl.get('remaining')); // output 4// --------- 2. 往todos数组新增元素的时候;lists.pushObject(Task.create({ isDone: false })); //新增一个isDone为false的对象console.log('3,>> Not complete lenght is ' + wl.get('remaining')); // output 5// --------- 3. 从todos数组删除元素的时候;lists.removeObject(item1); // 删除了一个元素console.log('4,>> Not complete lenght is ' + wl.get('remaining')); // output 4// --------- 4. 在控制器中todos数组被改变为其他的数组的时候;// 创建一个ControllerTodosController = Ember.Controller.extend({ // 在控制器内定义另外一个Task对象数组 todosInController: [ Task.create({ isDone: false }), Task.create({ isDone: true }) ], // 使用键”@each.isDone“遍历得到的filterBy()方法过滤后的对象的isDone属性 remaining: function() { // remaining()方法返回的是控制器内的数组 return this.get('todosInController').filterBy('isDone', false).get('length'); }.property('@each.isDone') // 指定遍历的属性});todosController = TodosController.create();var count = todosController.get('remaining');console.log('5,>> Not complete lenght is ' + count); // output 1
上述的情况中,我们对数组对象的是关注点是在对象的属性上,但是实际中往往很多情况我们并不关系对象内的属性是否变化了,而是把数组元素作为一个整体对象处理(比如数组元素个数的变化)。相比上述的代码下面的代码检测的是数组对象元素的变化,而不是对象的isDone
属性的变化。在这种情况你可以看看下面例子,在例子中使用键[]
代替键@each
。从键的变化也可以看出他们的不同之处。
Task = Ember.Object.extend({ isDone: false, // 默认为false name: 'taskName', // 为了显示结果方便,重写toString()方法 toString: function() { return '[name = '+this.get('name')+', isDone = '+this.get('isDone')+']'; }}); WorkerLists = Ember.Object.extend({ // 定义一个Task对象数组 lists: [ Task.create({ isDone: false, name: 'ibeginner.sinaapp.com' }), Task.create({ isDone: true, name: 'i2cao.xyz' }), Task.create(), Task.create({ isDone: true, name: 'ubuntuvim' }), Task.create({ isDone: true , name: '1527254027@qq.com'}), Task.create({ isDone: true }) ], index: null, indexOfSelectedTodo: Ember.computed('index', 'lists.[]', function() { return this.get('lists').objectAt(this.get('index')); })});var wl = WorkerLists.create();// 所有isDone属性值未做任何修改var index = 1;wl.set('index', index);console.log('Get '+wl.get('indexOfSelectedTodo').toString()+' by index ' + index);
Ember.computed
这个组件中有很多使用键[]
实现的方法。当你想创建一个计算属性是数组的时候特别适用。你可以使用Ember.computed.map
来构建你的计算属性。
const Hamster = Ember.Object.extend({ chores: null, excitingChores: Ember.computed('chores.[]', function() { //告诉Ember chores是一个数组 return this.get('chores').map(function(chore, index) { //return `${index} --> ${chore.toUpperCase()}`; // 可以使用${}表达式,并且在表达式内可以直接调用js方法 return `${chore}`; //返回元素值 }); })});// 为数组赋值const hamster = Hamster.create({ // 名字chores要与类Hamster定义指定数组的名字一致 chores: ['First Value', 'write more unit tests']});console.log(hamster.get('excitingChores'));hamster.get('chores').pushObject("Add item test"); //add an item to chores arrayconsole.log(hamster.get('excitingChores'));
Ember
还提供了另外一种方式去定义数组类型的计算属性。
const Hamster = Ember.Object.extend({ chores: null, excitingChores: Ember.computed('chores.[]', function() { return this.get('chores').map(function(chore, index) { //return `${index} --> ${chore.toUpperCase()}`; // 可以使用${}表达式,并且在表达式内可以直接调用js方法 return `${chore}`; //返回元素值 }); })});// 为数组赋值const hamster = Hamster.create({ // 名字chores要与类Hamster定义指定数组的名字一致 chores: ['First Value', 'write more unit tests']});console.log(hamster.get('excitingChores'));hamster.get('chores').pushObject("Add item test"); //add an item to chores arrayconsole.log(hamster.get('excitingChores'));
<br>博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用在github项目上给我个star
吧。您的肯定对我来说是最大的动力!!
Ember
可以检测任何属性的变化,包括计算属性。
Ember
可以察觉所有属性的变化,包括计算属性。观察者是非常有用的,特别是计算属性绑定之后需要同步的时候。观察者经常被Ember开发过度使用。Ember
框架本身已经大量使用观察者,但是对于大多数的开发者面对开发问题时使用计算属性是更适合的解决方案。使用方式:可以用Ember.observer
创建一个对象为观察者。
// Observer对于Emberjs来说非常重要,前面你看到的很多代码都是与它有关系,计算属性之所以能更新也是因为它Person = Ember.Object.extend({ firstName: null, lastName: null, fullName: Ember.computed('firstName', 'lastName', function() { return this.get('firstName') + " " + this.get('lastName'); }), // 当fullName被改变的时候触发观察者 fullNameChange: Ember.observer('fullName', function() { console.log("The fullName is changed by caller"); //return this.get('fullName'); })});var person = Person.create({ firstName: 'chen', lastName: 'ubuntuvim'});// 如果被观察的计算属性还没执行过get()方法不会触发观察者console.log('fullName = ' + person.get('fullName')); // fullName是依赖firstName和lastName的,这里改变了firstName的值,计算属性会自动更新,// fullName被改变了所以会触发观察者person.set('firstName', 'change firstName value'); // 观察者会被触发console.log('fullName = ' + person.get('fullName'));
fullName
是依赖firstName
和lastName
的,调用set()
方法改变了firstName
的值,自然的导致fullName
的值也被改变了,fullName
变化了就触发观察者。从执行的结果就可以看出来;
Ember
还为开发者提供了另一种使用观察者的方式。这种方式使你可以在类定义之外为某个计算属性增加一个观察者。
person.addObserver('fullName', function() { // deal with the change…});
目前,观察者在Ember
中是同步的(不是笔误,官网就是这么说的Observers in Ember are currently synchronous.
)。这就意味着只要计算属性一发生变化就会触发观察者。也因为这个原因很容易就会引入这样的bug
在计算属性没有同步的时候。比如下面的代码;
Person.reopen({ lastNameChanged: Ember.observer('lastName', function() { // The observer depends on lastName and so does fullName. Because observers // are synchronous, when this function is called the value of fullName is // not updated yet so this will log the old value of fullName console.log(this.get('fullName')); })});
然而由于同步的原因如果你的的观察者同时观察多个属性,就会导致观察者执行多次。
person = Ember.Object.extend({ firstName: null, lastName: null, fullName: Ember.computed('firstName', 'lastName', function() { return this.get('firstName') + " " + this.get('lastName'); }), // 当fullName被改变的时候触发观察者 fullNameChange: Ember.observer('fullName', function() { console.log("The fullName is changed by caller"); //return this.get('fullName'); })});Person.reopen({ partOfNameChanged: Ember.observer('firstName', 'lastName', function() { // 同时观察了firstName和lastName两个属性 console.log('========partOfNameChanged======'); })});var person = Person.create({ firstName: 'chen', lastName: 'ubuntuvim'});person.set('firstName', '[firstName]');person.set('lastName', '[lastName]');
显然上述代码执行了两次set()
所以观察者也会执行2次,但是如果开发中需要设置只能执行一次观察出呢?Ember提供了一个once()
方法,这个方法会在下一次循环所有绑定属性都同步的时候执行。
Person = Ember.Object.extend({ firstName: null, lastName: null, fullName: Ember.computed('firstName', 'lastName', function() { return this.get('firstName') + " " + this.get('lastName'); }), // 当fullName被改变的时候触发观察者 fullNameChange: Ember.observer('fullName', function() { console.log("The fullName is changed by caller"); //return this.get('fullName'); })});Person.reopen({ partOfNameChanged: Ember.observer('firstName', 'lastName', function() { // 同时观察了firstName和lastName两个属性 // 方法partOfNameChanged本身还是会执行多次,但是方法processFullName只会执行一次 console.log('========partOfNameChanged======'); // Ember.run.once(this, 'processFullName'); }), processFullName: Ember.observer('fullName', function() { // 当你同时设置多个属性的时候,此观察者只会执行一次,并且是发生在下一次所有属性都被同步的时候 console.log('fullName = ' + this.get('fullName')); })});var person = Person.create({ firstName: 'chen', lastName: 'ubuntuvim'});person.set('firstName', '[firstName]');person.set('lastName', '[lastName]');
观察者一直到对象初始化完成之后才会执行。如果你想观察者在对象初始化的时候就执行你必须要手动调用Ember.on()
方法。这个方法会在对象初始化之后就执行。
Person = Ember.Object.extend({ salutation:null, init() { this.set('salutation', 'hello'); console.log('init....'); }, salutationDidChange: Ember.on('init', Ember.observer('salutation', function() { console.log('salutationDidChange......'); }))});var p = Person.create();p.get('salutationDidChange'); // output > init.... salutationDidChange......console.log(p.get('salutation')); // output > hellop.set('salutation'); // output > salutationDidChange......
如果一个计算属性从来没有调用过get()
方法获取的其值,观察者就不会被触发,即使是计算属性的值发生变化了。你可以这么认为,观察者是根据调用get()
方法前后的值比较判断出计算属性值是否发生改变了。如果没调用过get()
之前的改变观察者认为是没有变化。通常我们不需要担心这个问题会影响到程序代码,因为几乎所有被观察的计算属性在触发前都会执行取值操作。如果你仍然担心观察者不会被触发,你可以在init()
方法了执行一次get
操作。这样足以保证你的观察在触发之前是执行过get操作的。
对于初学者来说,属性值的自动更新还是有点难以理解,到底它是怎么个更新法!!!先别急,先放一放,随着不断深入学习你就会了解到这个是多么强大的特性。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用在github项目上给我个star
吧。您的肯定对我来说是最大的动力!!
正如其他的框架一样,Ember
也有它特有的数据绑定方式,并且可以在任何一个对象上使用绑定。而然,数据绑定大多数情况都是使用在Ember
框架本身,对于开发者最好还是使用计算属性更为简单方便。
// 双向绑定Wife = Ember.Object.extend({ householdIncome: 800});var wife = Wife.create();Hasband = Ember.Object.extend({ // 使用 alias方法实现绑定 householdIncome: Ember.computed.alias('wife.householdIncome')});hasband = Hasband.create({ wife: wife});console.log('householdIncome = ' + hasband.get('householdIncome')); // output > 800// 可以双向设置值// 在wife方设置值wife.set('householdIncome', 1000);console.log('householdIncome = ' + hasband.get('householdIncome')); // output > 1000// 在hasband方设置值hasband.set('householdIncome', 10);console.log('wife householdIncome = ' + wife.get('householdIncome'));
需要注意的是绑定并不会立刻更新对应的值,Ember
会等待直到程序代码完成运行完成并且是在同步改变之前,所以你可以多次改变计算属性的值。由于绑定是很短暂的所以也不需要担心开销问题。
单向绑定只会在一个方向上传播变化。相对双向绑定来说,单向绑定做了性能优化,对于双向绑定来说如果你只是在一个方向上设置关联其实就是一个单向绑定。
var user = Ember.Object.create({ fullName: 'Kara Gates'});UserComponent = Ember.Component.extend({ userName: Ember.computed.oneWay('user.fullName')});userComponent = UserComponent.create({ user: user});console.log('fullName = ' + user.get('fullName'));// 从user可以设置user.set('fullName', "krang Gates");console.log('component>> ' + userComponent.get('userName'));// UserComponent 设置值,user并不能获取,因为是单向的绑定userComponent.set('fullName', "ubuntuvim");console.log('user >>> ' + user.get('fullName'));
关于数据绑定的知识点不多,相对来说不是重点,毕竟对象之间的关联关系是越少、越简单越好。关联关系多了反而难以维护。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用在github项目上给我个star
吧。您的肯定对我来说是最大的动力!!
在Ember
中,枚举是包含多个子对象的对象,并且提供了丰富的API(Ember.Enumerable API)去获取所包含的子对象。Ember
的枚举都是基于原生的javascript
数组实现的,Ember
扩展了其中的很多接口。Ember
提供一个标准化接口处理枚举,并且允许开发者完全改变底层数据存储,而无需修改应用程序的数据访问代码。Ember
的Enumerable API
尽可能的遵照ECMAScript
规范。为了减少与其他库不兼容,Ember
允许你使用本地浏览器实现数组。
下面是一些重要而常用的API
列表;请注意左右两列的不同。
标准方法 | 可被观察方法 | 说明 |
pop | popObject | 该函数从从数组中删除最后项,并返回该删除项 |
push | pushObject | 新增元素 |
reverse | reverseObject | 颠倒数组元素 |
shift | shiftObject | 把数组的第一个元素从其中删除,并返回第一个元素的值 |
unshift | unshiftObject | 可向数组的开头添加一个或更多元素,并返回新的长度 |
详细文档请看:http://emberjs.com/api/classes/Ember.Enumerable.html
在列表上右侧的方法是Ember
重写标准的JavaScript
方法而得的,他们最大的不同之处是使用普通的方法(左边部分)操作的数组不会在你的应用程序中自动更新(不会触发观察者),而使用Ember
重写过的方法则可以触发观察者,只要你的数据有变化Ember
就可以观察到,并且更新到模板上。
遍历数组元素使用forEach
方法。
var arr = ['chen', 'ubuntuvm', '1527254027@qq.com', 'i2cao.xyz', 'ubuntuvim.xyz'];arr.forEach(function(item, index) { console.log(index+1 + ", " +item);});
// 获取头尾的元素,直接调用Ember封装好的firstObject和lastObject方法即可console.log('The firstItem is ' + arr.get('firstObject')); // output> chenconsole.log('The lastItem is ' + arr.get('lastObject')); //output> ubuntuvim.xyz
// map方法,转换数组,并且可以在回调函数里添加自己的逻辑// map方法会新建一个数组,并且返回被转换数组的元素var arrMap = arr.map(function(item) { return 'map: ' + item; // 增加自己的所需要的逻辑处理});arrMap.forEach(function(item, index) { console.log(item);});console.log('-----------------------------------------------');
// mapBy 方法:返回对象属性的集合,// 当你的数组元素是一个对象的时候,你可以根据对象的属性名获取对应值var obj1 = Ember.Object.create({ username: '123', age: 25}); var obj2 = Ember.Object.create({ username: 'name', age: 35});var obj3 = Ember.Object.create({ username: 'user', age: 40}); var obj4 = Ember.Object.create({ age: 40}); var arrObj = [obj1, obj2, obj3, obj4]; //对象数组var tmp = arrObj.mapBy('username'); // tmp.forEach(function(item, index) { console.log(index+1+", "+item);}); console.log('-----------------------------------------------');
// filter 过滤器方法,过滤普通数组元素// filter方法可以跟你指定的条件过滤掉不匹配的数据,比如下面的代码:过滤了元素大于4的元素var nums = [1, 2, 3, 4, 5];// 参数self值数组本身var numsTmp = nums.filter(function(item, index, self) { return item < 4;}); numsTmp.forEach(function(item, index) { console.log('item = ' + item); // 1, 2, 3});console.log('-----------------------------------------------');
filter
方法会返回所有判断为true
的元素,会把判断结果为false
或者undefined
的元素过滤掉。
// 如果你想根据对象的某个属性过滤数组你需要用filterBy方法,比如下面的代码根据isDone这个对象属性过滤var o1 = Ember.Object.create({ name: 'u1', isDone: true}); var o2 = Ember.Object.create({ name: 'u2', isDone: false}); var o3 = Ember.Object.create({ name: 'u3', isDone: true}); var o4 = Ember.Object.create({ name: 'u4', isDone: true}); var todos = [o1, o2, o3, o4];var isDoneArr = todos.filterBy('isDone', true); //会把o2过滤掉isDoneArr.forEach(function(item, index) { console.log('name = ' + item.get('name') + ', isDone = ' + item.get('isDone')); // console.log(item);}); console.log('-----------------------------------------------');
filter
和filterBy
不同的地方是前者可以自定义过滤逻辑,后者可以直接使用。
// every、some 方法// every 用于判断数组的所有元素是否符合条件,如果所有元素都符合指定的判断条件则返回true,否则返回false// some 用于判断数组的所有元素只要有一个元素符合条件就返回true,否则返回falsePerson = Ember.Object.extend({ name: null, isHappy: true});var people = [ Person.create({ name: 'chen', isHappy: true }), Person.create({ name: 'ubuntuvim', isHappy: false }), Person.create({ name: 'i2cao.xyz', isHappy: true }), Person.create({ name: '123', isHappy: false }), Person.create({ name: 'ibeginner.sinaapp.com', isHappy: false })];var every = people.every(function(person, index, self) { if (person.get('isHappy')) return true;});console.log('every = ' + every); var some = people.some(function(person, index, self) { if (person.get('isHappy')) return true;});console.log('some = ' + some);
// 与every、some类似的方法还有isEvery、isAny console.log('isEvery = ' + people.isEvery('isHappy', true)); // 全部都为true,返回结果才是trueconsole.log('isAny = ' + people.isAny('isHappy', true)); //只要有一个为true,返回结果就是true
上述方法的使用与普通JavaScript
提供的方法基本一致。学习难度不大……自己敲两边就懂了!
这些方法非常重要,请一定要学会如何使用!!!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用在github项目上给我个star
吧。您的肯定对我来说是最大的动力!!
本篇之前的6篇文章都是第一章的内容,这一章节主要介绍了Ember
的对象模型。其中最重要的是计算属性和枚举这2章,非常之重要,一定要好好掌握!
下一章节是第二章模板,Ember
应用使用的模板库是handlebar
(点我查看更多有关此模板的介绍),这个模板库功能强大,有丰富的标签,包括判断标签if
,if else
,以及遍历标签each
等等。
另外,从下一章开始,我们不再自己手动搭建Ember
项目,也不用手动引入Ember
库文件,而是使用官方提供的一个非常棒的构建工具——Ember CLI
,要使用这个构建工具首先安装并配置。下面两个地址是介绍安装与配置的(推荐第一个):
Ember CLI
是一个非常重要的构建工具,它可以为开发者创建文件并初始化部分固定的代码。它还可以运行、打包、测试Ember
应用。
下面使用这个工具创建一个新的Ember
项目chapter2_tempalte
。
ember new chapter2_tempalte
cd chapter2_template
ember server
如果项目创建成功你可以继续往下看,如果项目创建不成功请重试,因为后面的代码都基于这个项目来演示的!!!对于创建项目后得到的每个文件和目录请你看官网文档,上面会有非常详细的说明。为了方便懒人在此就简单介绍其中几个很重要的文件和目录:
目录 | 说明 |
app | 项目的主要代码都是放在这个目录下 |
app/controllers | 存放C(MVC)层(controller)的代码文件 |
app/helpers | 存放自定义的helper代码文件 |
app/models | 存放M(MVC)层(model)代码文件 |
app/routes | 存放项目路由设置代码文件 |
app/templates | 存放项目模板代码文件 |
bower_components | 存放使用bower命令安装的第三方插件库 |
bower.json | 保存使用bower命令安装的第三方库的配置 |
package.json | 保存使用npm命令安装的第三方库的配置 |
node_modules | 存放使用npm命令安装的第三方插件库 |
ember-cli-build.js | 设置构建规范,引入第三方库 |
dist | 存放编译打包后的项目文件,可以直接复制到服务器中运行 |
上述这些文件或者目录是后面开发过程经常会用到,相对其他目录和文件来说这些目录和文件是很重要的,只要你是使用ember new appName
命令生成的项目都会包括上述这些目录或者文件。其中最重要的就是app
目录下的文件、目录了,从app
里面的目录结果你就可以很清楚的看到这是个MVC
框架的项目。Ember
之所以能找到controller
对应的template
也是根据目录和文件的名称找到的,Ember
是有自己的一套命名规则的,如果你想了解更多有关信息请移步folder-layout。
搭好环境之后开始我们的Ember
之旅吧!!!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
Ember
采用handlebars
模板库作为应用的view
层。Handlebars
模板与普通的HTML
非常相似。但是相比普通的HTML
而言handlebars
提供了非常丰富的表达式。Ember
采用handlebars
模板并且扩展了很多功能,让你使用handlebars
就像使用HTML
一样简单。
在前一篇介绍了一个很重要的构建工具Ember CLI
,从本篇开始后面所创建的文件都是使用这个构建工具来创建,先进入到项目路径下再执行Ember CLI
命令。
创建一个模板命令ember g template application
由于这个模板在创建项目的时候就已经有了,所以会提示你是否覆盖原来的文件,你可以选择覆盖或者不覆盖都行。
<h1>Kittens</h1><p>Kittens are the cutest!!</p>
注意:代码中的第一句注释的内容表明了这个文件的位置已经文件名称,后面的代码片段也会采用这种方式标注。如果没有特别的说明第一句代码都是注释文件的路径及其名称。
上述就是一个模板,非常简单的模板,只有一个h1和p标签,当你保存这个文件的时候Ember CLI会自动帮你刷新页面,不需要你手动去刷新!此时你的浏览器页面应该会看到如下信息:
那么恭喜你,模板定义成功了,至于为什么执行http://localhost:4200就直接显示到这里等你慢慢学到controller
和route
的时候自然会明白,你就当application.hbs
是一个默认的首页,这样你应该明白了吧!
每一个模板都会有一个与之关联的controller
类、route
类、model
类(当然这些类是不是必须有的)。这就是模板能显示表达式的值的原因,你可以在controller
类中设置模板中表达式显示的值,就像java web
开发中在servlet
或者Action
调用request.setAttribute()
方法设置某个属性一样。比如下面的模板代码:
<h2 id="title">Welcome to Ember</h2>Hello, <strong>{{firstName}} {{lastName}}</strong>!<br>My email is <b>{{email}}</b>
下面我们创建一个controller。这次我们用Ember CLI的命令创建: ember generate controller application,这句命令表示会创建一个controller并且名称是application,然后我们会得到如下几个文件:
app/controllers/application.js
--controller
本身tests/unit/controllers/application-test.js
--controller
对应的单元测试文件打开你的文件目录,是不是可以在app/controllers
下面看到了!现在为了演示表达式我们在controller
里添加一些代码:
// app/controllers/application.jsimport Ember from 'ember';/** * Ember会根据命名规则自动找到templates/application.hbs这个模板, * @type {hash} 需要设置的hash对象 */export default Ember.Controller.extend({ // 设置两个属性 firstName: 'chen', lastName: 'ubuntuvim', email: 'chendequanroob@gmail.com'});
然后修改显示的模板如下:
<h2 id="title">Welcome to Ember</h2>Hello, <strong>{{firstName}} {{lastName}}</strong>!<br>My email is <b>{{email}}</b>
保存,然后页面会自动刷新(Ember CLI
会自动检测文件是否改变,然后重新编译项目),我们可以看到在controller
设置的值,可以直接在模板上显示了。
这个就是表达式的绑定,后面你会学习到更多更有趣也更复杂的handlebasr
表达式。随着应用程序的规模不断扩大,会有更多的模板和与之关联的控制器。并且有时候一个模板还可以对应这多个控制器。也就是说模板上表达式的值可能有多个controller
控制。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
handlebars
模板提供了与一般语言类似的条件表达式,比如if
、if……else……
。在介绍这些条件表达式之前,我们先做好演示的准备工作。首先我会使用Ember CLI
命令创建route
、template
,然后在创建的template
上编写handlebars
模板代码。先创建route
:ember generate route handlbars-conditions-exp-route
或者:ember generate route handlbarsConditionsExpRoute
这两个命令创建的文件名都是一样的。最后Ember CLI
会为我们自动创建2个主要的文件:app/templates/handlbars-conditions-exp-route.hbs
和 app/routes/handlbars-conditions-exp-route.js
注意:如果你使用的是驼峰式的名称Ember CLI
会根据Ember
的命名规范自动创建以中划线-
分隔的名称。为什么我是先使用命令创建route
而不是template
呢??因为你创建route
的同时Ember CLI
会自动给你创建一个对应的模板文件,如果你是先创建template
的话,你还需要手动再执行创建route
的命令,即你要执行2条命令(ember generate template handlbars-conditions-exp-route
和ember generate route handlbars-conditions-exp-route
)。
得到演示所需要的文件后回到正题,开始介绍handlebars
的条件判断表达式。为了演示先在route
文件添加模拟条件代码:
// app/routes/handlebars-condition-exp-route.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function () { return {name: 'i2cao.xyz', age: 25, isAtWork: false, isReading: false }; // return { enable: true }; } });
对于handlebars-condition-exp-route.js
这个文件的内容会在后面路由这一章详细介绍,你可以暂且当做是返回了一个对象到模板上。与EL
表达式非常类似,你可以用.
获取对象里的属性值(如:person.name
)。
if
表达式{{#if model}}Welcome back, <b>{{model.name}}</b> !{{/if}}
每个条件表达式都要以#
开头并且要有对应的关闭标签,但是对于if
标签来说不是必须要关闭标签的,这里简单举个例子:
<div class="{{if flag 'show' 'hide'}}">测试内容</div>
这个 运行的时候需要注意两个地方,一个是浏览器执行的 建议:创建之后的路由名字最好不要修改, 结果是输出:This is else block...因为 如果 说白了其实就是一个三目运算。不难理解。不过这个例子与第一点讲没有关闭标签的 上述就是if
标签相当于一个三元运算符
,只是省略了?
和:
,他会根据属性flag
的值判断是显示那个CSS类,如果flag
的值不是false
,不是[]
空数组,也不是null
,也不是undefined
则div
会加上CSS类show
,模板编译之后的标签为hide
模板编译之后的标签为if
判断。没别的难点。。。URL
。如果你也是使用驼峰式的命名方式(创建命名:ember generate route handlbarsConditionsExpRoute
),那你的URL
跟我的是一样的,反正你只要记得执行的URL
跟你创建的route
的名称是一致的。当然这个名字是可以修改的。在app/router.js
里面修改,在Router.map
里的代码也是Ember CLI
自动创建的。我们可以看到有一个this.route('handlebarsConditionsExpRoute');
这个就是你的路由的名称。ember
会根据默认的命名规范查找route
对应的template
,如果你修改了router.js
里的名字你需要同时修改app/routes
和 app/templates
里相对应的文件名。否则URL
上的路由无法找到对应的template
显示你的内容,在router.js
里配置的名字必须与app/routes
目录下的路由文件名字对应,模板的名字不一定要与路由配置名称对应,应该可以在route
类中指定渲染的模板是那个,这个后面的内容会讲到(不是重点内容,了解即可)。说明:可能你看到的我截图给你的有点差别,是因为我修改了主模板(app/index.html
)你可以在这个文件里添加自己喜欢的样式,你一在app/index.html
引入你的样式,或者在ember-cli-build.js
引入第三方样式都可以,自定义的样式放在public/assets/
下,如果没有目录可以自行手动创建,在此就不再赘述,这个不是本文的重点。2,
if……else……
表达式{{#if model.isAtWork}}Ship that code..<br>{{else if model.isReading}}You can finish War and Peace eventually..<br>{{else}}This is else block...{{/if}}
isAtWork
和isReading
都是false
。读者可以自己修改app/routes/handlebars-condition-exp-route.js
里面对应的值然后查看输出结果。3,
unless
表达式unless
表达式类似于非操作,当model.isReading
值为false
的时候会输出表达式里面的内容。{{#unless model.isReading}}unless.....{{/unless}}
isReading
值为false
会输出unless…
否则不进入表达式内。4,在HTML标签内使用表达式
handlebars
表达式可以直接在嵌入到HTML
标签内。<span class="{{if" enable="" 'enable'="" 'disable'}}="">enable or disable</span>
if
例子一致,就当是复习吧=^=。handlebars
中最常用的几个条件表达式,自己作为小例子演示一遍肯定懂了,对于有点惊讶的开发者甚至看一遍即可。非常的简单,可能后面还会有其他的条件判断的表达式,后续会补上。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
采用与上一篇文章一样的方法,使用 ember generate route handlebars-each
命令创建了一个路由文件和一个对应的模板文件。这一篇将为你介绍遍历标签,数组的遍历几乎在任何的常用的开发语言中都能看到,也是使用非常广泛的一个功能。下面我将为大家介绍handlebars
的遍历标签,其使用方式与EL表达式几乎是一样的。我想你看一遍下来肯定也能明白了……废话少说,下面直接上演示代码吧!!
// app/routes/handlebars.jsimport Ember from 'ember';/** * 定义一个用于测试的对象数组 */export default Ember.Route.extend({ // 重写model回调函数,初始化测试数据 model: function() { return [ Ember.Object.create({ name: 'chen', age: 25}), Ember.Object.create({ name: 'i2cao.xyz', age: 0.2}), Ember.Object.create({ name: 'ibeginner.sinaapp.com', age: 1}), Ember.Object.create({ name: 'ubuntuvim.xyz', age: 3}) ]; }});
如上述所示,在route
类里构建了一个用于测试的对象数组,每个对象有2个属性(name
,age
)。下面是显示数据的模板:
{{! 遍历在route里设置的对象数组 }}<ul> {{#each model as |item|}} <li>Hello everyone, My name is {{item.name}} and {{item.age}} year old.</li> {{/each}}</ul>
有没有似曾相似的感觉呢!!跟EL表达式的forEach
标签几乎是一样的。不出意外你应该可以看到如下的结果。
提醒:记得此时运行的URL是刚刚新建的route。操作数组的时候注意使用官方建议的方法(如,新增使用pushObject
而不是push
),请看前面的文章。
有些情况我们可能需要获取数组的下标,比如有些时候可能会下标作为数据的序号。请看下面的演示:
{{! 遍历在route里设置的对象数组 }}
在实际的开发过程中你很有可能需要显示出对象数组的键或者值,如果你需要同时显示出对象的键和值你可以使用{{#each-in}}
标签。
注意:each-in
标签是Ember 2.0
才有的功能,之前的版本是无法使用这个标签的,如果是2.0一下的版本会报错:Uncaught Error: Assertion Failed: A helper named 'each-in' coulad not be found
准备工作:使用Ember CLI
生成一个component
,与此同时会生成一个对应的模板文件。ember generate component store-categories
执行上述命令得到下面的3个文件:
app/components/store-categories.jsapp/templates/components/store-categories.hbstests/integration/components/store-categories-test.js
然后在app/router.js
增加一个路由设置,在map
方法里添加this.route('store-categories');
;此时可以直接访问http://localhost:4200/store-categories;
http://guides.emberjs.com/v2.0.0/templates/displaying-the-keys-in-an-object/
// app/components/store-categories.jsimport Ember from 'ember';export default Ember.Component.extend({ // https://guides.emberjs.com/v2.4.0/components/the-component-lifecycle/ willRender: function() { // 设置一个对象到属性“categories”上,并且设置到categories属性上的对象结构是:key为字符串,value为数组 this.set('categories', { 'Bourbons': ['Bulleit', 'Four Roses', 'Woodford Reserve'], 'Ryes': ['WhistlePig', 'High West'] }); }));
willRender
方法在组件渲染的时候执行,更多有关组件的介绍会在后面章节——组件中介绍,想了解更多有关组件的介绍会在后面的文章中一一介绍,目前你暂且把组件当做是一个提取出来的公共HTML代码。
有了测试数据之后我们怎么去使用each-in
标签遍历出数组的键呢?
<ul> {{#each-in categories as |category products|}} <li>{{category}} <ol> {{#each products as |product|}} <li>{{product}}</li> {{/each}} </ol> </li> {{/each-in}}</ul>
上述模板代码中第一个位置参数category
就是迭代器的键,第二个位置参数product
就是键所对应的值。
为了显示效果,在application.hbs
中调用这个组件,组件的调用非常简单,直接使用{{组件名}}
方式调用。
{{store-categories}}
渲染后结果如下图:
{{each-in}}
表达式不会根据属性值变化而自动更新。上述示例中,如果你给属性categories
增加一个元素值,模板上显示的数据不会自动更新。为了演示这个特性在组件中增加一个触发属性变化的按钮,首先需要在组件类app/components/store-categories.js
中增加一个action
方法(有关action会在后面的章节介绍,暂时把他看做是一个普通的js函数),然后在app/templates/components/store-categories.hbs
中增加一个触发的按钮。
import Ember from 'ember';export default Ember.Component.extend({ // willRender方法在组件渲染的时候执行,更多有关组件的介绍会在后面章节——组件,中介绍 willRender: function() { // 设置一个对象到属性“categories”上,并且设置到categories属性上的对象结构是:key为字符串,value为数组 this.set('categories', { 'Bourbons': ['Bulleit', 'Four Roses', 'Woodford Reserve'], 'Ryes': ['WhistlePig', 'High West'] }); }, actions: { addCategory: function(category) { console.log('清空数据'); let categories = this.get('categories'); // console.log(categories); categories['Bourbons'] = []; // 手动执行重渲染方法更新dom元素,但是并没有达到预期效果 // 还不知道是什么原因 this.rerender(); } }});
<ul> {{#each-in categories as |category products|}} <li>{{category}} <ol> {{#each products as |product|}} <li>{{product}}</li> {{/each}} </ol> </li> {{/each-in}}</ul><button onclick={{action 'addCategory'}}>点击清空数据</button>
但是很遗憾,即使是手动调用了rerender
方法也没办法触发重渲染,界面显示的数据并没有发生变化。后续找到原因后再补上!!
空数组处理与表达式{{each}}
一样,同样是判断属性不是null
、undefined
、[]
就显示出数据,否则执行else
部分。
{{#each-in people as |name person|}} Hello, {{name}}! You are {{person.age}} years old.{{else}} Sorry, nobody is here.{{/each-in}}
可以参考上一篇的{{each}}
标签测试,这里不再赘述。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
简单讲属性绑定其实就是在HTML标签内(是在一个标签的””中使用)直接使用handlebars
表达式。可以直接用handlebars
表达式的值作为HTML标签中某个属性的值。
准备工作:ember generate route binding-element-attributes
<div id="logo"> <img src={{model.imgUrl}} alt='logo' /></div>
在对应的route:binding-element-attributes
里增加测试数据。
import Ember from 'ember';export default Ember.Route.extend({ model: function() { return { imgUrl: 'http://i1.tietuku.com/1f73778ea702c725.jpg' }; }});
运行之后模板会编译成如下代码:
<div id="logo"> <img alt="logo" src="http://i1.tietuku.com/1f73778ea702c725.jpg" rel="external nofollow" ></div>
可以看到{{model.imgUrl}}
会以字符串的形式绑定到src
属性上。
在开发过程中我们经常会根据某个值判断是否给某个标签增加CSS类,或者根据某个值决定按钮是否可用等等……那么ember是怎么做的呢??比如下面的代码演示的是checkbox
按钮根据绑定的属性isEnable
的值决定是否可用。
{{! 当isEnable为true时候,disabled为true,不可用;否则可用}}<input type='checkbox' disabled={{model.isEnable}}>
如果在route
里设置的值是true
那么渲染之后的HTML如下:
<input type="checkbox" disabled="">
否则
<input type="checkbox">
默认情况下,ember不会绑定到data-xxx
这一类属性上。比如下面的绑定结果就得不到你的预期。
{{! 绑定到data-xxx这种属性需要特殊设置}}{{#link-to 'photo' data-toggle='dropdown'}} link-to {{/link-to}}{{input type='text' data-toggle='tooltip' data-placement='bottom' title="Name"}}
模板渲染之后得到下面的HTML代码
<a id="ember455" href="/binding-element-attributes" class="ember-view active"> link-to </a><input id="ember470" title="Name" type="text" class="ember-view ember-text-field">
可以看到data-xxx
的属性都不见了!!!现在很多的前端框架都会用到data-xxx
这个属性,比如bootstrap
。那怎么办呢……你可以在view中指定对应的渲染组件Ember.LinkComponent
和Ember.TextField
(个人理解的)。执行命令得到view文件:ember generate view binding-element-attributes
,
在view中手动绑定,如下:
// app/views/binding-element-attributes.jsimport Ember from 'ember';export default Ember.View.extend({});// 下面是官方给的代码,但很明显看出来这种使用方式不是2.0版本的!!// 2.0版本的写法还在学习中,后续在补上,现在为了演示模板效果暂时这么写!毕竟本文的重点还是在模板属性的绑定上// 绑定inputEmber.TextField.reopen({ attributeBindings: ['data-toggle', 'data-placement']});// // 绑定link-toEmber.LinkComponent.reopen({ attributeBindings: ['data-toggle']});
渲染之后得到的结果符合预期。得到如下HTML代码
<a id="ember398" href="/binding-element-attributes" data-toggle="dropdown" class="ember-view active">link-to</a><input id="ember414" title="Name" type="text" data-toggle="tooltip" data-placement="bottom" class="ember-view ember-text-field">
可以看到data-xxx
的属性正常渲染到HTML上了。
本文介绍了几个常用的属性绑定方式,非常之实用!但是有点遗憾本人能力有限还没理解到最后一个实例在Ember2.0
版中是怎么使用的,现在的代码是根据个人理解把指定组件的代码放在view,官方教程给的也不是Ember2.0
版的,所以binding-element-attributes.js
这个文件的代码有点奇葩了……希望读者们不吝赐教!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
link-to
助手表达式渲染之后就是一个a
标签。而a
标签的href
属性的值是根据路由生成的,与路由的设置是息息相关的。并且每个设置的路由名称都是有着对应的关系的。为了演示效果,用命令生成了一个route
(或者手动创建文件)并获取测试数据。本文结合路由设置,随便串讲了一些路由方面的知识,如果你能看懂最好了,看不懂也不要紧后面会有一整章介绍路由。
// app/routers.jsimport Ember from 'ember';import config from './config/environment';var Router = Ember.Router.extend({ location: config.locationType });Router.map(function() { this.route('posts', function() { this.route('detail', {path: '/:post_id'}); //指定子路由,:post_id会自动转换为数据的id });});export default Router;
如上述代码,在posts
下增加一个子路由detail
。并且指定路由名为/:post_id
,:post_id
是一个动态字段,一般情况下默认为model
的id
属性。经过模板渲染之后会得到类似于posts/1
、posts/2
这种形式的路由。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls'); }});
使用Ember提供的方法直接从远程获取测试数据。测试数据的格式可以用浏览器直接打开上面的URL就可以看到。
<div class="container"> <div class="row"> <div class="col-md-10 col-xs-10"> <div style="margin-top: 70px;"> <ul class="list-group"> {{#each model as |item|}} <li class="list-group-item"> {{#link-to 'posts.detail' item}} {{item.title}} {{/link-to}} </li> {{/each}} </ul> </div> </div> </div></div>
直接用{{#each}}
遍历出所有的数据,并显示在界面上,由于数据比较多可能显示的比较慢,特别是页面刷新之后看到一片空白,请不要急着刷新页面,等待一下即可……。下图是结果的一部分:
我们查看页面的源代码,可以看到link-to
助手渲染之后的HTML代码,自动生成了URL,在router.js
里配置的post_id
渲染之后都被model
的id
属性值代替了。
如果你没有测试数据,你还可直接把link-to
助手的post_id
写死,可以直接把数据的id
值设置到link-to
助手上。在模板文件的ul
标签下一行增加如下代码,在link-to
助手中指定id
为1
:
<li class="list-group-item"> {{#link-to 'posts.detail' 1}}增加一条直接指定id的数据{{/link-to}} </li>
渲染之后的HTML代码如下:
<li class="list-group-item"><a id="ember404" href="/posts/1" class="ember-view">增加一条直接指定id的数据</a></li>
可以看到与前面使用动态数据渲染之后的href
的格式是一样的。如果你想设置某个a
标签是激活状态,你可以直接在标签上增加一个CSS类(class=”active”
)。
开发中,路由的路径经常不是2层的(post/1
)也有可能是多层次的(post/1/comment
、post/1/2
或者post/1/comment/2
等等形式。),如果是这种形式的URL在link-to
助手上又要怎么去定义呢?老样子,在演示模板之前还是需要先构建好测试数据以及修改对应的路由设置,此时的路由设置是多层的,因为link-to
助手渲染之后得到的href属性值就是根据路由生成的!!!这个必须要记得……
// app/routers.jsimport Ember from 'ember';import config from './config/environment';var Router = Ember.Router.extend({ location: config.locationType });Router.map(function() { // this.route('handlebarsConditionsExpRoute'); // this.route('handlebars-each'); // this.route('store-categories'); // this.route('binding-element-attributes'); // link-to实例理由配置 // this.route('link-to-helper-example', function() { // this.route('edit', {path: '/:link-to-helper-example_id'}); // }); this.route('posts', function() { //指定子路由,:post_id会自动转换为数据的id this.route('detail', {path: '/:post_id'}, function() { //增加一个comments路由 this.route('comments'); // 第二个子路由comment,并且路由是个动态字段comment_id this.route('comment', {path: '/:comment_id'}); }); }); });export default Router;
如果是上述配置,渲染之后得到的路由格式posts/detail/comments
。由于获取远程数据比较慢直接注释掉posts.js
里的model
回调方法,就直接使用写死id的方式。注意:上述配置中,在路由detail
下有2个子路由,一个是comments
,一个是comment
,并且这个是一个动态段。由此模板渲染之后应该是有2种形式的URL。一种是posts.detail.comments
(posts/1/comments
),另一种是posts.detail.comment
(posts/1/2
)。如果能理解这个那route
嵌套层次再多应该也能看明白了!
<div class="container"> <div class="row"> <div class="col-md-10 col-xs-10"> <div style="margin-top: 70px;"> <ul class="list-group"> <li class="list-group-item"> {{#link-to 'posts.detail.comments' 1 class='active'}} posts.detail.comments(posts/1/comments)形式 {{/link-to}} </li> <li class="list-group-item"> {{#link-to 'posts.detail.comment' 1 2 class='active'}} posts.detail.comment(posts/1/2)形式 {{/link-to}} </li> </ul> </div> </div> </div></div>
渲染之后的结果如下:
如果是动态段的一般都是model
的id
代替,如果不是动态段的直接显示配置的路由名称。
上面演示了多个子路由的情况,下面接着介绍一个路由有多个层次,并且是有个多个动态段和非动态段组成的情况。首先修改路由配置,把comments
设置为detail
的子路由。并且在comments
下在设置一个动态段的子路由comment_id
。
// app/routers.jsimport Ember from 'ember';import config from './config/environment';var Router = Ember.Router.extend({ location: config.locationType });Router.map(function() { this.route('posts', function() { //指定子路由,:post_id会自动转换为数据的id this.route('detail', {path: '/:post_id'}, function() { //增加一个comments路由 this.route('comments', function() { // 在comments下面再增加一个子路由comment,并且路由是个动态字段comment_id this.route('comment', {path: '/:comment_id'}); }); }); });});export default Router;
模板使用路由的方式posts.detail.comments.comment
。正常情况应该生成类似posts/1/comments/2
这种格式的URL。
<div class="container"> <div class="row"> <div class="col-md-10 col-xs-10"> <div style="margin-top: 70px;"> <ul class="list-group"> <li class="list-group-item"> {{#link-to 'posts.detail.comments.comment' 1 2 class='active'}} posts.detail.comments.comment(posts/1/comments/2)形式 {{/link-to}} </li> </ul> </div> </div> </div></div>
渲染之后得到的HTML如下:
<ul class="list-group"> <li class="list-group-item"> <a id="ember473" href="/posts/1/comments/2" class="active ember-view"> posts.detail.comments.comment(posts/1/comments/2)形式 </a> </li> </ul>
结果正如我们预想的组成了4层路由(/posts/1/comment/2
)。补充内容。对于上述第二点多层路由嵌套的情况你还可以使用下面的方式设置路由和模板,并且可用同时设置了/posts/1/comments
和/posts/1/comments/2
。
this.route('posts', function() { //指定子路由,:post_id会自动转换为数据的id this.route('detail', {path: '/:post_id'}, function() { //增加一个comments路由 this.route('comments'); // 路由调用:posts.detail.comment // 注意区分与前面的设置方式,detai渲染之后会被/:post_id替换,comment渲染之后直接被comments/:comment_id替换了, //会得到如posts/1/comments/2这种形式的URL this.route('comment', {path: 'comments/:comment_id'}); }); });
<div class="container"> <div class="row"> <div class="col-md-10 col-xs-10"> <div style="margin-top: 70px;"> <ul class="list-group"> <li class="list-group-item"> {{#link-to 'posts.detail.comments' 1 class='active'}} posts.detail.comments(/posts/1/comments)形式 {{/link-to}} </li> <li class="list-group-item"> {{#link-to 'posts.detail.comment' 1 2 class='active'}} posts.detail.comments.comment(posts/1/comments/2)形式 {{/link-to}} </li> </ul> </div> </div> </div></div>
渲染之后的结果如下:
两种方式定义的路由渲染的结果是一样的,看个人喜欢了,定义方式也是非常灵活的。第一种定义方式看着比较清晰,看代码就知道是一层层的。但是需要写更多代码。第二种定义方式更加简洁,不过看不出嵌套的层次。对于上述route的设置如果还不能理解也不要紧,后面会有一整章是介绍路由的,然而你能结合link-to助手理解了路由设置对于后面route章节的学习是非常有帮助的。
handlebars
允许你直接在link-to
助手增加额外的属性,经过模板渲染之后a
标签就有了增加的额外属性了。比如你可用为a
标签增加CSS的class
。
{{link-to "show text info" 'posts.detail' 1 class="btn btn-primary"}}{{#link-to "posts.detail" 1 class="btn btn-primary"}}show text info{{/link-to}}
上述两种写法都是可以的,渲染的结果也是一样的。渲染之后的HTML为:
<a id="ember434" href="/posts/1" class="btn btn-primary ember-view">show text info</a>
注意:上述两种方式的写法所设置的参数的位置不能调换。但是官网没有看到这方面的说明,有可能是我的演示实例的问题,如果读者你的可用欢迎给我留言。第一种方式,显示的文字必须放在最前面,并且中间的参数是路由设置,最有一个参数是额外的属性设置,如果你还要其他的属性需要设置仍然只能放在最后面。第二章方式的参数位置也是有要求的,第一个参数必须是路由设置,后面的参数设置额外的属性。对于渲染之后的HTML代码中出现标签id
为ember
,或者ember-xxx
,这些属性都是Ember默认生成的,我们可以暂时不用理它。综合,本来这篇是讲解link-to
的,但是由于涉及到了route
的配置就顺便讲讲,有点难度,主要在路由的嵌套这个知识点上,但是对于后面的route
这一章的学习是很有帮助的,route
的设置几乎都是为URL设置的。这两者是紧密关联的!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
在Ember中路由和模板的执行都是有一定顺序的,它们的顺序为:主路由->子路由1->子路由2->子路由3->……。模板渲染的顺序与路由执行顺序刚好相反,从最后一个模板开始解析渲染。
注意:模板的渲染是在所有路由执行完之后,从最后一个模板开始。关于这一点下面的代码会演示验证,官网教程有介绍,点击查看。
比如有一路由格式为application/posts/detail/comments/comment
,此时路由执行的顺序为:application/posts
-> detail
-> comments
-> comment
,application
是项目默认的路由,用户自定义的所有路由都是application
的子路由(默认情况下),相对应的模板也是这样,所有用户自定义的模板都是application.hbs
的子模板。如果你要修改模板的渲染层次你可以在route
中重写renderTemplate
回调函数,在函数内使用render
方法指定要渲染的模板(如:render('other')
,渲染到other
这个模板上)更多有关信息请查看这里。并且它们对应的文件模板结构如下图:
路由与模板是相对应的,所以模板的目录结构与路由的目录结构是一致的。你有两种方式构建上述目录:
comment.js
使用命令:ember generate route posts/detail/comments/comment
,Ember CLI会自动为我们创建目录和文件。创建好目录结构之后我们添加一些代码到每个文件。运行项目之后你就会一目了然了……。下面我按前面讲的路由执行顺序分别列出每个文件的内容。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { console.log('running in posts...'); return { id: 1, routeName: 'The route is posts'}; // return Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls'); } });
import Ember from 'ember';export default Ember.Route.extend({ model: function(params) { console.log('params id = ' + params.post_id); console.log('running in detail....'); return { id: 1, routeName: 'The route is detail..' }; }});
// app/routes/posts/detail.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { console.log('params id = ' + params.post_id); console.log('running in detail....'); return { id: 1, routeName: 'The route is detail..' }; }});
// app/routes/posts/detail/comments.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { console.log('running in comments...'); return { id: 1, routName: 'The route is comments....'}; }});
// app/routes/posts/detail/comments/comment.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { console.log('params id = ' + params.post_id); console.log('running in comment...'); return { id: 1, routeName: 'The route is comment...'}; }});
下面是模板各个文件的内容。其列出才顺序与路由的顺序一致。
{{model.routeName}} >> {{outlet}}
{{model.routeName}} >> {{outlet}}
{{model.routeName}} >> {{outlet}}
{{model.routeName}} >> {{outlet}}
下图是路由执行的顺序,并且在执行的过程中渲染路由对应的模板。
从上图中可用清楚的看到当你运行一个URL时,与URL相关的路由是怎么执行的。
application
),此时进入到路由的model
回调方法,并且返回了一个对象{ id: 1, routeName: 'The route is application...' }
,执行完回调之后继续转到子路由执行直到最后一个路由执行完毕,所有的路由执行完毕之后开始渲染页面。detail.js
和comments.js
。在代码中加入一个模拟休眠的操作。// app/routes/posts/detail.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function(params) {console.log('params id = ' + params.post_id);console.log('running in detail....');
// 执行一个循环,模拟休眠for (var i = 0; i < 10000000000; i++) {
} console.log('The comment route executed...');
return { id: 1, routeName: 'The route is detail..' };}});
```javascript// app/routes/posts/detail/comments.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { console.log('params id = ' + params.post_id); console.log('running in comment...'); // 执行一个循环,模拟休眠 for (var i = 0; i < 10000000000; i++) { } return { id: 1, routeName: 'The route is comment...'}; }});
刷新页面,注意查看控制台输出信息和页面显示的内容。新开一个窗口,执行URL:http://localhost:4200/posts/2/comments。
控制台输出到这里时处理等待(执行for
循环),此时已经执行了两个路由application
和posts
,并且正在执行detail
,但是页面是空白的,没有任何HTML元素。
在detail
路由执行完成之后转到路由comments
。然后执行到for
循环模拟休眠,此时页面仍然是没有任何HTML元素。然后等到所有route
执行完毕之后,界面才显示model
回调里设置的信息。
每个子路由设置的信息都会渲染到最近一个父路由对应模板的{{outlet}}
上面。
comment
得到的内如为:“comment
渲染完成”comment
最近的父模板comments
得到的内容为:“comment
渲染完成 comments
渲染完成”comments
最近的父模板detail
得到的内容为:“comment
渲染完成 comments
渲染完成 detail
渲染完成”detail
最近的父模板posts
得到的内容为:“comment
渲染完成 comments
渲染完成 detail
渲染完成 posts
渲染完成”posts
最近的父模板application
得到的内容为:“comment
渲染完成 comments
渲染完成 detail
渲染完成 posts
渲染完成 application
渲染完成”只要记住一句话:子模板的都会渲染到父模板的{{outlet}}
上,最终所有的模板都会被渲染到application
的{{outlet}}
上。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
action
助手所现实的功能与javascript
里的事件是相似的,都是通过用户点击元素触发定义在元素上的事件。Ember的action助手还允许你传递参数到对应的controller
、component
类,在controller
或者component
上处理事件的逻辑。准备工作,我们使用Ember CLI命令创建一个名称为myaction
的controller
和同名的route
,如果你不知道怎么使用Ember CLI请看前面的文章Ember.js 入门指南之七第一章对象模型小结,这篇文件讲解了怎么使用Ember CLI构建一个简单的Ember项目。
// apap/routes/myaction.jsimport Ember from 'ember';export default Ember.Route.extend({ // 返回测试数据到页面 model: function() { return { id:1, title: 'ACTIONS', body: "Your app will often need a way to let users interact with controls that change application state. For example, imagine that you have a template that shows a blog title, and supports expanding the post to show the body.If you add the {{action}} helper to an HTML element, when a user clicks the element, the named event will be sent to the template's corresponding component or controller." }; }});
重写model
回调,直接返回一个对象数据。
<h2 {{action ' showDetailInfo '}} style="cursor: pointer;">{{model.title}}</h2>{{#if isShowingBody}}<p>{{model.body}}</p>{{/if}}
默认下只显示文章的标题,当用户点击标题的时候触发事件toggleBody
显示文章的详细信息。
// app/controllers/myaction.jsimport Ember from 'ember';export default Ember.Controller.extend({ // 控制页面文章详细内容是否显示 isShowingBody: false, actions: { showDetailInfo: function() { // toggleProperty方法直接把isShowingBody设置为相反值 // toggleProperty方法详情:http://devdocs.io/ember/classes/ember.observable#method_toggleProperty this.toggleProperty('isShowingBody'); } }});
对于controller
的处理逻辑你还可以直接编写触发的判断。
actions: { showDetailInfo: function() { // toggleProperty方法直接把isShowingBody设置为相反值 // toggleProperty方法详情:http://devdocs.io/ember/classes/ember.observable#method_toggleProperty // this.toggleProperty('isShowingBody'); // 变量作用域问题 var isShowingBody = this.get('isShowingBody'); if (isShowingBody) { this.set('isShowingBody', false); } else { this.set('isShowingBody', true); } } }
如果你不使用toggleProperty
方法改变isShowingBody
的值,你也可用直接编写代码修改它的值。最后执行URL:http://localhost:4200/myaction,默认情况下页面上是不显示文章的详细信息的,当你点击标题则会触发事件,显示详细信息,下面2个图片分别展示的是默认情况和点击标题之后。当我们再次点击标题,详细内容又变为隐藏。
通过上述的小例子可以看到action
助手使用起来也是非常简单的。主要注意下模板上的action
所指定的事件名称要与controller
里的方法名字一致。
就像调用javascript
的方法一样,你也可以为action
助手增加必要的参数。只要在action
名字后面接上你的参数即可。
<p><button {{action 'hitMe' model}}>点击我吧</button></p>
对应的在controller
增加处理的方法selected
。在此方法内打印获取到的参数值。
// app/controllers/myaction.jsimport Ember from 'ember';export default Ember.Controller.extend({ // 控制页面文章详细内容是否显示 isShowingBody: false, actions: { showDetailInfo: function() { // ……同上面的例子 }, hitMe: function(model) { // 参数的名字可以任意 console.log('The title is ' + model.title); console.log('The body is ' + model.body); } }});
Ember规定我们编写的动作处理的方法都是放在actions
这个哈希内。哈希的键就是方法名。在controller
方法上的参数名不要求与模板上传递的名字一致,你可以任意定义。比如方法hitMe
的参数model
你也可以使用m
作为hitMe
方法的参数。
当用户点击按钮“点击我吧”就会触发方法hitMe
,然后执行controller
的同名方法,最后你可以在浏览器的console
下看到如下的打印信息。
看到这些打印结果很好的说明了获取的参数是正确的。
默认情况下action
触发的是click
事件,你可以指定其他事件,比如键盘按下事件keypress
。事件的名字与javascript
提供的名字是相似的,唯一不同的是Ember所识别是事件名字如果是由不同单词组成的需要用中划线分隔,比如keypress
事件在Ember模板中你需要写成key-press
。注意:你指定事件的时候要把事件的名字作为on
的属性。比如on='key-press'
。
<a href="#/myaction" {{action 'triggerMe' on="mouse-over"}}>鼠标移到我身上触发</a>
triggerMe: function() { console.log('触发mouseover事件。。。。');}
action
触发事件的辅助按键甚至你还可以指定按下键盘某个键后点击才触发action
所指定的事件,比如按下键盘的Alt
再点击才会触发事件。使用allowedkeys
属性指定按下的是那个键。
<br><br><button {{action 'pressALTKeyTiggerMe' allowedkeys='alt'}}>按下Alt点击触发我</button>
在action
助手内使用属性preventDefault=false
可以禁止标签的默认行为,比如下面的a标签,如果action
助手内没有定义这个属性那么你点击链接时只会执行执行的action
动作,a
标签默认的行为不会被触发。
<a href="http://www.baidu.com" rel="external nofollow" target="_blank" target="_blank" {{action "showDetailInfo" preventDefault=false}}>点我跳转</a>
controller
handlebars
的action
助手真的是非常强大,你甚至可以把触发的事件作为action
的参数直接传递到controller
。不过你需要把action
助手放在javascript
的事件里。比如下面的代码当失去焦点时触发,并且通过action
指定的dandDidChange
把触发的事件blur
传递到controller
。
失去焦点时候触发<input type="text" value={{textValue}} onblur={{action 'bandDidChange'}} />
// app/controllers/myaction.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { bandDidChange: function(event) { console.log('event = ' + event); } } });
从控制台输出结果看出来event
的值是一个对象并且是一个focus
事件。但是如果你在action
助手内增加一个属性value='target.value'
(别写错只能是target.value
)之后,传递到controller
的则是输入框本身的内容。不再是事件对象本身。
<input type="text" value={{textValue}} onblur={{action 'bandDidChange' value="target.value"}} />
这个比较有意思,实现的功能与前面的参数传递类似的。
action
助手用在非点击元素上`action`助手可以用在任何的`DOM`元素上,不仅仅是用在能点击的元素上(比如`a`、`button`),但是用在其他非点击的元素上默认情况下是不可用的,也就是说点击也是无效的。比如用在`div`标签上,但是你点击`div`元素是没有反应的。如果你需要让`div`元素也能触发单击事件你需要给元素添加一个CSS类'cursor:pointer;`。
总的来说Ember的action
助手与普通的javascript的事件是差不多的。用法基本上与javascript的事件相似。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
Ember提供的表单元素都是经过封装的,封装成了view
组件。经过解析渲染之后就会生成普通的HTML标签。更多详细信息你可以查看他们的实现源码:Ember.TextField、Ember.Chechbox、Ember.TextArea。
按照惯例,先创建一个route
:ember generate route form-helper
。
input
助手{{! //app/templates/form-helper.hbs }}{{input name="username" placeholder="your name"}}
其中可以使用在input
助手上的属性有很多,包括readonly
、value
、disabled
、name
等等,更多有关的属性介绍请移步官网介绍。
注意:对于使用在input
助手上的属性是不是使用双引号括住是有区别的。比如value='helloworld'
和value=helloworld
渲染之后的结果是不一样的,第一种写法是直接把"helloworld"这个字符串赋值设置到value
上,第二种写法是从上下文获取变量helloworld的值再设置到value
上,通常是在controller
或者route
设置的值。看下面2行代码的演示结果:
{{input name="username" placeholder="your name" value="model.helloworld"}}<br><br>{{input name="username" placeholder="your name" value=model.helloworld}}
修改对应的route
类,重写model
回调,返回一个字符串;或者你可以在模板对应的controller
类设置。比如下面的第二段代码(使用命令ember generate controller form-helper
得到模板对应的controller
类。)。
// app/routes/form-helper.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return { helloworld: 'The value from route...' } }});
在controller
类初始化测试数据。
// app/controllers/form-helper.jsimport Ember from 'ember';export default Ember.Controller.extend({ helloworld: 'The value from route...'});
对应的,如果你使用的是controller
初始化测试数据,那么你的模板获取数据的方式就要稍微修改下。需要去掉前缀model.
。controller
不需要在回调中初始化测试数据,可用直接定义成controller
的属性。
{{input name="username" placeholder="your name" value=helloworld}}
input
助手上指定触发事件你可以想想下,我们平常写过的javascript代码,不是可用直接在input
输入框上使用javascript的函数,同理的,input
助手上可以使用javascript函数,不过使用方式有点差别,请看下面示例。比如按enter
键触发指定的事件、失去焦点触发事件等等。首先编写input
输入框,获取input
输入框的值有点不按常理=^=。在controller
类获取input
输入框的值是通过不用双引号的value
属性获取的。
按enter键触发{{input value=getValueKey enter="getInputValue" name=getByName placeholder="请输入测试的内容"}}
// app/controllers/form-helper.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { getInputValue: function() { var v = this.get('getValueKey'); console.log('v = ' + v); var v2 = this.get('getByName'); console.log('v2 = ' + v2); } }});
输入测试内容后按enter
键。
最后的输出结果有那么一点点意外。v
的值是正确的,v2
却是undefined
。可见在controller
层获取页面的值是通过value
这个属性而不是name
这个属性。跟我们平常HTML的input
有点不一样了!!这个需要注意下。
checkbook
助手checkbox
这个表单元素也是经过Ember封装了,作为一个组件使用。使用过程需要注意的问题与前面的input
是一样的,属性是不是使用双引号所起的作用是不一样的。
checkbox{{input type="checkbox" checked=isChecked }}
你可以在controller
增加一个属性isChecked
并设置为true
,checkbox
将默认为选中。
// app/controllers/form-helper.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { // …… }, isChecked: true});
textarea
助手{{textarea value=key cols="80" rows="3" enter="getValueByV"}}
同样的也是通过value
属性获取输入的值。
本篇简单的介绍了常用的表单元素,使用的方式比较简单,唯一需要注意的是获取的输入框输入值的方式与平常使用的HTML表单元素有点差别。其他的基本上与普通的HTML表单元素没什么差别。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
Ember不仅提供了专门用于调试Ember程序的谷歌、火狐浏览器插件Ember Inspector( 安装插件可能需要翻墙,如果你也是一个程序员我想翻墙对于你来说应该不是什么难事!!!),还提供了用于调试的helper
。按照惯例,先做好准备工作,分别执行Ember CLI命令创建controller
、route
和模板:
ember generate controller dev-helperember generate route dev-helper
{{log}}
{{log}}
可以把从controller
、route
类传递到页面上的值以日志的形式直接输出在浏览器的控制台上。下面代码在controller
类添加测试数据。
// app/controllers/dev-helper.jsimport Ember from 'ember';export default Ember.Controller.extend({ testName: 'This is a testvalue...'});
我们可以在模板上直接显示字符串testName
的值,也可以使用{{log}}
助手以日志形式输出在控制台。当然你也可以直接使用{{log 'xxx'}}
在控制台打印"xxxx"。第二点断点助手的示例中将为你演示{{log 'xxx'}}
用法。
直接显示在页面上:{{testName}}{{log testName}}
运行http://localhost:4200/dev-helper之后我们可以在页面上看到字符串testName
的值。打开谷歌或者火狐的控制台(console标签下)可以看到也打印的字符的值。比较简单我就不再截图了……
{{debugger}}
当你需要调试的时候,你可以在模板上需要添加断点的地方添加这个助手,运行的时候会自动停在添加这个助手的地方。
{{log '这句话在断点前面'}}{{debugger}}<br>{{log '这句话在断点后面'}}
不出意外程序会停在有{{debugger}}
这一行。控制台应该会打印“这句话在断点前面”。然后通过点击下一步跳过断点,然后继续打印“这句话在断点后面”。
运行结果不好截图,请读者自己试试吧!!!
当你使用了{{debugger}}
,并且程序停止进入debug状态的时候,你可以直接在浏览器控制台的命令行输入get('key')
来获取controller
设置的值。
在箭头所指的位置输入get('testName')
,然后按enter
键执行。会得到如下结果:
可以看到正确的获取到了前面在controller
类里设置的值。如果你不是在调试模式下输入get('testName')
那么会提示如下错误。
你还可以在遍历助手{{each}}
中使用{{debugger}}
,点击一次“下一步”就会执行一次循环。
首先重写route
类的model
回调,在里面添加测试数据。
// app/routes/dev-helper.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return [ { id: 1, name: 'chen', age: 25 }, { id: 2, name: 'ibeginner.sinaapp.com', age: 2 } ]; }});
在模板的each
助手中使用{{debugger}}
助手。
{{#each model as |item|}} {{debugger}} <li>item</li>{{/each}}
运行,浏览器自动进入debug模式(如果不能自动进入debug模式可以手动按F12
进入debug)。此时你可以在浏览器控制台命令输入get('item.name')
来获取本次循环对象的属性值。然后你几点“下一步”或者按F8
,程序自动进入到下一次循环,然后你再输入get('item.name')
,此时得到的是本次循环对象属性值。然后点击下一步或者按F8进入第三次循环,由于route
类设置返回的数组只有2个元素,第三次已经没有元素。所以这次会自动退出debug模式。如果运行正常你可会得到下图所示的输出信息。
在调试状态下你还可以直接在浏览器控制台命令行输入context
获取上下文信息。会输出本页面所包含的所有类和属性。
上述介绍的就是Ember提供的调试助手的所有使用方法。在你开发Ember应用的时候应该是很有用的,特别是在each
循环遍历的时候。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
本篇主要介绍格式转换、自定义helper
、自定义helper
参数、状态helper
、HTML标签转义这几个方面的东西。
按照文章惯例先准备好测试所需要的数据、文件。仍然是使用Ember CLI命令,这次我们创建的是helper
、controller
、route
(创建route
会自动创建template
)。
ember generate helper my-helperember generate controller tools-helperember generate route tools-helper
helper
自定义助手非常简答直接使用Ember CLI命令生成就可以了。当然你也可以手动创建,自定义的助手都是放在app/helpers
目录下。Ember会根据模板上使用的助手自动到这个目录查找。定义了helper
之后你就可以直接在模板上使用。
my-helper: {{my-helper}}
程序没有报错,但是什么也没有显示。是的什么也没有显示。没有显示就对了。因为我们对于刚刚创建的app/helpers/my-helper.js
没有做任何的修改。你可以看这个文件的代码。直接返回了params
,目前来说这个参数是空的。修改这个文件,直接返回一个字符串。
// app/helpers/my-helper.jsimport Ember from 'ember';export function myHelper(params/*, hash*/) { return "hello world!";}export default Ember.Helper.helper(myHelper);
此时可以在页面上看到直接打印了“hello world!”这个字符串。这就是一个最简单的自定义helper
,不过这么简单helper
显然是没啥用的。Ember的作者肯定不会这么傻的,接着下面介绍helper
的参数。注意:使用模板的名字跟文件名是一致的。不同单词使用-
分隔,虽然这个命名规则不是强制性的但是Ember建议你这么做,Ember会自动根据helper
的名字找到对应的自定义的helper
,然后执行helper
里名字为myHelper
(名字是文件名的驼峰式命名)的方法,在这个方法里你可以实现你需要的逻辑。这些工作Ember自动帮你做了,不需要你编写解析的代码。
helper
无名参数上面的代码定义了一个最简单的helper,不过没啥用,Ember允许在自定义的helper上添加自定义的参数。
my-helper-param: {{my-helper 'chen' 'ubuntuvim'}}
在这个自定义的helper
中增加了两个参数,既然有了参数那么又有什么用呢?当然是有用的,你可以在自定义的helper
中获取参数,获取模板的参数有两种方式。
写法一
// app/helpers/my-helper.jsimport Ember from 'ember';export function myHelper(params/*, hash*/) { // 获取模板上的参数 var p1 = params[0]; var p2 = params[1]; console.log('p1 = ' + p1 + ", p2 = " + p2); return p1 + " " + p2;}export default Ember.Helper.helper(myHelper);
写法二
// app/helpers/my-helper.jsimport Ember from 'ember';export function myHelper([arg1, arg2]) { console.log('p1 = ' + arg1 + ", p2 = " + arg2); return arg1 + " " + arg2;}export default Ember.Helper.helper(myHelper);
参数很多的情况使用第一种方式用循环获取参数比较方便,参数少情况第二种方式更加简便,直接使用!
注意:参数的顺序与模板传入的顺序一致。
页面刷新之后可以在页面或者浏览器控制台看到在helper
上设置的参数值了吧!!如果你的程序没有错误在浏览器上你也会得到下图的结果:
第一行因为在模板上没有传入参数所以是undefined
,第二行传入了参数,并且直接从helper
返回显示。
helper
命名参数上一点演示了在模板中传递无名的参数,这一小节讲为你介绍有名字的参数。
my-helper-named-param: {{my-helper firstName='chen' lastName='ubuntuvim'}}
相比于第一种使用方式给每个参数增加了参数名。那么helper
处理类有要怎么去获取这些参数呢?
// app/helpers/my-helper.jsimport Ember from 'ember';// 对于命名参数使用namedArgs获取export function myHelper(params, namedArgs) { console.log('namedArgs = ' + namedArgs); console.log('params = ' + params); console.log('========================='); return namedArgs.firstName + ", " + namedArgs.lastName; }export default Ember.Helper.helper(myHelper);
获取命名参数使用namedArgs
,其实你也可以按照前面的方法使用params
获取参数值。你在第一行打印语句上打上断点,是浏览器进入debug模式,但不执行,你会发现params
一开始是有值namedArgs
没有值,但是执行到最后正好相反,params
的值被置空了,namedArgs
却有了模板设置的值,你可以猜想下,Ember可能是把params
的值赋值到namedArgs
上了,不同之处是namedArgs
是以对象属性的方式取值并且不用关心参数的顺序问题,params
是以数组的方式取值需要关心参数的顺序。
做开发的都应该遇到过数字或者时间格式问题,特别是时间格式问题应该是最普遍遇到的。不同的项目时间格式往往不同,有yyyy-DD-mm
类型的有yyyyMMdd
类型以及其他类型。
同样的Ember模板也给我们提供了类似的解决办法,那就是自定义格式化方法。通过自定义helper
实现数据的格式化。
helper
:ember generate helper format-date
controller
初始化一个时间数据。// app/controllers/tools-helper.jsimport Ember from 'ember';export default Ember.Controller.extend({ currentDate: new Date()});
默认情况下显示数据currentDate
。
{{ currentDate}}`
此时显示的默认的数据格式。运行http://localhost:4200/tools-helper,可以在页面看到:Mon Sep 21 2015 23:46:03 GMT+0800 (CST)
这种格式的时间。显然不太合法我们的习惯,看着都觉得不舒服。那下面使用自定义的helper
格式化日期格式。
// app/helpers/format-data.jsimport Ember from 'ember';/** * 注意:方法名字是文件名去掉中划线变大写,驼峰式写法 * 或者你也可以直接作为helper的内部函数 * @param {[type]} params 从助手{{format-data}}传递过来的参数 */export function formatDate(params/*, hash*/) { console.log('params = ' + params); var d = Date.parse(params); var dd = new Date(parseInt(d)).toLocaleString().replace(/:d{1,2}$/,' '); // 2015/9/21 下午11:21 var v = dd.replace("/", "-").replace("/", "-").substr(0, 9); return v;}export default Ember.Helper.helper(formatDate);
或者你也可以这样写。
export default Ember.Helper.helper(function formatDate(params/*, hash*/) { var d = Date.parse(params); var dd = new Date(parseInt(d)).toLocaleString().replace(/:d{1,2}$/,' '); // 2015/9/21 下午11:21 var v = dd.replace("/", "-").replace("/", "-").substr(0, 9); return v;});
为了简便,直接就替换字符,修改时间分隔字 /
为-
。 然后修改显示的模板,使用自定义的helper
。
{{format-date currentDate}}
此时页面上显示的时间是我们熟悉的时间格式:
上面介绍的是简答的用法,Ember还允许你传入时间的格式(format
),以及本地化类型(locale
)。
helper
:ember generate helper format-date-time
controller
类里新增两个用于测试的属性cDate
和currentTime
。// app/controllers/tools-helper.jsimport Ember from 'ember';export default Ember.Controller.extend({ currentDate: new Date(), cDate: '2015-09-22', currentTime: '00:22:32'});
<br><br><br>format-date-time: {{format-date-time currentDate cDate currentTime format="yyyy-MM-dd h:mm:ss"}}<br><br><br>format-date-time-local: {{format-date-time currentDate cDate currentTime format="yyyy-MM-dd h:mm:ss" locale="en"}}
在助手format-date-time
上一共有4个属性。cDate
和currentTime
是从上下文获取值的变量,format
和locale
是Ember专门提供用于时间格式化的属性。
下面看看format-date-time
这个助手怎么获取页面的数据。
// app/helpers/format-date-time.jsimport Ember from 'ember';export function formatDateTime(params, hash) { // 参数的顺序跟模板{{format-date-time currentDate cDate currentTime}}上使用顺序一致, // cDate比currentTime先,所以第一个参数是cDate console.log('params[0] = ' + params[0]); //第一个参数是cDate, console.log('params[1] = ' + params[1]); // 第二个是currentTime console.log('hash.format = ' + hash.format); console.log('hash.locale = ' + hash.locale); console.log('------------------------------------'); return params;}export default Ember.Helper.helper(formatDateTime);
我只是演示怎么获取页面format-date-time
助手设置的值,得到页面设置的值你想干嘛就干嘛……最后看看浏览器控制台的输出信息。
因为页面使用了两次这个助手,所以自然也就打印了两次。
官方的解释是:为了保护你的应用免受跨点脚本攻击(XSS),Ember会自动把helper
返回值中的HTML标签转义。
新建一个helper
:ember generate helper escape-helper
// app/helpers/escape-helper.jsimport Ember from 'ember';export function escapeHelper(params/*, hash*/) { // return Ember.String.htmlSafe(`<b>${params}</b>`); return `<b>${params}</b>`;}export default Ember.Helper.helper(escapeHelper);
escape-helper: {{escape-helper "helloworld!"}}
此时页面上会直接显示“helloworld!”而不是“helloworld”被加粗了!如果你确定你返回的字符串是安全的你可用使用htmlSafe
方法,这个方法不会把HTML代码转义,HTML代码仍然能起作用,那么页面显示的将是加粗的“helloworld!”。
到此模板这一章全部讲完了!!!但愿你能从中得到一点收获!!后面的文章将开始讲route
,route
在Ember.js 入门指南之十三{{link-to}} 助手这一篇已经讲过一点,但不是很详细。接下来的一章将会为你详细解释route
。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
真快,第二章模板(template
)已经介绍完毕了!这个章节相对来说是比较简单,只有是有点HTML基础的学习起来并不会很难,几乎也不需要去记忆,自己动手实践实践就能理解。其中比较重要的是{{link-to}}
和{{action}}
这两篇。特别是{{link-to}}
,这个标签几乎都是与路由结合使用的,要注意与路由配置一一对应。
在下一章将为读者介绍第三章路由,如果你是看官网文档的你会发现路由是在模板之前介绍的,我稍微做了下调整,因为根据我自己学习的ember的经验我觉得先介绍模板更好学习。很多东西结合显示效果讲会容易很多。
在介绍路由这一章之前,重新创建了一个项目用于演示,依然是使用Ember CLI创建项目。下面是创建命名并且运行项目,测试项目是否创建成功。
ember new chapter3_routescd chapter3_routesember server
在浏览器运行:http://localhost:4200/,在界面上能看到Welcome to Ember说明项目搭建成功了!!
如果你还不知道怎么使用Ember CLI创建项目,请自行根据提供的地址安装配置Ember CLI命令环境,在第一章的小节已经详细介绍过,这里不再赘述。
下面开始路由的学习之旅吧~~~
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
当你的应用启动的时候,路由器就会匹配当前的URL到你定义的路由上。然后按照定义的路由层次逐个加载数据、设置应用程序状态、渲染路由对应的模板。
在app/router.js
的map
方法里定义的路由会映射到当前的URL。当map
方法被调用的时候方法体内的route
方法就会创建路由。
下面使用Ember CLI命令创建两个路由:
ember generate route aboutember generate route favorites
命令执行完之后你可在你的项目目录app/routes
下面看到已经创建好的两个路由文件已经app/templates
下面路由对应的模板文件。此时在app/router.js
的map
方法中已经存在了刚刚创建的路由配置。这个是Ember CLI自动为你创建了。
// app/router.jsimport Ember from 'ember';import config from './config/environment';var Router = Ember.Router.extend({ location: config.locationType});Router.map(function() { this.route('about'); this.route('favorites');});export default Router;
现在分别修改app/templates
下面的两个模板文件如下:
这个是about模板!<br>{{outlet}}
这个是favorites模板!<br>{{outlet}}
然后访问http://localhost:4200/about或者http://localhost:4200/favorites,如果你的程序没有问题你也会得到如下显示结果:
如果你觉得favorites
这个路由名字太长是否可以修改成其他名字呢?答案是肯定的,你只要修改router.js
中map
方法的配置即可。
Router.map(function() { this.route('about'); // 注意:访问的URL可以写favs但是项目中如果是使用route的地方仍然是使用favorites this.route('favorites', { path: '/favs' });});
此时访问:http://localhost:4200/favs,界面显示的结果与之前是一样的。
说明:默认情况下访问的URL与路由名字是一致的,比如this.route('about')
与this.route('about', { path: ‘/about’ })
是同一个路由,如果URL与路由不同名则需要使用{path: '/xxx'}
设置映射的URL。
在handlebars模板中可以使用{{link-to}}
助手在不同的路由间切换,使用时需要在link-to
助手内指定路由名称。比如下面的代码使用link-to
助手实现在about
和favs
两个路由间切换。为了页面能美观一点引入bootstrap,使用npm命令安装:bower install bootstrap
,如果安装成功你可以在bower_components目录下看到bootstrap相关的文件。安装成功之后引入到项目中,修改chapter3_routes/ember-cli-build.js
。在return
语句前加入如下两行代码(作用就是引入bootstrap框架):
app.import("bower_components/bootstrap/dist/css/bootstrap.css");app.import("bower_components/bootstrap/dist/js/bootstrap.js");
修改application.hbs
,增加一个导航菜单。
<div class="container-fluid"> <div class="navbar-header" href="#"> {{#link-to 'index' class="navbar-brand"}}Home{{/link-to}} </div> <ul class="nav navbar-nav"> <li>{{#link-to 'about'}}about{{/link-to}}</li> <li>{{#link-to 'favorites'}}favorites{{/link-to}}</li> </ul> <ul class="nav navbar-nav navbar-right"> <li><a href="#">Login</a></li> <li><a href="#">Logout</a></li> </ul> </div><div class="container-fluid" style="margin-top: 70px;">{{outlet}}</div>
如果看到页面没有bootstrap效果请重新启动项目。如果运行项目后再浏览器控制台出现如下错误。
如果出现上图错误需要在config/environment.js
中加上一些安全策略设置代码,有关设置的解释请看下面网址的文章介绍。
, contentSecurityPolicy: { 'default-src': "'none'", 'script-src': "'self' 'unsafe-inline' 'unsafe-eval' use.typekit.net connect.facebook.net maps.googleapis.com maps.gstatic.com", 'font-src': "'self' data: use.typekit.net", 'connect-src': "'self'", 'img-src': "'self' www.facebook.com p.typekit.net", 'style-src': "'self' 'unsafe-inline' use.typekit.net", 'frame-src': "s-static.ak.facebook.com static.ak.facebook.com www.facebook.com"}
上图是我的演示项目配置。然后点击about
会得到如下界面
可以看到浏览器地址栏的URL变为about
了,并且页面显示的内容也是about
模板的内容。同理你点击favorites
地址栏就变为http://localhost:4200/favs并且显示的内容是favorites
的(为什么URL是favs
而不是favorites
呢,因为前面已经修改了route
和URL的映射关系,路由favorites
对应的URL是favs
)。上述演示的就是路由的切换!!!
可以看到浏览器地址栏的URL变为about了,并且页面显示的内容也是about模板的内容。同理你点击“favorites”地址栏就变为http://localhost:4200/favs并且显示的内容是favorites的(为什么URL是favs而不是favorites呢,因为前面已经修改了route和URL的映射关系,路由favorites对应的URL是favs)。上述演示的就是路由的切换!!!
还记得在前面的Ember.js 入门指南之十三{{link-to}} 助手这篇文章的内容吗?在这篇文章中比较详细的介绍了路由的嵌套与怎么使用嵌套的路由。不妨回过头去看看。在这里打算就不讲了……如果有不明白的请看官网的教程。
application
路由是默认的路由,是程序的入口,所有其他自定义的路由都先进过application
才到自定义的路由。并且application
路由对应的application.hbs
模板是所有自定义模板的父模板,所有自定义的模板都会渲染到application.hbs模板的{{outlet}}
上。有关于路由的执行顺序以及模板的渲染顺序在前面的Ember.js 入门指南之十三{{link-to}} 助手也讲过了,在此也不打算在做过多的介绍了。你可以回头看之前的文章或者到官网查看。
对于所有的嵌套的路由,包括最顶层的路由Ember会自动生成一个访问URL为/
对应路由名称为index
的路由。
比如下面的两种路由设置是等价的。
// app/router.js// ……Router.map(function() { this.route('about'); // 注意:访问的URL可以写favs但是项目中如果是使用route的地方仍然是使用favorites this.route('favorites', { path: '/favs' });});export default Router;
// app/router.js// ……Router.map(function() { this.route('index', { path: '/' }); this.route('about'); // 注意:访问的URL可以写favs但是项目中如果是使用route的地方仍然是使用favorites this.route('favorites', { path: '/favs' });});export default Router;
index
路由会渲染到application.hbs
模板的{{outlet}}
上。这个是Ember默认设置。当用户访问/about
时Ember会把index
模板替换为about
模板。
对于路由嵌套的情况也是如此。
// app/router.js// ……Router.map(function() { this.route('posts', function() { this.route('new'); });});export default Router;
// app/router.js// ……Router.map(function() { this.route('index', { path: '/' }); this.route('posts', function() { this.route('index', { path: '/' }); this.route('new'); });});export default Router;
两种设置方式都会得到如下图的路由表。打开浏览器的“开发者工具”点开“Ember”选项卡,在点开“/#Routes”你就可以看到如下路由表(显示是顺序有可能跟你的不一样)。
注:loading
和error
这两个路由是ember自动生成的,他们的用法会在后面的文章介绍。
当用户访问/posts
时实际进入的路由是posts.index
对应的模板是posts/index.hbs
,但是实际中我并没有创建这个模板,因为Ember默认把这个模板渲染到posts.hbs
的{{outlet}}
上。由于这个模板不存在也就相当于什么都没做。当然你也可以创建这个模板。
使用命令:ember generate template posts/index
然后在这个模板中添加以下显示的内容:
<h2>这里是/posts/index.hbs。。。</h2>
再此访问http://localhost:4200/posts,是不是可以看到增加的内容了。
你可以这么理解对于每一个有子路由的路由都有一个名为index
的子路由并且这个路由对应的模板为index.hbs
,如果把有子路由的路由当做一个模块看待那么index.hbs
就是这个模块的首页。特别是做过一些信息系统的朋友应该是很熟悉的,基本上没个子模块都会有一个首页,这个首页现实的内容就是一进入这个模块时就显示的内容。既然是子模板当然也不会例外它也会渲染到父模板的{{outlet}}
上。比如上面的例子当用户访问http://localhost:4200/posts实际进入的是http://localhost:4200/posts/(后面多了一个/
,这个/
对应的模板就是index
),当用户访问的是http://localhost:4200/posts/new,那么进入的就是posts/new.hbs
这个模板(也是渲染到posts.hbs
的{{outlet}}
上)。
关于动态段在前面的Ember.js 入门指南之十三{{link-to}} 助手也介绍过了,在这里就再简单补充下。
路由最主要的任务之一就是加载model
。
例如对于路由this.route('posts');
会加载项目中所有的posts
下的model
。但是当你只想加载其中一个model
的时候怎么处理呢?而且大多数情况我们是不需要一次性加载完全部数据的,一般情况都是加载其中一小部分。这个时候就需要动态段了!
动态段以:
开头,并且后面接着model
的id
属性。
// app/router.js// ……Router.map(function() { this.route('about'); // 注意:访问的URL可以写favs但是项目中如果是使用route的地方仍然是使用favorites // this.route('favorites', { path: '/favs' }); this.route('posts', function() { this.route('post', { path: '/:post_id'}); }); });export default Router;
此时你可以访问http://localhost:4200/posts/1,不过我们还没创建model
所以会报错。这个我们暂时不管,后面会有一章是介绍model
的。现在只要知道你访问http://localhost:4200/posts/1就相当于获取id
值唯一的model
。
Ember也同样运行你使用*
作为URL通配符。有了通配符你可以设置多个URL访问同一个路由。
this.route('about', { path: '/*wildcard' });
然后访问:http://localhost:4200/wildcard或者访问http://localhost:4200/2423432ffasdfewildcard或者http://localhost:4200/2333都是可以进入到about
这个路由,但是http://localhost:4200/posts仍然进入的是posts
这个路由。因为可以匹配到这个路由。
在有路由嵌套的情况下,一般情况我们访问URL的格式都是父路由名/子路由名
,Ember提供了一个resetNamespace:true
选项可以用户重置子路由的命名空间,使用这个设置的路由可以直接这样访问/子路由名
,不需要写父路由名。
this.route('posts', function() { this.route('post', { path: '/:post_id'}); this.route('comments', { resetNamespace: true}, function() { this.route('new'); });});
此时如果你想问的new
这个路由你可以直接不写comments
。http://localhost:4200/posts/new,而不需要http://localhost:4200/posts/comments/new,不过模板渲染的顺序没变,new
模板仍然是渲染到comments
的{{outlet}}
上。
不过个人觉得还是不使用这个设置比较好,特别是在开发的时候你可以看到访问的URL的层次,对你调试代码还是很有帮助的。
以上的内容就是定义路由的全部内容。都是非常重要的知识,希望你能好好掌握,对于路由的嵌套请看之前的文章。如果有疑问请给我留言或者访问官网看原教程。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
路由其中一个很重要的职责就是加载适合的model
,初始化数据,然后在模板上显示数据。
// app/router.js// ……Router.map(function() { this.route('posts');});export default Router;
对于posts
这个路由如果要加载名为post
的model
要怎么做呢?代码实现很简单,其实在前面的代码也已经写过了。你只需要重写model
回调,在回调中返回获取到的model
即可。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // return Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls'); // 加载post(是一个model) return this.store.query('post'); }});
model
回调可以返回一个Ember Data记录,或者返回任何的promise对象(Ember Data也是promise
对象),又或者是返回一个简单的javascript对象、数组都可以。但是需要等待数据加载完成才会渲染模板,所以如果你是使用Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls');
获取远程数据页面上会有一段时间是空白的,其实就是在加载数据,不过这样的用户体验并不好,不过你也不需要担心这个问题,Ember已经提供了解决办法。还记得上一篇的截图的路由表吗?是不是每个路由都有一个xxx_loading
路由,这个路由就是在数据加载的时候执行的,有关xxx_loading
更多详细的信息在后面的博文介绍。
一个route
有时候只加载同一个model
,比如路由/photos
就通常是加载模型photo
。如果用户离开或者是重新进入这个路由模型也不会改变。然而,有些情况下路由所加载的model
是变化的。比如在一个图片展示的APP中,路由/photos
会加载一个photo
模型集合并渲染到模板photos
上。当用户点击其中一幅图片的时候路由只加载被点击的model
数据,当用户点击另外一张图片的时候加载的又是另外一个model
并且渲染到模板上,而且这两次加载的model
数据是不一样的。
在这种情形下,访问的URL就包含了很重要的信息,包括路由和模型。
在Ember应用中可以通过定义动态段实现加载不同的模型。有关动态段的知识在前面的Ember.js 入门指南之十三{{link-to}} 助手和Ember.js 入门指南之二十路由定义已经做过介绍。
一旦在路由中定义了动态段Ember就会从URL中提取动态段的值作为model
回调的第一个参数。
// app/router.js// ……Router.map(function() { this.route('posts', function() { this.route('post', { path: '/:post_id'}); }); });export default Router;
这段代码定义了一个动态段:post_id
,记得动态段是以:
”开头。然后在model
回调中使用动态段获取数据。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { return this.store.findRecord('post', params.post_id); }});
可以看到在model
回调中也是使用在路由中定义的动态段,并把这个动态段作为参数传递给Ember的方法findRecord
,Ember会把URL对应位置上的数据解析到这个动态段上。
注意:在model
中的动态段只在通过URL访问的时候才会被解析。如果你是通过其他方式(比如使用link-to
进入路由)转入路由的,那么路由中model
回调方法里的动态不会被解析,所请求的数据会直接从上下文中获取(你可以把上下文想象成ember的缓存)。下面的代码将为你演示这个说法:
import DS from 'ember-data';export default DS.Model.extend({ title: DS.attr('string'), body: DS.attr('string'), timestamp: DS.attr('number')});
定义了3个属性,id
属性不需要显示定义,ember会默认加上。
Router.map(function() { this.route('posts', function() { this.route('post', { path: '/:post_id'}); }); });
然后用Ember CLI命令(ember g route posts/post
)在创建路由post
,同时也会自动创建出子模板post.hbs
。创建完成之后会得到如下两个文件:
1.app/routes/posts/post.js2.app/templates/posts/post.hbs
修改路由posts.js
,在model
回调中返回设定的数据。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { return [ { "id":"-JzySrmbivaSSFG6WwOk", "body" : "testsssss", "timestamp" : 1443083287846, "title" : "test" }, { "id":"-JzyT-VLEWdF6zY3CefO", "body" : "33333333", "timestamp" : 1443083323541, "title" : "test33333" }, { "id":"-JzyUqbJcT0ct14OizMo" , "body" : "body.....", "timestamp" : 1443083808036, "title" : "title1231232132" } ]; }});
修改posts.hbs
,遍历显示所有的数据。
<ul> {{#each model as |item|}} <li> {{#link-to 'posts.post' item}}{{item.title}}{{/link-to}} </li> {{/each}}</ul><hr>{{outlet}}
修改子路由post.js
,使得子路由根据动态段返回匹配的数据。
// app/routes/posts/post.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { console.log('params = ' + params.post_id); return this.store.findRecord('post', params.post_id); }});
注意打印信息语句console.log()
;,然后接着修改子模板post.hbs
。
<h2>{{model.title}}</h2><p>{{model.body}}</p>
到此,全部所需的测试数据和代码已经编写完毕。下面执行http://localhost:4200/posts,可以看到界面上显示了所有在路由posts的model回调中设置的测试数据。查看页面的HTML代码:
可以看到每个连接的动态段都被解析成了数据的id属性值。注意:随便点击任意一个,注意看浏览器控制台打印的信息。我点击了以第一个连接,浏览器的URL变为
看浏览器的控制台是不是并没有打印出params = -JzySrmbivaSSFG6WwOk
,在点击其他的连接结果也是一样的,浏览器控制台没有打印出任何信息。
下面我我们直接在浏览器地址栏上输入:http://localhost:4200/posts/-JzyUqbJcT0ct14OizMo然后按enter执行,注意看浏览器控制台打印的信息!!!此时打印了params = -JzyUqbJcT0ct14OizMo
,你可以用同样的方式执行另外两个链接的地址。同样也会打印出params = xxx
(xxx
为数据的id
值)。
我想这个例子应该能很好的解释了Ember提示用户需要的注意的问题。只有直接用过浏览器访问才会执行包含了动态段的model
回调,否则不会执行包含有动态段的回调;如果没有包含动态段的model
回调不管是通过URL访问还是通过link-to
访问都会执行。你可以在路由posts
的model
回调中添加一句打印日志的代码,然后通过点击首页上的about
和posts
切换路由,你可以看到控制台打印出了你在model
回调中添加的日志信息。
对于在一个mode
l回调中同时返回多个模型的情况也是时常存在的。对于这种情况你需要在model
回调中修改返回值为Ember.RSVP.hash
对象类型。比如下面的代码就是同时返回了两个模型的数据:一个是song
,一个是album
。
// app/routes/favorites.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return Ember.REVP.hash({ songs: this.store.find('song'), albums: this.store.find('slbum') }); }});
然后在模板favorites.hbs
中就可以使用{{#each}}
把两个数据集遍历出来。遍历的方式与普通的遍历方式一样。
<h2>Song list</h2><ul>{{#each model.songs as |item|}} <li>{{item.name}}</li>{{/each}}</ul><hr><h2>Album list</h2><ul>{{#each model.albums as |item|}} <li>{{item.name}}</li>{{/each}}</ul>
到此所有路由的model
回调的情况介绍完毕,model
回调其实就是把模型绑定到路由上。实现数据的初始化,然后把数据渲染到模板上显示。这也是Ember推荐这么做的——就是把操作数据相关的处理放在route
而不是放在controller
。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
路由的另一个重要职责是渲染同名字的模板。
比如下面的路由设置,posts
路由渲染模板posts.hbs
,路由new
渲染模板posts/new.hbs
。
Router.map(function() { this.route('posts', function() { this.route('new'); });});
每一个模板都会渲染到父模板的{{outlet}}
上。比如上面的路由设置模板posts.hbs
会渲染到模板application.hbs
的{{outlet}}
上。application.hbs
是所有自定义模板的父模板。模板posts/new.hbs会渲染到模板posts.hbs
的{{outlet}}
上。
如果你想渲染到另外一个模板上也是允许的,但是要在路由中重写renderTemplate
回调方法。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ // 渲染到favorites模板上 renderTemplate: function() { this.render('favorites'); }});
模板的渲染这个知识点比较简单,内容也很少,在前面的Ember.js 入门指南之十四番外篇,路由、模板的执行、渲染顺序已经介绍过相关的内容。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
声明:对于transition这个词直译是“过渡”的意思,但是总觉得“路由的过渡”读起来总有那么一点别扭,想了下于是就用“切换”替代吧,如有不妥欢迎指正。
我们熟知的Java、PHP等语言都提供了URL的重定向,那么Ember的重定向又是怎么去实现的呢?
如果是从路由重定向到另外一个路由你可以调用transitionTo
方法,如果是从controller
重定向到一个route
则调用transitionToRoute
方法。transitionTo
方法所实现的功能与link-to
的作用是一样的,都可以实现路由的切换。如果重定向之后的路由包含有动态段你需要解析model
数据或者指定动态段的值。由于不是直接执行URL所以不会执行重定向之后的路由的model
回调。
如果你想在路由切换的时候不加载model
你可以调用beforeModel
回调,在这个回调中实现路由的切换。
beforeModel() { this.transitionTo('posts');}
有些情况下你需要先根据model
回调获取到的数据然后判断跳转到某个路由上。此时你可以使用afterModel
回调方法。
afterModel: function(model, transition) { if (model.get(‘length’) === 1) { this.transitionTo('post', model.get('firstObject')); }}
切换路由,并初始化数据为model
的第一个元素数据。
Router.map(function() { this.route('posts', function() { this.route('post', { path: '/:post_id'}); }); });
子路由的重定向有些许不同,如果你需要重定向到上面这个段代码的子路由posts.post
上,如果是使用beforeModel
、model
、afterModel
回调重定向到posts.post
父路由posts
会重新在执行一次,再次执行父路由这种方式就显得有点多余了,特别父路由需要加载的数据比较多的时候,会影响到加载的效率。如果是这种情况我们可以使用redirect
回调,此回调不会再次执行父路由。仅仅是实现路由切换而已。
redirect: function(model, transition) { if (model.get('length') === 1) { this.transitionTo('posts.post', model.get('firstObject')); }}
重定向到子路由,解析之后会得到的类似于posts/2
这种形式的URL。
以上就是全部路由的重定向方式,主要有4个回调:beforeModel
、model
、afterModel
、redirect
。前面三种使用场景差别不大,redirect
主要用于重定向到子路由。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
在路由的转换过程中,Ember路由器会通过回调(beforeModel
、model
、afterModel
、redirect
)解析一个transition
对象到转换的下一路由中。任何一个回调都可以通过传递过来的transition
参数获取transition
对象,然后使用这个对象调用transition.abort()
方法立即终止路由的转换,如果你的程序保存了这个对象(transition
对象)之后你还可以在需要的地方取出来并调用transition.retry()
方法激活路由转换这个动作,最终实现路由的转换。
当用户通过{{link-to}}
助手、transition
方法或者直接执行URL来转换路由,当前路由会自动执行willTransition
方法。每个活动的路由都可以决定是否执行转换路由。
想象一下,在当前路由所渲染的页面是一个比较复杂的表单,并且用户已经填写了很多信息,但是用户很可能无意中点击了返回或者关闭页面,这就导致了用户填写的信息直接丢失了,这样的用户体验并不好。此时我们可以通过使用willTransition
方法阻止用户的行为并提示用户是否确认离开本页面。
为了验证这个特性我们需要创建好测试所需的文件。
ember g controller formember g route form
首先在controller
增加测试数据。
// app/controllers/form.jsimport Ember from 'ember';export default Ember.Controller.extend({ firstName: 'chen', lastName: 'ubuntuvim'});
再创建一个模拟用户填写信息的模板。
<div class="form-group"> FirstName {{input type="text" class="form-control" id="exampleInputEmail1" placeholder="FirstName" value=firstName}} </div> <div class="form-group"> LashName {{input type="text" class="form-control" id="exampleInputPassword1" placeholder="LashName" value=lastName}} </div> <button type="submit" class="btn btn-primary">Submit</button><br><br>{{#link-to 'about'}}<b>转到about</b>{{/link-to}}
关键部分来了,我们在路由里添加willTransition
方法。
// app/routes/form.jsimport Ember from 'ember';export default Ember.Route.extend({ actions: { willTransition: function(transition) { // 如果是使用this.get('key')获取不了页面输入值,因为不是通过action提交表单的 var v = this.controller.get('firstName'); // 任意获取一个作为判断表单输入值 if (v && !confirm("你确定要离开这个页面吗??")) { transition.abort(); } else { return true; } } }});
运行:http://localhost:4200/form,先点击submit
提交表单,可以看到表单顺利提交没有任何问题,然后再点击转到about
,你可以看到会弹出如下提示框。
接着,点击“取消”页面没有跳转,如果是点击“确定”页面会跳转到about
页面。再接着,把FirstName
这个输入框的内容清空然后点击“转到about”页面直接跳转到了about
页面。
很多博客网站都是有这个功能的!!
beforeModel(transition) { if (new Date() > new Date('January 1, 1980')) { alert('Sorry, you need a time machine to enter this route.'); transition.abort(); }}
这段代码演示的就是在beforeModel
回调中使用abort
方法阻止路由的转换。代码比较简单我就不做例子演示了!
对于使用abort
方法终止的路由可以调用retry
方法重新激活。一个很典型的例子就是登陆。如果登陆成功就转到首页,否则跳转回登陆页面。文件准备工作:
ember g controller authember g route authember g controller loginember g route login
下面是演示用到的代码。
// app/controllers/login.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { login: function() { // 获取跳转过来之前路由中设置的transition对象 var transitionObj = this.get('transitionObj'); console.log('transitionObj = ' + transitionObj); if (transitionObj) { this.set("transitionObj", null); transitionObj.retry(); } else { // 转回首页 this.transitionToRoute('index'); } } }});
// app/controllers/auth.jsimport Ember from 'ember';export default Ember.Controller.extend({ userIsLogin: false});
// app/routes/auth.jsimport Ember from 'ember';export default Ember.Route.extend({ beforeModel(transition) { // 在名为auth的controller设置了userIsLogin为false,默认是未登录 if (!this.controllerFor("auth").get('userIsLogin')) { var loginController = this.controllerFor("login"); // 保存transition对象 loginController.set("transitionObj", transition); this.transitionTo("login"); // 跳转到路由login } }});
这个是登陆页面
login
页面,结果显示如下:可以看到URL确实是转到login
了。
transitionObj
是undefined
。由于没有经auth
这个路由的跳转所以获取不到transition
对象。自然就跳转回index
这个路由。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
在前面的Ember.js 入门指南之二十路由定义提过loading
、error
子路由,它们是Ember默认创建的,并在beforeModel
、model
、afterModel
这三个回调执行完毕之前会先渲染当前路由的loading
和error
模板。
Router.map(function() { this.route('posts', function() { this.route('post', { path: '/:post_id'}); });});
对于上述的路由设置Ember会生成如下的路由列表:
每个路由都会自动生成一个loading
、error
路由,下面我将一一演示这两个路由的作用。图片前面loading
、error
路由对应的application
路由。posts_loading
和posts_error
对应的是posts
路由。
Ember建议数据放在beforeModel
、model
、afterModel
回调中获取并传递到模板显示。但是只要是加载数据就需要时间,对于Ember应用来说,在model
等回调中加载完数据才会渲染模板,如果加载数据比较慢那么用户看到的页面就是一个空白的页面,用户体验很差!
Ember提供的解决办法是:在beforeModel
、model
、afterModel
回调还没返回前先进入一个叫loading
的子状态,然后渲染一个叫routeName-loading
的模板(如果是application
路由则对应的直接是loading
、error
不需要前缀)。
为了演示这效果在app/templates
下创建一个posts-loading
模板。如果程序正常,在渲染模板posts
之前会先渲染这个模板。
<img src="assets/images/loading/loading.gif" />
然后修改路由posts.js
,让model
回调执行时间更长一些。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 模拟一个延时操作, for (var i = 0; i < 10000000;i++) { } return Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls'); }});
执行http://localhost:4200/posts,首先会看到执行的loading
模板的内容,然后才看到真正要显示的数据。有一个加载过程,如下面2幅图片所示。
在beforeModel
、model
、afterModel
回调没有立即返回之前,会先执行一个名为loading的事件。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 模拟一个延时操作, for (var i = 0; i < 10000000;i++) { } return Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls'); }, actions: { loading: function(transition, originRoute) { alert("Sorry this is taking so long to load!!"); } }});
页面刷新后会弹出一个提示框,先不点击“确定”。打开浏览器的“开发者 -> 开发者工具”,切换到Network标签下。找到“pulls”这个请求,点击它。
从图中可以看到此时model
回调并没有返回。此时响应的内容是空的,说明loading
事件实在model
回调返回之前执行了。
然后点击弹出框的“确定”,此时可以看到Response有数据了。说明model
回调已经执行完毕。注意:如果当前的路由没有显示定义loading
事件,这个时间会冒泡到父路由,如果父路由也没有显示定义loading
事件,那么会继续向上冒泡,一直到最顶端的路由application
。
与loading
子状态类似,error
子状态会在beforeModel
、model
、afterModel
回调执行过程中出现错误的时候触发。
命名方式与loading
子状态也是类似的。现在定义一个名为posts-error.hbs
的模板。
<p style="color: red;">posts回调解析出错。。。。</p>
然后在model
回调中手动添加一个错误代码。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 模拟一个延时操作, for (var i = 0; i < 10000000;i++) { } var e = parseInt(value); return Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls'); }});
注意var e = parseInt(value);
这句代码,由于value
没有定义所以应该会报错。那么此时页面会显示什么呢??
如果你的演示程序没有其他问题那么你也会得到上图的结果。但是如果没有定义这个模板,那么界面上将是什么都不显示。
如果你想在xxx-error.hbs
模板上看到是什么错误信息,你可以在模板上打印model
的值。如下:
<p style="color: red;">posts回调解析出错。。。。<br>{{model}}</p>
此时页面会显示出你的代码是什么错误。
不过相比于浏览器控制台打印的错误信息简单很多!!!
error
事件与第一点讲的loading事件也是相似的。使用方式与loading
一样。个人觉得这个事件非常有用,我们可以在这个事件中根据error
状态码的不同执行不同的逻辑,比如跳转到不同的路由上。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls____'); }, actions: { error: function(error, transition) { console.log('error = ' + error.status); // 打印error对象里的所有属性和方法名 for(var name in error){ console.log(name); // console.log('属性值或者方法体==》' + error[name]); } alert(names); if (error && error.status === 400) { return this.transitionTo("about"); } else if (error.status === 404) { return this.transitionTo("form"); } else { console.log('else......'); } } }});
注意getJSON
方法里的URL,我在URL后面随机加了一些字符,目的是让这个URL不存在。此时请求应该会找不到这个地址error
的响应码应该是404。然后直接跳转到form
这个路由上。运行http://localhost:4200/posts之后,浏览器控制台打印信息如下:
页面也会跳转到form
。
到此路由加载数据过程中涉及的两个状态loading
和error
的内容全部介绍完,这两个状态在优化用户体验方面是非常有用的,希望想学习Ember的同学好好掌握!!!=^=
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
查询参数是在URL的问号(?)右边部分,通常是键值对形式出现。
http://example.com/articles?sort=ASC&page=2
比如这个URL的查询参数有两个,一个是sort
,一个是page
,它们的值分别是ASC
和2
。
查询参数通常是声明为controller
类中。比如在当前活动路由articles
下,你需要根据文章的类型category
过滤,此时你必须要在controller
内声明过滤参数category
。
使用Ember CLI新建一个controller
、route
:
ember g controller article;ember g route articles;
// app/controllers/articles.jsimport Ember from 'ember';export default Ember.Controller.extend({ queryParams: ['category'], category: null});
绑定一个查询参数到URL,并且参数的值为null
。当你进入路由articles
时,如果参数category
的值发生变化会自动更新到controller
中的category
;反之亦然。你可以设置一个默认值,比如把category
设置为Java
。可以在模板上获取这个值。
{{outlet}}category = {{category}}
执行http://localhost:4200/articles,页面会显示出 category = Java
。如果执行http://localhost:4200/articles?category=PHP,那么页面会显示category = PHP
。
下面代码演示了怎么使用查询参数:
// app/controllers/articles.jsimport Ember from 'ember';export default Ember.Controller.extend({ queryParams: ['category'], category: null, // 定义一个返回数组的计算属性,可以直接在模板上遍历 filteredArticles: Ember.computed('category', 'model', function() { var category = this.get('category'); var articles = this.get('model'); if (category) { return articles.filterBy('category', category); } else { return articles; } })});
创建一个计算属性,这个计算属性是一个数组类型。由于是计算属性,并且这个计算属性关联了另外两个属性category
和model
,只要这两个属性其中之一发生改变都会导致filteredArticles
发生改变,所以返回的数组元素也会跟着改变。
在route
初始化测试数据。
// app/routes/article.jsimport Ember from 'ember';export default Ember.Route.extend({ model(params) { return [ { id: 1, title: 'Bower: dependencies and resolutions new', body: "In the bower.json file, I see 2 keys dependencies and resolutionsWhy is that so? I understand Bower has a flat dependency structure. So has it got anything to do with that ?", category: 'java' }, { id: 2, title: 'Highly Nested JSON Payload - hasMany error', body: "Welcome to the Ember.js discussion forum. We're running on the open source, Ember.js-powered Discourse forum software. They are also providing the hosting for us. Thanks guys! Please use this space for discussion abo… read more", category: 'php' }, { id: 3, title: 'Passing a jwt to my REST adapter new ', body: "This sets up a binding between the category query param in the URL, and the category property on controller:articles. In other words, once the articles route has been entered, any changes to the category query param in the URL will update the category property on controller:articles, and vice versa.", category: 'java' } ]; }});
下面看看怎么在模板显示数据,并且根据category
显示不同数据。
<div class="col-md-4 col-xs-4"><ul> 输入分类:{{input value=category placeholder ='查询的分类'}}</ul><ul> {{#each filteredArticles as |item|}} <li> {{#link-to 'articles.article' item}} {{item.title}}--{{item.category}} {{/link-to}} </li> {{/each}}</ul></div><div class="col-md-8 col-xs-8">{{outlet}}</div>
精彩的时刻到了!!先执行http://localhost:4200/articles,此时显示的是所有类型的数据。如下图:
接着你就可以做点好玩的事了,直接在输入框输入分类名。由于计算属性的特性会自动更新数组filteredArticles
。所以我们可以看到随着你输入字符的变化显示的数据也在变化!这个例子也说明了Ember计算属性自动更新变化的强大!!用着确实爽啊!!官网教程没有说怎么在模板中使用,讲得也不是很明白,就给了一句
Now we just need to define a computed property of our category-filtered array that the articles template will render:”
也有可能是我看不懂,反正摸索好一阵子才知道要这么用!!
link-to
助手使用query-params
子表达式直接指定查询参数,需要注意的是这个表达式需要放在括号内使用,切记别少了这个括号。
……<ul> {{#link-to 'articles' (query-params category='java')}} java {{/link-to}} <br> {{#link-to 'articles' (query-params category='php')}} php {{/link-to}} <br> {{#link-to 'articles' (query-params category='')}} all {{/link-to}}</ul>……
在显示数据的ul标签后面新增上述两个link-to
助手。它们的作用分别是指定分类类型为java、php、全部。但用户点击三个连接直接显示与连接指定类型匹配的数据(并且查询的输入框也变为链接指定的类型值)。比如我点击了第一个链接,输入显示如下图:
route
对象的transitionTo
方法和controller
对象的transitionToRoute
方法都可以接受final
类型的参数。并且这个参数是一个包括一个key
为queryParams
的对象。
修改前面已经创建好的路由posts.js
。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ beforeModel: function(params) { // 转到路由articles上,并且传递查询参数category,参数值为Java this.transitionTo('articles', { queryParams: { category: 'java' }}); }});
执行http://localhost:4200/posts后,可以看到路由直接跳转到http://localhost:4200/articles?category=java,实现了路由切换的同时也指定了查询的参数。界面显示的数据我就不截图了,程序不出错,显示的都是category
为java
的数据。
另外还有三种切换路由的方式。
// 可以传递一个object过去this.transitionTo('articles', object, { queryParams: { category: 'java' }});// 这种方式不会改变路由,只是起到设置参数的作用,如果你在//路由articles中使用这个方法,你的路由仍然是articles,只是查询参数变了。this.transitionTo({ queryParams: { direction: 'asc' }});// 直接指定跳转的URL和查询参数this.transitionTo('/posts/1?sort=date&showDetails=true');
上面的三种方式请读者自己编写例子试试吧。光看不练假把式……
transitionTo
和link-to
提供的参数仅会改变查询参数的值,而不会改变路由的层次结构,这种路由的切换被认为是不完整的,这也就意味着比如model
和setupController
回调方法就不会被执行,只是使得controller
里的属性值为新的查询参数值以及更新URL。
但是有些情况是查询参数改变需要从服务器重新加载数据,这种情况就需要一个完整的路由切换了。为了能在查询参数改变的时候切换到一个完整的路由你需要在controller
对应的路由中配置一个名为queryParams
哈希对象。并且需要设置一个名为refreshModel
的查询参数,这个参数的值为true
。
queryParams: { category: { refreshModel: true }},model: function(params) { return this.store.query('article', params);}
关于这段代码演示实例请查看官方提供的代码!
默认情况下,Ember使用pushState
更新URL来响应controller
类中查询参数属性的变化,但是如果你想使用replaceState
来替换pushState
你可以在route
类中的queryParams
哈希对象中设置replace
为true
。设置为true
表示启用这个设置。
queryParams: {category: { replaceState:true}}
默认情况下,在controller
类中指定的查询属性foo
会绑定到名为foo
的查询参数上。比如:?foo=123
。你也可以把查询属性映射到不同的查询参数上,语法如下:
// app/controllers/articles.jsimport Ember from 'ember';export default Ember.Controller.extend({ queryParams: { category: 'articles_category' } category: null});
这段代码就是把查询属性category
映射到查询参数articles_category
上。对于有多个查询参数的情况你需要使用数组指定。
// app/controllers/articles.jsimport Ember from 'ember';export default Ember.Controller.extend({ queryParams: ['page', 'filter', { category: 'articles_category' }], category: null, page: 1, filter: 'recent'});
上述代码定义了三个查询参数,如果需要把属性映射到不同名的参数需要手动指定,比如category
。
export default Ember.Controller.extend({ queryParams: 'page', page: 1});
在这段代码中设置了查询参数page
的默认值为1
。
这样的设置会有两种默认的行为:
1.查询的时候查询属性值会根据默认值的类型自动转换,所以当用户输入http://localhost:4200/articles?page=1的时候page
的值1
会被识别成数字1
而不是字符'1'
,应为设置的默认值1
是数字类型。2.当查询的值正好是默认值的时候,该值不会被序列化到URL中。比如查询值正好是?page=1
这种情况URL可能是/articles
,但是如果查询值是?page=2
,URL肯定是/articles?page=2
。
默认情况下,在Ember中查询参数是“粘性的”,也就是说如果你改变了查询参数或者是离开页面又回退回来,新的查询值会默认在URL上,而不会自动清除(几乎所见的URL都差不多是这种情况)。这是个很有用的默认设置,特别是当你点击后退回到原页面的时候显示的数据没有改变。
此外,粘性的查询参数值会被加载的route
存储或者回复。比如,包括了动态段/:post_id
的路由posts
,以及路由对应的controller
包含了查询属性filter
。如果你导航到/badgers
并且根据reookies
过滤,然后再导航到/bears
并根据best
过滤,然后再导航到/potatose
并根据lamest
过滤。如下面的链接:
<ul> {{#link-to 'posts' 'badgers'}}Badgers{{/link-to}}<br> {{#link-to 'posts' 'bears'}}Bears{{/link-to}}<br> {{#link-to 'posts' 'potatoes'}}Potatoes{{/link-to}}<br></ul>
模板编译之后得到如下HTML代码:
<ul> <a href="/badgers?filter=rookies">Badgers</a> <a href="/bears?filter=best">Bears</a><a href="/potatoes?filter=lamest">Potatoes</a></ul>
可以看到一旦你改变了查询参数,查询参数就会被存储或者是关联到route
所加载的model
上。如果你想重置查询参数你有如下两种方式处理:
1.在link-to
或者transitionTo
上显式指定查询参数的值;2.使用Route.resetController
回调设置查询参数的值并回退到切换之前的路由或者是改变model
的路由。
下面的代码片段演示了一个查询参数在controller
中重置为1
,同时作用于切换前ActiclesRoute
的model
。结果就是当返回到当前路由时查询值已经被重置为1
。
// app/routes/article.jsimport Ember from 'ember';export default Ember.Route.extend({ resetController(controller, isExiting, transition) { // 只有model发生变化的时候isExiting才为false if (isExiting) { // 重置查询属性的值 controller.set('page', 1); } }});
某些情况下,你不想是用查询参数值限定路由模式,而是让查询参数值改变的时候路由也跟着改变并且会重新加载数据。这时候你可用在对应的controller
类中设置queryParams
哈希对象,在这对象中配置一个参数scope
为controller
。如下:
queryParams: [{ showMagnifyingGlass: { scope: 'controller' }}]
粘性的查询参数值这个只是点理解起来好难的说,看下一遍下来都不知道这个有何用!!!现在还是学习阶段还没真正在项目中使用这个特性,所以我也不知道怎么解释更容易理解,建议直接看官网教程吧!!
说明:本文是基于官方2.0参考文档缩写,相对于其他版本内容会有出入。
以上的内容就是有关查询参数的全部了,主要是理解了查询参数的设置使用起来也就没什么问题。有点遗憾的是没能写出第4点的演示实例!能力有限只能遇到或者明白其使用的时候再补上了!!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
本文将为你介绍路由的高级特性,这些高级特性可以用于处理项目复杂的异步逻辑。
关于单词promises,直译是承诺,但是个人觉得还是使用原文吧。读起来顺畅点。
Ember的路由处理异步逻辑的方式是使用Promise。简而言之,Promise就是一个表示最终结果的对象。这个对象可能是fulfill
(成功获取最终结果)也可能是reject
(获取结果失败)。为了获取这个最终值,或者是处理Promise失败的情况都可以使用then
方法,这个方法接受两个可选的回调方法,一个是Promise获取结果成功时执行,一个是Promise获取结果失败时执行。如果promises获取结果成功那么获取到的结果将作为成功时执行的回调方法的参数。相反的,如果Promise获取结果失败,那么最终结果(失败的原因)将作为Promise失败时执行的回调方法的参数。比如下面的代码段,当Promise获取结果成功时执行fulfill
回调,否则执行reject
回调方法。
// app/routes/promises.jsimport Ember from 'ember';export default Ember.Route.extend({ beforeModel: function() { console.log('execute model()'); var promise = this.fetchTheAnswer(); promise.then(this.fulfill, this.reject); }, // promises获取结果成功时执行 fulfill: function(answer) { console.log("The answer is " + answer); }, // promises获取结果失败时执行 reject: function(reason) { console.log("Couldn't get the answer! Reason: " + reason); }, fetchTheAnswer: function() { return new Promise(function(fulfill, reject){ return fulfill('success'); //如果返回的是fulfill则表示promises执行成功 //return reject('failure'); //如果返回的是reject则表示promises执行失败 }); }});
上述这段代码就是promises的一个简单例子,promises的then
方法会根据promises的获取到的最终结果执行不同的回调,如果promises获取结果成功则执行fulfill
回调,否则执行reject
回调。
promises的强大之处不仅仅如此,promises还可以以链的形式执行多个then
方法,每个then方法都会根据promises的结果执行fulfill
或者reject
回调。
// app/routes/promises.jsimport Ember from 'ember';export default Ember.Route.extend({ beforeModel() { // 注意Jquery的Ajax方法返回的也是promises var promiese = Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls'); promiese.then(this.fetchPhotoOfUsers) .then(this.applyInstagramFilters) .then(this.uploadThrendyPhotAlbum) .then(this.displaySuccessMessage, this.handleErrors); }, fetchPhotoOfUsers: function(){ console.log('fetchPhotoOfUsers'); }, applyInstagramFilters: function() { console.log('applyInstagramFilters'); }, uploadThrendyPhotAlbum: function() { console.log('uploadThrendyPhotAlbum'); }, displaySuccessMessage: function() { console.log('displaySuccessMessage'); }, handleErrors: function() { console.log('handleErrors'); }});
这种情况下会打印什么结果呢??
在前的文章已经使用过Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls');
获取数据,是可以成功获取数据的。所以promises获取结果成功,应该执行的是获取成功对应的回调方法。浏览器控制台打印结果如下:
fetchPhotoOfUsersapplyInstagramFiltersuploadThrendyPhotAlbumdisplaySuccessMessage
但是如果我把Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls');
改成一个不存在的URL,比如改成Ember.$.getJSON('https://www.my-example.com');
执行代码之后控制台会提示出404
错误,并且打印'handleErrors'。说明promises获取结果失败,执行了then里的reject回调。为了验证每个回调的reject
方法再修改修改代码,如下:
// app/routes/promises.jsimport Ember from 'ember';export default Ember.Route.extend({ beforeModel() { // 注意Jquery的Ajax方法返回的也是promises var promiese = Ember.$.getJSON(' https://www.my-example.com '); promiese.then(this.fetchPhotoOfUsers, this.fetchPhotoOfUsersError) .then(this.applyInstagramFilters, this.applyInstagramFiltersError) .then(this.uploadThrendyPhotAlbum, this.uploadThrendyPhotAlbumError) .then(this.displaySuccessMessage, this.handleErrors); }, fetchPhotoOfUsers: function(){ console.log('fetchPhotoOfUsers'); }, fetchPhotoOfUsersError: function() { console.log('fetchPhotoOfUsersError'); }, applyInstagramFilters: function() { console.log('applyInstagramFilters'); }, applyInstagramFiltersError: function() { console.log('applyInstagramFiltersError'); }, uploadThrendyPhotAlbum: function() { console.log('uploadThrendyPhotAlbum'); }, uploadThrendyPhotAlbumError: function() { console.log('uploadThrendyPhotAlbumError'); }, displaySuccessMessage: function() { console.log('displaySuccessMessage'); }, handleErrors: function() { console.log('handleErrors'); }});
由于promises获取结果失败故执行其对应的失败处理回调。这种调用方式有点类似于try……catch……
,但是本文的重点不是讲解promises,更多有关promises的教材请读者自行Google或者百度吧,在这里介绍一个js库RSVP.js,它可以让你更加简单的组织你的promises代码。在附上几个promises的参考网站:
极力推荐看第二个网站的教材,这个网站可以直接运行js代码。还有源码和PDF。非常棒!!!
当发生路由切换的时候,在model
回调(或者是beforeMode
、afterModel
)中获取的数据集会在切换完成的时候传递到路由对应的controller
上。如果model回调返回的是一个普通的对象(非promises对象)或者是数组,路由的切换会立即执行,但是如果model回调返回的是一个promises对象,路由的切换将会被中止直到promises执行完成(返回fulfill
或者是reject
)才切换。
路由器任务任何一个包含了then方法的对象都是一个promises。
如果promises获取结果成功则会从被中止的地方继续往下执行或者是执行路由链的下一个路由,如果promises返回的依然是一个promises,那么路由依然再次被中止,等待promises的返回结果,如果是fulfill
则从被中止的地方开始往下执行,以此类推,一直到获取到model
回调所需的结果。
传递到每个路由的setupController
回调的值都是promises返回fulfill
时的值。如下代码:
// app/routes/tardy.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return new Ember.RSVP.Promise(function(resolver) { console.log('start......'); Ember.run.later(function() { resolver({ msg: 'Hold your horses!!'}); }, 3000); }); }, setupController(controller, model) { console.log('msg = ' + model.msg); }});
一进入路由tardy
,model
回调就会被执行并且返回一个延迟3秒才执行的promises,在这期间路由会中止。当promises返回fulfill
路由会继续执行,并将model
返回的对象传递到setupController
方法中。
虽然这种中止的行为会影响响应速度但是这是非常必要的,特别是你需要保证model
回调得到的数据是完整的数据的时候。
文章前面主要讲的是promises获取结果成功的情况,但是如果是获取结果失败的情况又是怎么处理呢??
默认情况下,如果model
回调返回的是一个promises对象并且此promises返回的是reject
,此时路由切换将被终止,也不会渲染对应的模板,并且会在浏览器控制台打印出错误日志信息,例子promises-ret-reject.js
会演示。
你可以自定义处理出错信息的逻辑,只要在route
的actions
哈希对象中配置即可。当promises获取结果失败的默认情况下会执行一个名为error
的处理事件,否则会执行你自定义的处理事件。
// app/routes/promises-ret-reject.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 为了测试效果直接返回reject return Ember.RSVP.reject('FAIL'); }, actions: { error: function(reason) { console.log('reason = ' + reason); // 如果你想让这个事件冒泡到顶级路由application只需要返回true // return true; } }});
如果没有不允许事件冒泡打印结果仅仅是reason = FAIL
。并且页面上什么都不显示(不渲染模板)。
如果去掉最后一行代码的注释,让事件冒泡到顶级路由application
中的默认方法处理,那么结果又是什么呢?
结果是先打印了处理结果,然后再打印出提示错误的日志信息。并且页面上什么都不显示(不渲染模板)。
在前面第3点介绍了promises获取结果失败时会终止路由转换,但是如果model
返回是一个promises链呢?程序能到就这样死了!!!显然是不行的,做法是把model回调中返回的reject
转换为fulfill
。这样就可以继续执行或者切换到下一个路由了!
// app/routes/funky.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { var promises = Ember.RSVP.reject('FAIL'); // 由于已经知道promises返回的是reject,所以fulfill回调直接写为null return promises.then(null, function() { return { msg: '恢复reject状态:其实就是在reject回调中继续执行fulfill状态下的代码。' }; }); }});
为了验证model
回调的结果,直接在模板上显示msg
。
funky模板<br>{{model.msg}}
执行URL:http://localhost:4200/funky,得到如下结果:
说明model回调进入到reject回调中,并正确返回了预期结果。
到本文为止有关路由这以整章的内容也全部介绍完毕了!!难点在Ember.js 入门指南之二十六查询参数这一篇。能力有限没有把这篇的内容讲明白,暂时搁下待日后完善!
总的来说路由主要职责是获取数据,根据逻辑处理数据。有点MVC架构的dao层,专门做数据的CRUD操作。当然另外一个重要职责就是路由的切换,以及切换的时候参数的设置问题。
结束完这一章下一章接着介绍组件(Component)。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
不得不说,Ember的更新是在是太快了!!本教程还没写到一半就又更新到v2.1.0
了!!!!不过为了统一还是使用官方v2.0.0
的参考文档!!
从本篇开始进入新的一章——组件。这一章将用6篇文章介绍Ember的组件,从它的定义开始知道它的使用方式,我将为你一一解答!
准备工作:本章代码统一访问项目chapter4_components下,项目代码可以在以下网址上找到:https://github.com/ubuntuvim/my_emberjs_code
与之前的文章一样,项目仍然是使用Ember CLI命令创建项目和各个组件文件。
创建项目并测试运行,首先执行如下四条命令,最后在浏览器执行:http://localhost:4200/。
ember new chapter4_componentscd chapter4_componentsember server
如果你能在页面上看到Welcome to Ember说明项目框架搭建成功!那么你可以继续往下看了,否则想搭建好项目再往下学习~~~
创建组件方法很简单:ember generate component my-component-name
。一条命令即可,但是需要注意的是组件的名称必须要包含中划线-
,比如blog-post
、test-component
、audio-player-controls
这种格式的命名是合法,但是post
、test
这种方式的命名是不合法的!其一是为了防止用户自定义的组件名与W3C规定的元素标签名重复;其二是为了确保Ember能自动检测到用户自定义的组件。
下面定义一个组件,ember g component blog-post
。Ember CLI会自动为你创建组件对应的的模板,执行这条命令之后你可以在app/components
和app/templates/components
下看到创建的文件。
<h1>{{title}}</h1> <p>{{yield}}</p> <p>Edit title: {{input type="text" value=title}}</p>
为了演示组件的使用需要做些准备工作:ember g route index
// app/routes/index.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return [ { id: 1, title: 'Bower: dependencies and resolutions new', body: "In the bower.json file, I see 2 keys dependencies and resolutionsWhy is that so? I understand Bower has a flat dependency structure. So has it got anything to do with that ?", category: 'java' }, { id: 2, title: 'Highly Nested JSON Payload - hasMany error', body: "Welcome to the Ember.js discussion forum. We're running on the open source, Ember.js-powered Discourse forum software. They are also providing the hosting for us. Thanks guys! Please use this space for discussion abo… read more", category: 'php' }, { id: 3, title: 'Passing a jwt to my REST adapter new ', body: "This sets up a binding between the category query param in the URL, and the category property on controller:articles. In other words, once the articles route has been entered, any changes to the category query param in the URL will update the category property on controller:articles, and vice versa.", category: 'java'} ]; }});
{{#each model as |item|}} {{#blog-post title=item.title}} {{item.body}} {{/blog-post}}{{/each}}
在这段代码中,使用了自定义的组件来显示数据。最后页面显示如下:
自定义的组件被渲染到了模板index.hbs
使用blog-post
的地方。并且自定义组件的HTML标签没有变化。到这里大概应该知道怎么去使用组件了,至于它是怎么就渲染到了使用组件的地方,以及它是怎么渲染上去的。别急~~后面的文章会为你一一解答。
说明:默认情况下,自定义的组件会被渲染到div
标签内,当然这种默认情况也是可以修改的,比较简单在此不过多介绍,请自行学习,网址:customizing-a-components-element。
用户自定义的组件类都需要继承Ember.Component
类。
通常情况下我们会把经常使用的模板片段封装成组件,只需要定义一次就可以在项目任何一个模板中使用,而且不需要编写任何的javascript代码。比如上述第一点“自定义组件及使用”中描述的一样。
但是如果你想你的组件有特殊的行为,并且这些行为是默认组件类无法提供的(比如:改变包裹组件的标签、响应组件模板初始化某个状态等),那么此时你可以自定义组件类,但是要继承Ember.Component
,如果你自定义的组件类没有继承这个类,你自定义的组件就很有可能会出现一些不可预知的问题。
Ember所能识别的自定义组件类的名称是有规范的。比如,你定义了一个名为blog-post
的组件,那么你的组件类的名称应该是app/components/blog-post.js
。如果组件名为audio-player-controls
那么对应的组件类名为app/components/audio-player-controls.js
。即:组件类名与组件同名,这个是v2.0
的命名方法,请区别就版本的Ember,旧版本的组件命名规则是驼峰式的命名规则。
举个简单的例子,在第一点“自定义组件及使用”中讲过,组件默认会被渲染到div
标签内,你可以在组件类中修改这个默认标签。
// app/components/blog-post.jsimport Ember from 'ember';export default Ember.Component.extend({ tagName: 'nav'});
这段代码修改了包裹组件的标签名,页面刷新后HTML代码如下:
可以看到组件的HTML代码被包含在nav
标签内。
组件的动态渲染与Java的多态有点相似。{{component}}
助手会延迟到运行时才决定使用那个组件渲染页面。当程序需要根据数据不同渲染不同组件的时,这种动态渲染就显得特别有用。可以使你的逻辑和试图分离开。
那么要怎么使用呢?非常简单,只需要把组件名作为参数传递过去即可,比如:使用{{component 'blog-post'}}
与{{blog-post}}
结果是一致的。我们可以修改第一点“自定义组件及使用”实例中模板index.hbs
的代码。
{{#each model as |item|}} {{component 'blog-post' title=item.title}} {{item.body}}{{/each}}
页面刷新之后,可以看到结果是一样的。
下面为读者演示如何根据数据不同渲染不同的组件。
按照惯例,先做好准备工作,使用Ember CLI命令创建2个不同的组件。
ember g component foo-componentember g component bar-component
<h1>Hello from bar</h1><p>{{post.body}}</p>
为何能用post
获取数据,因为在使用组件的地方传递了参数。在模板index.hbs
中可以看到。
<h1>Hello from foo</h1><p>{{post.body}}</p>
修改显示的数据,注意数据的最后增加一个属性pn
,pn
的值就是组件的名称。
// app/routes/index.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return [ { id: 1, title: 'Bower: dependencies and resolutions new', body: "In the bower.json file, I see 2 keys dependencies and resolutionsWhy is that so? I understand Bower has a flat dependency structure. So has it got anything to do with that ?", pn: 'bar-component' }, { id: 2, title: 'Highly Nested JSON Payload - hasMany error', body: "Welcome to the Ember.js discussion forum. We're running on the open source, Ember.js-powered Discourse forum software. They are also providing the hosting for us. Thanks guys! Please use this space for discussion abo… read more", pn: 'foo-component' }, { id: 3, title: 'Passing a jwt to my REST adapter new ', body: "This sets up a binding between the category query param in the URL, and the category property on controller:articles. In other words, once the articles route has been entered, any changes to the category query param in the URL will update the category property on controller:articles, and vice versa.", pn: 'bar-component'} ]; }});
修改调用组件的模板index.hbs
。
{{#each model as |item|}} {{component item.pn post=item}}{{/each}}
模板编译之后会得到形如{{component foo-component post}}
的组件调用代码。
相信你应该了解了动态渲染组件是怎么回事了!自己动手试试吧~~
到此组件的定义与使用介绍完毕了,不知道你有没有学会呢?如果你有疑问请给我留言或者直接看官方教程学习。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
每个组件都是相对独立的,因此任何组件所需的数据都需要通过组件的属性把数据传递到组件中。
比如上篇Ember.js 入门指南之二十八组件定义的第三点{{component item.pn post=item}}
就是通过属性post把数据传递到组件foo-component
或者bar-component
上。如果在index.hbs
中是如下方式调用组件那么渲染之后的页面是空的。{{component item.pn}}
请读者自己修改index.hbs
的代码后演示效果。
传递到组件的参数也是动态更新的,当传递到组件上的参数变化时组件渲染的HTML也会随之发生改变。
传递的属性参数不一定要指定参数的名字。你可以不指定属性参数的名称,然后根据参数的位置获取对应的值,但是要在组件对应的组件类中指定位置参数的名称。比如下面的代码:
准备工作:
ember g route passing-properties-to-componentember g component passing-properties-to-component
调用组件的模板,传入两个位置参数,分别是item.title
、item.body
。
!-- apptemplatespassing-properties-to-component.hbs --{{#each model as item}} !-- 传递到组件blog-post第一个参数为数据的title值,第二个为body值 -- {{passing-properties-to-component item.title item.body}}{{each}}
准备需要显示的数据。
approutespadding-properties-to-component.jsimport Ember from 'ember';export default Ember.Route.extend({ model function() { return [ { id 1, title 'Bower dependencies and resolutions new', body In the bower.json file, I see 2 keys dependencies and resolutionsWhy is that so }, { id 2, title 'Highly Nested JSON Payload - hasMany error', body Welcome to the Ember.js discussion forum. We're running on the open source, Ember.js-powered Discourse forum software. }, { id 3, title 'Passing a jwt to my REST adapter new ', body This sets up a binding between the category query param in the URL, and the category property on controllerarticles. } ]; }});
在组件类中指定位置参数的名称。
appcomponentspadding-properties-to-component.jsimport Ember from 'ember';export default Ember.Component.extend({ 指定位置参数的名称 positionalParams ['title', 'body']});
注意:属性positionalParams指定的参数不能在运行期改变。
组件直接使用组件类中指定的位置参数名称获取数据。
!-- apptemplatescomponentspassing-properties-to-component.hbs --article h1{{title}}h1 p{{body}}particle
注意:获取数据的名称必须要与组件类指定的名称一致,否则无法正确获取数据。显示结果如下:
Ember还允许你指定任意多个参数,但是组件类获取参数的方式就需要做点小修改。比如下面的例子:
调用组件的模板
!-- apptemplatespassing-properties-to-component.hbs --{{#each model as item}} !-- 传递到组件blog-post第一个参数为数据的title值,第二个为body值 -- {{passing-properties-to-component item.title item.body 'third value' 'fourth value'}}{{each}}
指定参数名称的组件类,获取参数的方式可以Ember.js 入门指南之三计算属性这章。
appcomponentspadding-properties-to-component.jsimport Ember from 'ember';export default Ember.Component.extend({ 指定位置参数为参数数组 positionalParams 'params', title Ember.computed('params.[]', function() { return this.get('params')[0]; 获取第一个参数 }), body Ember.computed('params.[]', function() { return this.get('params')[1]; 获取第二个参数 }), third Ember.computed('params.[]', function() { return this.get('params')[2]; 获取第三个参数 }), fourth Ember.computed('params.[]', function() { return this.get('params')[3]; 获取第四个参数 })});
下面看组件是怎么获取传递过来的参数的。
!-- apptemplatescomponentspassing-properties-to-component.hbs --article h1{{title}}h1 p{{body}}p pthird {{third}}p pfourth {{fourth}}particle
显示结果如下:
到此组件参数传递的内容全部介绍完毕。总的来说没啥难度。Ember中参数的传递与获取方式基本是相似的,比如link-to助手、action助手。
br博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
准备工作:
ember g route wrapping-content-in-component-routeember g component wrapping-content-in-component
有些情况下,你需要定义一个包裹其他模板提供的数据的组件。比如下面的例子:
<h1>{{title}}</h1><div class='body'>{{body}}</div>
上述代码定义了一个普通的组件。
{{wrapping-content-in-component title=model.title body=model.body}}
调用组件,传入两个指定名称的参数,更多有关组件参数传递问题请看Ember.js 入门指南之二十九属性传递。
下面在route
中增加一些测试数据。
// app/routes/wrapping-content-in-component-route.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return { id: 1, title: 'test title', body: 'this is body ...', author: 'ubuntuvim' }; }});
如果程序代码没写错,界面应该会显示如下信息。
在上述例子中组件正常显示出model
回调中初始化的数据。但是如果你定义的组件需要包含自定义的HTML内容呢??
除了上述这种简单的数据传递之外,Ember还支持使用block form
(块方式),换句话说你可以直接传递一个模板到组件中,并在组件中使用{{yield}}
助手显示传入进来的模板。
为了能使用块方式传递模板到组件中,在调用组件的时候必须使用#
开始的方式(两种调用方式:{{component-name}}
或者{{#component-name}}……{{/component-name}}
),注意一定要有关闭标签!
稍加改造前面的例子,这时候不只是传递一个简单的数据,而是传入一个包含HTML标签的简单模板。
<h1>{{title}}</h1><div class='body'>{{yield}}</div>
注意此时div
标签内使用的是{{yield}}
助手,而不是直接使用{{body}}
。
下面是调用组件的模板。
{{!wrapping-content-in-component title=model.title body=model.body}}{{#wrapping-content-in-component title=model.title}} {{model.body}} <small>by {{model.author}}</small>{{/wrapping-content-in-component}}
页面加载之后效果如下:
查看页面HTML源代码,可以看到在
按照惯例,先做好准备工作,使用Ember CLI命令生成演示所需的文件:
ember g route customizing-component-elementember g component customizing-component-elementember g route homeember g route about
默认情况下,组件会被包裹在div
标签内。比如,组件渲染之后得到下面的代码:
<div id="ember180" class="ember-view"> <h1>My Component</h1></div>
h1
标签就是组件的内容。以ember
开头的id
和class
都是Ember自动生成的。如果你需要修改渲染之后生成的HTML不是被包裹在div
标签,或者修改id
和class
等属性值为自定义的值,你可以在组件类中设置。
默认情况下,组件会被包裹在div
标签内,如果你需要修改这个默认值你可以在组件类中指定这个包裹的HTML标签。
// app/components/customizing-component-element.jsimport Ember from 'ember';export default Ember.Component.extend({ // 使用tabName属性指定渲染之后HTML标签 // 注意属性的值必须是标准的HTML标签名 tagName: 'nav'});
下面自定义一个组件。
<ul> <li>{{#link-to 'home'}}Home{{/link-to}}</li> <li>{{#link-to 'about'}}About{{/link-to}}</li></ul>
下面是调用组件的模板代码。注意组件被包裹在那个HTML标签内,正确情况下应该是被包裹在nav
标签中。
{{customizing-component-element}}
页面加载之后查看页面的源代码。如下:
可以看到组件customizing-component-element
的内容确实是被包裹在nav
标签之中,如果在组件类中没有使用属性tagName
指定包裹的HTML标签,默认是div
,你可以把组件类中tagName
属性删除之后再查看页面的HTML源码代码。
默认情况下,Ember会自动为包裹组件的HTML元素增加一个以ember
开头的类名,如果你需要增加自定义的CSS类,可以在组件类中使用className
数组属性指定,可以一次性指定多个CSS类。比如下面的代码例子:
// app/components/customizing-component-element.jsimport Ember from 'ember';export default Ember.Component.extend({ // 使用tabName属性指定渲染之后HTML标签 // 注意属性的值必须是标准的HTML标签名 tagName: 'nav', classNames: ['primary', 'my-class-name'] //指定包裹元素的CSS类});
页面重新加载之后查看源代码,可以看到nav
标签中多了两个CSS类,一个是primary
,一个是my-class-name
。
……
如果你想根据某个数据的值决定是否增加CSS类也是可以做到的,比如下面的代码,当urgent
为true
的时增加一个CSS类urgent
,否则不增加这个类。要达到这个目的可以通过属性classNameBindings
设置。
// app/components/customizing-component-element.jsimport Ember from 'ember';export default Ember.Component.extend({ // 使用tabName属性指定渲染之后HTML标签 // 注意属性的值必须是标准的HTML标签名 tagName: 'nav', classNames: ['primary', 'my-class-name'], //指定包裹元素的CSS类 classNameBindings: ['urgent'], urgent: true});
页面重新加载之后查看源代码,可以看到nav
标签中多了一个CSS类urgent
,如果属性urgent
的值为false
,CSS类urgent
将不会渲染到nav
标签上。
……
注意:classNameBindings
指定的属性值必须要跟用于判断数据的属性名一致,比如这个例子中classNameBindings
指定的属性值是urgent
,用户判断是否增加类的属性也是urgent
。如果这个属性只是驼峰式命名的那么渲染之后CSS类名将是以中划线-
分隔,比如classNameBindings
指定一个名为secondClassName
,渲染后的CSS类为second-class-name
。比如下面的演示代码:
// app/components/customizing-component-element.jsimport Ember from 'ember';export default Ember.Component.extend({ // 使用tabName属性指定渲染之后HTML标签 // 注意属性的值必须是标准的HTML标签名 tagName: 'nav', classNames: ['primary', 'my-class-name'], //指定包裹元素的CSS类 classNameBindings: ['urgent', 'secondClassName'], urgent: true, secondClassName: true});
页面重新加载之后查看源代码,可以看到nav
标签中多了一个CSS类second-class-name
。
……
如果你不想渲染之后的CSS类名被修改为中划线分隔形式,你可以值classNameBindings
属性中指定渲染之后的CSS类名。比如下面的代码:
// app/components/customizing-component-element.jsimport Ember from 'ember';export default Ember.Component.extend({ // 使用tabName属性指定渲染之后HTML标签 // 注意属性的值必须是标准的HTML标签名 tagName: 'nav', classNames: ['primary', 'my-class-name'], //指定包裹元素的CSS类 classNameBindings: ['urgent', 'secondClassName:scn'], //指定secondClassName渲染之后的CSS类名为scn urgent: true, secondClassName: true});
页面重新加载之后查看源代码,可以看到nav
标签中原来CSS类为second-class-name
的变成了scn
。
……
有没有感觉Ember既灵活又强大!!Ember的设计理念是“约定优于配置”!所以很多的属性默认的设置都是我们平常开发中最常用的格式。
除了上述可以指定CSS类名之外,还可以在classNameBindings
增加简单的逻辑,特别是在处理一些动态效果的时候上述特性是非常有用的。
// app/components/customizing-component-element.jsimport Ember from 'ember';export default Ember.Component.extend({ // 使用tabName属性指定渲染之后HTML标签 // 注意属性的值必须是标准的HTML标签名 tagName: 'nav', classNames: ['primary', 'my-class-name'], //指定包裹元素的CSS类 classNameBindings: ['urgent', 'secondClassName:scn', 'isEnabled:enabled:disabled'], urgent: true, secondClassName: true, isEnabled: true //如果这个属性为true,类enabled将被渲染到nav标签上,如果属性值为false类disabled将被渲染到nav标签上,类似于三目运算});
正如代码的注释所说的,isEnabled:enabled:disabled
可以理解为一个三目运算,会根据isEnabled
的值渲染不同的CSS类到nav
上。
下面的HTML代码是isEnabled
为true
的情况,对于isEnabled
为false
的情况请读者自己试试:
……
注意:如果用于判断的属性值不是一个Boolean
值而是一个字符串那么得到的结果与上面的结果是不一样的,Ember会直接把这个字符串的值作为CSS类名渲染到包裹的标签上。比如下面的代码:
// app/components/customizing-component-element.jsimport Ember from 'ember';export default Ember.Component.extend({ // 使用tabName属性指定渲染之后HTML标签 // 注意属性的值必须是标准的HTML标签名 tagName: 'nav', classNames: ['primary', 'my-class-name'], //指定包裹元素的CSS类 classNameBindings: ['urgent', 'secondClassName:scn', 'isEnabled:enabled:disabled', 'stringValue'], urgent: true, secondClassName: true, isEnabled: true, //如果这个属性为true,类enabled将被渲染到nav标签上,如果属性值为false类disabled将被渲染到nav标签上,类似于三目运算 stringValue: 'renderedClassName'});
此时页面的HTML源码就有点不一样了。renderedClassName
作为CSS类名被渲染到了nav
标签上。
……
对于这点需要特别注意。Ember对于Boolean
值和其他值的判断结果是不一样的。
在前面两点介绍了包裹组件的HTML元素的标签名、CSS类名,在HTML标签上出来CSS类另外一个最常用的就是属性,那么Ember同样提供了自定义包裹HTML元素的属性的方法。使用attributeBindings
属性指定,这个属性的属性方式与classNameBindings
基本一致。为了与前面的例子区别开新建一个组件link-items
,使用命令ember g component link-items
创建。
这是个组件
在模板中调用组件。
{{customizing-component-element}}<br><br>{{link-items}}
下面设置组件类,指定包裹的HTML标签为a
标签,并增加一个属性href
。
// app/components/link-items.jsimport Ember from 'ember';export default Ember.Component.extend({ tagName: 'a', attributeBindings: ['href'], href: 'http://www.google.com.hk'});
页面重新加载之后得到如下结果:
比较简单,对于渲染之后的结果我就不过多解释了,请参考classNameBindings
属性理解。
到此,有关于组件渲染之后包裹组件的HTML标签的相关设置介绍完毕。内容不多,classNameBindings
和attributeBindings
这两个属性的使用方式基本相同。如有疑问欢迎给我留言或者直接查看官方教程。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
你可以在组件中响应事件,比如用户的双击、鼠标滑过、键盘的按下等等事件。只需要在组件类中增加Ember提供的处理事件,然后Ember会自动判断用户的操作执行相应的事件,只要在组件类中添加的事件不冲突你甚至一次性增加多个事件,事件执行次序根据触发的次序执行。
1,简单事件处理准备工作,使用Ember CLI创建演示所需文件:
ember g component handle-eventsember g route component-route
生成的组件模板不做任何修改。
{{yield}}
注意看组件类的实现:
// app/components/handle-events.jsimport Ember from 'ember';export default Ember.Component.extend({ click: function() { alert('click...'); return true; // 返回true允许事件冒泡到父组件 }, mouseLeave: function() { alert("mouseDown...."); return true; }});
在组件类中增加了两个事件click
和mouseLeaver
,一个是单击事件一个是鼠标移开事件,更多Ember支持的事件请看handling-events。
调用组件的模板如下:
{{#handle-events}}<span style="cursor: pointer;">从我身上飘过或者点我都会触发事件~</span>{{/handle-events}}
当用户只是把鼠标从文字“从我身上飘过或者点我都会触发事件~”上划过市只执行mouseLeave
事件,当用户点击文字时先执行click
事件再执行mouseLeave
事件,因为用户点击文字时鼠标还没移开。
但是如果你增加的事件是有冲突的可能会得到无法预知的结果,比如在组件类中增加了双击和单击事件,此时只会执行单击事件,双击事件就无法触发。
某些情况下,你的组件需要支持拖放事件。比如组件可能要发送一个id
到drop
事件中。
{{drop-target action=”didDrop”}}
你可以定义组件的事件处理器去管理drop
事件。如果有需要可以通过返回false
防止事件冒泡。
// app/components/drop-target.jsimport Ember from 'ember';export default Ember.Component.extend({ attribuBindings: ['draggable'], draggable: 'true', dragOver: function() { return false; }, didDrop: function(event) { let id = event.dataTransfer.getData('text/data'); this.sendAction('action', id); }});
本章内容不多,重点是第一点的内容,第二点的内容就简单介绍,更多详细信息请移步官网文档。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
组件就像一个相对独立的盒子。在前面的文章中介绍过组件是怎么通过属性传递参数,并且这个属性值你可以在模板或者js代码中获取。
但是到目前为止还没介绍过子组件从父组件中获取数组,在Ember应用中组件之间的通信是通过actions
实现的。
跟着下面的步骤来,创建一个组件之间通信的示例。
创建组件的方法不用我多说,直接使用Ember CLI命令创建即可。
ember g component button-with-confirmationember g component user-profileember g route button-with-confirmation-route
为了测试方便多增加了一个路由。
下面是组件user-profile
的定义,调用组件button-with-confirmation
,那么此时user-profile
作为父组件,button-with-confirmation
作为子组件:
{{button-with-confirmation text="Click OK to delete your account"}}
要想action
能执行需要作如下两步:
下面是实现代码:
实现父组件动作(action)
在父组件中,定义好当用户点击“确认”之后触发的动作。在这个例子中的动作(action
)是先找出用户账号再删除。
在Ember应用中,每个组件都有一个名为actions
的属性。这个属性值是函数,是可以被用户或者子组件执行的函数。
// app/components/user-profile.jsimport Ember from 'ember';export default Ember.Component.extend({ actions: { userDidDeleteAccount: function() { console.log(“userDidDeleteAccount…”); } }});
现在已经实现了父组件逻辑,但是并没有告诉Ember这个动作什么时候触发,下一步将实现这个功能。
实现子组件动作(action)
这一步我们将实现当用户点击“确定”之后触发事件的逻辑。
// app/components/button-with-confirmation.jsimport Ember from 'ember';export default Ember.Component.extend({ tagName: 'button', click: function() { if (confirm(this.get('text'))) { // 在这里获取父组件的事件(数据)并触发 } }});
现在我们在user-profile
组件中使用onConfirm()
方法触发组件button-with-confirmation
类中的userDidDeleteAccount
事件。
{{#button-with-confirmation text="Click OK to delete your account" onConfirm=(action 'userDidDeleteAccount')}}执行userDidDeleteAccount方法{{/button-with-confirmation}}
这段代码的意思是告诉父组件,userDidDeleteAccount
方法会通过onConfirm
方法执行。
现在你可以在子组件中使用onConfirm
方法执行父组件的动作。
// app/components/button-with-confirmation.jsimport Ember from 'ember';export default Ember.Component.extend({ tagName: 'button', click: function() { if (confirm(this.get('text'))) { // 在父组件中触发动作 this.get('onConfirm')(); } }});
this.gete(“onConfirm”)
从父组件返回一个值onConfirm
,然后与()
组合成了一个个方法onConfirm()
。
在模板button-with-confirmation-route.hbs
中调用组件。
{{user-profile}}
点击这个button
,会触发事件。弹出对话框。再点击“确认”后执行方法userDidDeleteAccount
,可以看到浏览器控制台输出了userDidDeleteAccount…,未点击“确认”前或者点击“取消”不会输出这个信息,说明不执行这个方法userDidDeleteAccount
。
像普通属性,actions
可以组件的一个属性,唯一的区别是,属性设置为一个函数,它知道如何触发的行为。
在组件的actions
属性中定义的方法,允许你决定怎么去处理一个事件,有助于模块化,提高组件重用率。
到此,组件这一章节的内容全部介绍完毕了,不知道你看懂了多少?如果有疑问请给我留言一起交流学习,获取是直接去官网学习官方教程。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
对于组件这一章是非常重要的,组件会在3.0
之后的版本替代控制器。
这一章最重要的内容包括如下几篇博文:
要想学好组件必须多看上述几篇文章。特别是第四篇,介绍了组件的生命周期,对于理解组件的原理是非常有帮助的。
组件到此也介绍完毕了,其中很重要的两个知识点没有放到博客中,请自行从官方参考文档学习。在接下来的一章将为大家介绍控制器(controller
),虽然控制器会在3.0
版本中被移除,但是目前仍然是支持的,所以还需要简单讲解。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
从本篇开始进入第五章控制器,controller
在Ember2.0
开始越来越精简了,职责也更加单一——处理逻辑。
下面是准备工作。重新创建一个Ember项目,仍旧使用的是Ember CLI命令创建。
ember new chapter5_controllerscd chapter5_controllersember server
在浏览器执行项目,看到如下信息说明项目搭建成功。Welcome to Ember。
控制器与组件非常相似,由此,在未来的新版本中很有可能组件将会完全取代控制器,很可能随着Ember版本的更新控制器将退出Ember。目前的版本中组件还不能直接通过路由访问,需要通过模板调用才能使用组件,但是未来的版本会解决这个问题,到时候controller
可能就真的从Ember退出了!
正因如此,模块化的Ember应用很少用到controller
。即便是使用了controller
也是为了处理下面的两件事情:
controller
主要是为了维持当前路由状态。一般来说,model的属性会保存到服务器,但是controller
的属性却不会保存到服务器。controller
层转到route
层。模板上下文的渲染是通过当前controller
的路由处理的。Ember所追随的理念是“约定优于配置”,这也就意味着如果你只需要一个controller
你就创建一个,而不是一切为了“便于工作”。
下面的例子是演示路由显示blog post
。假设模板blog-post
用于展示模型blog-post
的数据,并在这个模型包含如下属性(隐含属性id
,因为在model
中不需要手动指定id
属性):
model
定义如下:
// app/models/blog-post.jsimport DS from 'ember-data';export default DS.Model.extend({ title: DS.attr('string'), // 属性默认为string类型,可以不指定 intro: DS.attr('string'), body: DS.attr('string'), author: DS.attr('string')});
在route
层增加测试数据,直接返回一个model
对象。
// app/routes/blog-post.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { var blogPost = this.store.createRecord('blog-post', { title: 'DEFINING A COMPONENT', // 属性默认为string类型,可以不指定 intro: "Components must have at least one dash in their name. ", body: "Components must have at least one dash in their name. So blog-post is an acceptable name, and so is audio-player-controls, but post is not. This prevents clashes with current or future HTML element names, aligns Ember components with the W3C Custom Elements spec, and ensures Ember detects the components automatically.", author: 'ubuntuvim' }); // 直接返回一个model,或者你可以返回promises, return blogPost; }});
显示信息的模板如下:
<h1>{{model.title}}</h1><h2>{{model.author}}</h2><div class="intro"> {{model.intro}}</div><hr><div class="body"> {{model.body}}</div>
如果你的代码没有编写错误那么也会得到如下结果:
Welcome to Ember是主模板的信息,你可以在application.hbs
中删除,但是记得不要删除{{outlet}}
,否则什么信息也不显示。
这个例子中没有显示任何特定的属性或者指定的动作(action
)。此时,控制器的model属性所扮演的角色仅仅是模型属性的pass-through
(或代理)。注意:控制器获取的model
是从route
得到的。
下面为这个例子增加一个功能:用户可以点击标题触发显示或者隐藏post
的内容。通过一个属性isExpanded
控制,下面分别修改模板和控制器的代码。
// app/controllers/blog-post.jsimport Ember from 'ember';export default Ember.Controller.extend({ isExpanded: false, //默认不显示body actions: { toggleBody: function() { this.toggleProperty('isExpanded'); } }});
在controller
中增加一个属性isExpanded
,如果你不在controller
中定义这个属性也是可以的。对于这个controller
代码的解释请看Ember.js 入门指南之十五{{action}} 助手。
<h1>{{model.title}}</h1><h2>{{model.author}}</h2><div class="intro"> {{model.intro}}</div><hr>{{#if isExpanded}} <button {{action 'toggleBody'}}>hide body</button> <div class="body"> {{model.body}} </div>{{else}} <button {{action 'toggleBody'}}>Show body</button>{{/if}}
在模板中使用if
助手判断isExpanded
的值,如果为true
则显示body
,否则不显示。
页面加载之后结果如下,首先是不显示body
内容,点击按钮“Show body”则显示内容,并且按钮变为“hide body”。然后在点击这个按钮则不显示body
内容。
到此controller
的职责你应该大致了解了,其主要的作用是逻辑的判断、处理,比如这里例子中判断body
内容的显示与否,其实你也可以把controller
类中的处理代码放在route
类中也可以实现这个效果,但是要作为model
的属性返回(把isExpanded
当做model
的属性处理),请读者自己动手试试,但是把逻辑放到route
又会使得route
变得“不专一”了,route
的主要职责是初始化数据的。我想这也是Ember还留着controller
的原因之一吧!!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
在有路由嵌套的情况下,你可能需要在两个不同的controller
之间通信。按照惯例先做准备工作:
ember g route postember g route post/commentsember g model post
比如下面的路由设置:
// router.jsimport Ember from 'ember';import config from './config/environment';var Router = Ember.Router.extend({ location: config.locationType});Router.map(function() { this.route('blog-post'); this.route('post', { path: '/posts/:post_id' }, function() { this.route('comments'); });});export default Router;
对于这个路由配置生成的路由表请看Ember.js 入门指南之十三{{link-to}} 助手。
如果用户访问/posts/1/comments
。模型post
就会加载到postController
,并不会直接加载到commentsController
。然后如果你想在一篇post
中显示comment
信息呢?
为了实现这个功能,可以把postController
注入到commentController
中。
// app/controllers/comments.jsimport Ember from 'ember';export default Ember.Controller.extend({ postController: Ember.inject.controller('post')});
一旦comments
路由被访问,postController
就会获取控制器对应的model
,并且这个model
是只读的。为了能获取到模型post
还需要增加一个引用postController.model
。
// app/controllers/comments.jsimport Ember from 'ember';export default Ember.Controller.extend({ postController: Ember.inject.controller('post'), post: Ember.computed.reads('postController.model')});
最后可以直接在comment
模板中显示模型post
和comment
的信息。
<h1>Comments for {{post.title}}</h1><ul> {{#each model as |comment|}} <li>{{comment.text}}</li> {{/each}}</ul>
有关更多别名的介绍请移步这里查看API文档的介绍。如果你想了解更多关于注入的问题请看这里的教程(新版官网已经没有这个地址的文档了)。
controller
这章的内容到此也全部介绍完毕了,只有寥寥的2篇教程,可见controller
在Ember未来版本会被组件替代已成必然。
那么下一章将为大伙介绍模型,模型对于Ember来说是一块非常重要的内容,内容也比较多!我回用9篇文章来给你介绍模型,从定义到其使用等等内容。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
Ember官网用了大篇幅来介绍model
,相比之前的controller
简直就是天壤之别啊!
从本篇开始学习Ember的模型,这一章也是Ember基础部分的最后一章内容,非常的重要(不管你信不信反正我是信了)。
在开始学习model
之前先做好准备工作:重新创建一个Ember项目,仍旧使用的是Ember CLI命令创建。
ember new chapter6_modelscd chapter6_modelsember server
在浏览器执行项目,看到如下信息说明项目搭建成功。Welcome to Ember
本章演示所用到的代码都可以从https://github.com/ubuntuvim/my_emberjs_code/tree/master/chapter6_models获取。
在介绍model
之前先在项目中引入firebase。相关的配置教材请移步这里(如果无法加载页面请先在https://www.firebase.com/注册用户)。firebase的官网提供了专门用于Ember的版本,还提供了非常简单的例子。从安装到整合都给出了非常详细代码教程。下面是我的整合步骤(命令都是在项目目录下执行的):
ember install emberfire
安装完成之后会自动创建adapter(app/adapters/application.js)
,对于这个文件不需要做任何修改,官网提供的代码也许跟你的项目的代码不同,应该是官网的版本是旧版的。
config/environment.js
修改第八行firebase: 'https://YOUR-FIREBASE-NAME.firebaseio.com/'
。这个地址是你注册用户时候得到的。你可以从这里查看你的地址。比如下图所示位置config/enviroment.js
的APP:{}
(大概第20行)后面新增如下代码APP: { // Here you can pass flags/options to your application instance // when it is created},contentSecurityPolicy: { 'default-src': "'none'", 'script-src': "'self' 'unsafe-inline' 'unsafe-eval' *", 'font-src': "'self' *", 'connect-src': "'self' *", 'img-src': "'self' *", 'style-src': "'self' 'unsafe-inline' *", 'frame-src': "*"}
然后再注释掉第7行原有属性(安装firebase
自动生成的,但是配置不够完整):contentSecurityPolicy
。
或者你可以参考我的配置文件:
/* jshint node: true */module.exports = function(environment) { var ENV = { modulePrefix: 'chapter6-models', environment: environment, // contentSecurityPolicy: { 'connect-src': "'self' https://auth.firebase.com wss://*.firebaseio.com" }, firebase: '你的firebase连接', baseURL: '/', locationType: 'auto', EmberENV: { FEATURES: { // Here you can enable experimental features on an ember canary build // e.g. 'with-controller': true } }, APP: { // Here you can pass flags/options to your application instance // when it is created }, contentSecurityPolicy: { 'default-src': "'none'", 'script-src': "'self' 'unsafe-inline' 'unsafe-eval' *", 'font-src': "'self' *", 'connect-src': "'self' *", 'img-src': "'self' *", 'style-src': "'self' 'unsafe-inline' *", 'frame-src': "*" } }; // 其他代码省略…… return ENV;};
如果不做这个配置启动项目之后浏览器会提示一堆的错误。主要是一些访问权限问题。配置完之后需要重启项目才能生效!
model
是一个用于向用户呈现底层数据的对象。不同的应用有不同的model
,这取决于解决的问题需要什么样的model
就定义什么样的model
。model
通常是持久化的。这也就意味着用户关闭了浏览器窗口model
数据不应该丢失。为了确保model
数据不丢失,你需要存储model
数据到你所指定的服务器或者是本地数据文件中。
一种非常常见的情况是,model
数据会以JSON
的格式通过HTTP
发送到服务器并保存在服务中。Ember还未开发者提供了一种更加简便的方式:使用IndexedDB(使用在浏览器中的数据库)。这种方式是把model
数据保存到本地。或者使用Ember Data,又或者使用firebase,把数据直接保存到远程服务器上,后续的文章我将引入firebase,把数据保存到远程服务器上。
Ember使用适配器模式连接数据库,可以适配不同类型的后端数据库而不需要修改任何的网络代码。你可以从emberobserver上看到几乎所有Ember支持的数据库。
如果你想把你的Ember应用与你的远程服务器整合,几遍远程服务器API
返回的数据不是规范的JSON
数据也不要紧,Ember Data可以配置任何服务器返回的数据。
Ember Data还支持流媒体服务器,比如WebSocket。你可以打开一个socket连接远程服务器,获取最新的数据或者把变化的数据推送到远程服务器保存。
Ember Data为你提供了更加简便的方式操作数据,统一管理数据的加载,降低程序复杂度。
对于model
与Ember Data的介绍就到此为止吧,官网用了大量篇幅介绍Model,在此我就不一一写出来了!太长了,写出来也没人看的!!!如果有兴趣自己看吧!点击查看详细信息。
下面先看一个简单的例子,由这个例子延伸出有关于model
的核心概念。这些代码是旧版写法,仅仅是为了说明问题,本文也不会真正执行。
// app/components/list-of-drafts.jsexport default Ember.Component.extend({ willRender() { // ECMAScript 6语法 $.getJSON('/drafts').then(data => { this.set('drafts', data); }); }});
定义了一个组件类。并在组件类中获取json
格式数据。下面是组件对应的模板文件。
<ul> {{#each drafts key="id" as |draft|}} <li>{{draft.title}}</li> {{/each}}</ul>
再定义另外一个组件类和模板
// app/components/list-button.jsexport default Ember.Component.extend({ willRender() { // ECMAScript 6语法 $.getJSON('/drafts').then(data => { this.set('drafts', data); }); }});
{{#link-to ‘drafts’ tagName=’button’}}Drafts ({{drafts.length}}){{/link-to}}
组件list-of-drafts
类和组件list-button
类是一样的,但是他们的对应的模板却不一样。但是都是从远程服务器获取同样的数据。如果没有Store
(model
核心内容之一)那么每次这两个模板渲染都会是组件类调用一次远程数据。并且返回的数据是一样的。这无形中增加了不必要的请求,暂用了不必要的宽带,用户体验也不好。但是有了Store
就不一样了,你可以把Store
理解为仓库,每次执行组件类时先到Store
中获取数据,如果没有再去远程获取。当在其中一个组件中改变某些数据,数据的更改也能理解反应到另一个获取此数据的组件上(与计算属性自动更新一样),而这个组件不需要再去服务请求才能获取最新更改过的数据。
下面的内容将为你一一介绍Ember Data最核心的几个东西:models
、records
、adapters
、store
。
声明:下面简介内摘抄至http://www.emberjs.cn/guides/models/#toc_。
store
是应用存放记录的中心仓库。你可以认为store
是应用的所有数据的缓存。应用的控制器和路由都可以访问这个共享的store
;当它们需要显示或者修改一个记录时,首先就需要访问store
。
DS.Store
的实例会被自动创建,并且该实例被应用中所有的对象所共享。
store
可以看做是一个缓存。在下面的cache
会结合store
介绍。
下面的例子结合firebase演示:创建路由和model
:
ember g route store-exampleember g model article
// app/models/article.jsimport DS from 'ember-data';export default DS.Model.extend({ title: DS.attr('string'), body: DS.attr('string'), timestamp: DS.attr('number'), category: DS.attr('string')});
这个就是model
,本章要讲的内容就是它!为何没有定义id属性呢?Ember
会默认生成id
属性。
我们在路由的model
回调中获取远程的数据,并显示在模板上。
// app/routes/store-example.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 从store中获取id为JzySrmbivaSSFG6WwOk的数据,这个数据是我在我的firebase中初始化好的 return this.store.find('article', '-JzySrmbivaSSFG6WwOk'); }});
find
方法的第一个参数是model
类名,第二个参数对象的id
属性值。记得id属性不需要在model
类中手动定义,Ember会自动为你定义。
<h1>{{model.title}}</h1><div class="body">{{model.body}}</div>
页面加载之后可以看到获取到的数据。
下面是我的firebase上的部分数据截图。
可以看到成功获取到id
为-JzySrmbivaSSFG6WwOk
的数据。更多关于数据的操作在后面会详细介绍。
有关model
的概念前面的简介已经介绍了,这里不再赘述。model
定义:
model
是由若干个属性构成的。attr
方法的参数指定属性的类型。
export default DS.Model.extend({ title: DS.attr('string'), // 字符串类型 flag: DS.attr('boolean'), // 布尔类型 timestamp: DS.attr('number'), // 数字类型 birth: DS.attr(‘date’) //日期类型});
模型也声明了它与其他对象的关系。例如,一个Order
可以有许多LineItems
,一个LineItem
可以属于一个特定的Order
。
App.Order = DS.Model.extend({ lineItems: DS.hasMany('lineItem')});App.LineItem = DS.Model.extend({ order: DS.belongsTo('order')});
这个与数据的表之间的关系是一样的。
record
是model
的实例,包含了从服务器端加载而来的数据。应用本身也可以创建新的记录,以及将新记录保存到服务器端。
记录由以下两个属性来唯一标识:
比如前面的实例article
就是通过find
方获取。获取到的结果就是一个record
。
适配器是一个了解特定的服务器后端的对象,主要负责将对记录(record
)的请求和变更转换为正确的向服务器端的请求调用。
例如,如果应用需要一个ID
为1
的person
记录,那么Ember Data是如何加载这个对象的呢?是通过HTTP,还是Websocket?如果是通过HTTP,那么URL会是/person/1
,还是/resources/people/1
呢?
适配器负责处理所有类似的问题。无论何时,当应用需要从store
中获取一个没有被缓存的记录时,应用就会访问适配器来获取这个记录。如果改变了一个记录并准备保存改变时,store
会将记录传递给适配器,然后由适配器负责将数据发送给服务器端,并确认保存是否成功。
store
会自动缓存记录。如果一个记录已经被加载了,那么再次访问它的时候,会返回同一个对象实例。这样大大减少了与服务器端的往返通信,使得应用可以更快的为用户渲染所需的UI。
例如,应用第一次从store
中获取一个ID
为1
的person
记录时,将会从服务器端获取对象的数据。
但是,当应用再次需要ID
为1
的person
记录时,store
会发现这个记录已经获取到了,并且缓存了该记录。那么store
就不会再向服务器端发送请求去获取记录的数据,而是直接返回第一次时候获取到并构造出来的记录。这个特性使得不论请求这个记录多少次,都会返回同一个记录对象,这也被称为Identity Map
(标识符映射)。
使用标识符映射非常重要,因为这样确保了在一个UI上对一个记录的修改会自动传播到UI其他使用到该记录的UI。同时这意味着你无须手动去保持对象的同步,只需要使用ID
来获取应用已经获取到的记录就可以了。
应用第一次从store
获取一个记录时,store
会发现本地缓存并不存在一份被请求的记录的副本,这时会向适配器发请求。适配器将从持久层去获取记录;通常情况下,持久层都是一个HTTP服务,通过该服务可以获取到记录的一个JSON
表示。
如上图所示,适配器有时不能立即返回请求的记录。这时适配器必须向服务器发起一个异步的请求,当请求完成加载后,才能通过返回的数据创建的记录。
由于存在这样的异步性,store
会从find()
方法立即返回一个承诺(promise
)。另外,所有请求需要store
与适配器发生交互的话,都会返回承诺。一旦发给服务器端的请求返回被请求记录的JSON数据时,适配器会履行承诺,并将JSON
传递给store
。store
这时就获取到了JSON
,并使用JSON
数据完成记录的初始化,并使用新加载的记录来履行已经返回到应用的承诺。
下面将介绍一下当store
已经缓存了请求的记录时会发生什么。
在这种情形下,store
已经缓存了请求的记录,不过它也将返回一个承诺,不同的是,这个承诺将会立即使用缓存的记录来履行。此时,由于store
已经有了一份拷贝,所以不需要向适配器去请求(没有与服务器发生交互)。
models
、records
、adapters
、store
是你必须要理解的概念。这是Ember Data最核心的东西。
有关于上述的概念将会在后面的文章一一用代码演示。理解了本文model
这一整章的内容都不是问题了!!!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
模型也是一个类,它定义了向用户展示的属性和数据行为。模型的定义非常简单,只需要继承DS.Model类即可,或者你也可以直接使用Ember CLI命令创建。比如使用命令模型 ember g model person
定义了一个模型类person
。
// app/models/person.jsimport DS from 'ember-data';export default DS.Model.extend({ });
这个是个空的模型,没有定义任何属性。有了模型类你就可以使用find
方法查找数据了。
上面定义的模型类person
还没有任何属性,下面为这个类添加几个属性。
// app/models/person.jsimport DS from 'ember-data';export default DS.Model.extend({ firstName: DS.attr(), lastName: DS.attr(), birthday: DS.attr() });
上述代码定义了3个属性,但是还未给属性指定类型,默认都是string
类型。这些属性名与你连接的服务器上的数据key
是一致的。甚至你还可以在模型中定义计算属性。
// app/models/person.jsimport DS from 'ember-data';export default DS.Model.extend({ firstName: DS.attr(), lastName: DS.attr(), birthday: DS.attr(), fullName: Ember.computed('firstName', 'lastName', function() { return `${this.get('firstName')} ${this.get('lastName')}`; })});
这段代码在模型类中定义了一个计算属性fullName
。
前面定义的模型类是没有指定属性类型的,默认情况下都是string
类型,显然这是不够的,简单的模型属性类型包括:string
,number
,boolean
,date
。这几个类型我想不用我解释都应该知道了。
不仅可以指定属性类型,你还可以指定属性的默认值,在attr()方法的第二个参数指定。比如下面的代码:
// app/models/person.jsimport DS from 'ember-data';export default DS.Model.extend({ username: DS.attr('string'), email: DS.attr('string'), verified: DS.attr('boolean', { defaultValue: false }), //指定默认值是false // 使用函数返回值作为默认值 createAt: DS.attr('string', { defaultValue(){ return new Date(); } })});
正如代码注释所述的,设置默认值的方式包括直接指定或者是使用函数返回值指定。
Ember的模型也是有类似于数据库的关联关系的。只是相对于复制的数据库Ember的模型就显得简单很多,其中包括一对一,一对多,多对多关联关系。这种关系是与后台的数据库是相统一的。
声明一对一关联使用DS.belongsTo设置。比如下面的两个模型。
// app/models/user.jsimport DS from 'ember-data';export default DS.Model.extend({ profile: DS.belongsTo(‘profile’);});
// app/models/profile.jsimport DS from ‘ember-data’;export default DS.Model.extend({ user: DS.belongsTo(‘user’);});
声明一对多关联使用DS.belongsTo(多的一方使用)和DS.hasMany(少的一方使用)设置。比如下面的两个模型。
// app/models/post.jsimport DS from ‘ember-data’;export default DS.Model.extend({ comments: DS.hasMany(‘comment’);});
这个模型是一的一方。下面的模型是多的一方;
// app/models/comment.jsimport DS from ‘ember-data’;export default DS.Model.extend({ post: DS.belongsTo(‘post’);});
这种设置的方式与Java 的hibernate非常相似。
声明一对多关联使用DS.hasMany设置。比如下面的两个模型。
// app/models/post.jsimport DS from ‘ember-data’;export default DS.Model.extend({ tags: DS.hasMany(‘tag’);});
// app/model/tag.jsimport DS from ‘ember-data’;export default DS.Model.extend({ post: DS.hasMany(‘post’);});
多对多的关系设置都是使用DS.hasMany,但是并不需要“中间表”,这个与数据的多对多有点不同,如果是数据的多对多通常是通过中间表关联。
Ember Data会尽力去发现两个模型之间的关联关系,比如前面的一对多关系中,当comment
发生变化的时候会自动更新到post
,因为每一个comment
只对应一个post
,可以有comment
确定到某个一个post
。
然而,有时候同一个模型中会有多个与此关联模型。这时你可以在反向端用DS.hasMany的inverse
选项指定其关联的模型:
// app/model/comment.jsimport DS from 'ember-data';export default DS.Model.extend({ onePost: DS.belongsTo(‘post’), twoPost: DS.belongsTo(‘post’), redPost: DS.belongsTo(‘post’), bluePost: DS.belongsTo(‘post’)});
在一个模型中同时与3个post
关联了。
// app/models/post.jsimport DS from ‘ember-data’;export default DS.Model.extend({ comments: hasMany(‘comment’, { inverse: ‘redPost’ });});
当comment
发生变化时自动更新到redPost
这个模型。
当你想定义一个自反关系的模型时(模型本身的一对一关系),你必须要显式使用inverse
指定关联的模型。如果没有逆向关系则把inverse
值设置为null
。
// app/models/folder.jsimport DS from ‘ember-data’;export default DS.Model.extend({ children: DS.hasMany(‘folder’, { reverse: ‘parent’ }); parent: DS.hasMany(‘folder’, { reverse: ‘children’ });});
一个文件夹通常有父文件夹或者子文件夹。此时父文件夹和子文件夹与本身都是同一个类型的模型。此时你需要显式使用inverse
属性指定,比如这段代码所示,“children……”这行代码意思是这个模型有一个属性children
,并且这个属性也是一个folder
,模型本身作为父文件夹。同理“parent……”这行代码的意思是这个模型有个属性parent
,并且这个属性也是一个folder
,模型本身是这个属性的子文件夹。比如下图结构:
这个有点像数据结构中的链表。你可以把children
和parent
想象成是一个指针。
如果仅有关联关系没有逆向关系直接把inverse
设置为null
。
// app/models/folder.jsimport DS from ‘ember-data’;export default DS.Model.extend({ parent: DS.belongsTo(‘folder’, { inverse: null });});
// app/models/user.jsimport DS from ‘ember-data’;export default DS.Model.extend({ bestFriend: DS.belongsTo(‘folder’, { inverse: ‘bestFriend’ });});v
这个关系与数据库设置设计中双向一对一很类似。
有些模型可能会包含深层嵌套的数据对象,如果也是使用上述的关联关系定义那么将是个噩梦!对于这种情况最好是把数据定义成简单对象,虽然增加点冗余数据但是降低了层次。另外一种是把嵌套的数据定义成模型的属性(也是增加冗余但是降低了嵌套层次)。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
store提供了统一的获取数据的接口。包括创建新记录、修改记录、删除记录等,更多有关Store API请点击网址看详细信息。
为了演示这些方法的使用我们结合firebase,关于firebase与Ember的整合前面的文章已经介绍,就不过多介绍了。做好准备工作:
ember g route articlesember g route articles/article
首先配置route
,修改子路由增加一个动态段article_id
,有关动态的介绍请看Dynamic Segments。
// app/router.js// 其他代码略写,Router.map(function() { this.route('store-example'); this.route('articles', function() { this.route('article', { path: '/:article_id' }); });});
下面是路由代码,这段代码直接调用Store的find方法,返回所有数据。
// app/routes/articles.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 返回firebase数据库中的所有article return this.store.findAll('article'); }});
为了界面看起来舒服点我引入了bootstrap框架。引入的方式:bower install bootstrap
安装插件。然后修改ember-cli-build.js
,在return
之前引入bootstrap:
app.import("bower_components/bootstrap/dist/js/bootstrap.js");app.import("bower_components/bootstrap/dist/css/bootstrap.css");
重启项目使其生效。
下面是显示数据的模板articles.hbs
。
<div class="container"> <div class="row"> <div class="col-md-4 col-xs-4"> <ul class="list-group"> {{#each model as |item|}} <li class="list-group-item"> {{#link-to 'articles.article' item.id}} {{item.title}} {{/link-to}} </li> {{/each}} </ul> </div> <div class="col-md-8 col-xs-8"> {{outlet}} </div> </div></div>
在浏览器运行:http://localhost:4200/articles/。稍等就可以看到显示的数据了,等待时间与你的网速有关。毕竟firebase不是在国内的!!!如果程序代码没有写错那么你会看到如下图的结果:
但是右侧是空白的,下面点击任何一条数据,可以看到右侧什么都不显示!下面在子模板中增加显示数据的代码:
<h1>{{model.title}}</h1><div class = "body">{{model.body}}</div>
在点击左侧的数据,右侧可以显示对应的数据了!但是这个怎么就显示出来了呢??其实Ember自动根据动态段过滤了,当然你也可以显示使用findRecord
方法过滤。
// app/routes/articles/article.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { console.log('params = ' + params.article_id); // 'chendequanroob@gmail.com' return this.store.findRecord('article', params.article_id); }});
此时得到的结果与不调用findRecord
方法是一致的。为了验证是不是执行了这个方法,我们把动态段params.article_id
的值改成一个不存在的值’ ubuntuvim’,可以确保的是在我的firebase数据中不存在id
为这个值的数据。此时控制台会出现下面的错误信息,从错误信息可以看出来是因为记录不存在的原因。
在上述的例子中,我们使用了findAll()
方法和findRecord()
方法,还有两个方法与这两个方法是类似的,分别是peekRecord()
和peekAll()
方法。这两个方法的不同之处是不会发送请求,他们只会在本地缓存中获取数据。
下面分别修改articles.js
和article.js
这两个路由。使用peekRecord()
和peekAll()
方法测试效果。
// app/routes/articles.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 返回firebase数据库中的所有article // return this.store.findAll('article'); return this.store.peekAll('article'); }});
由于没有发送请求,我也没有把数据存储到本地,所以这个调用什么数据都没有。
// app/routes/articles/article.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { // return this.store.findRecord('article', params.article_id); return this.store.peekRecord('article', params.article_id); }});
由于在父路由中调用findAll
获取到数据并已经存储到Store
中,所以可以用peekRecord()
方法获取到数据。 但是在模型简介这篇文章介绍过Store
的特性,当界面获取数据的时候首先会在Store
中查询数据是否存在,如果不存在在再发送请求获取,所以感觉peekRecord()
和findRecord()
方法区别不是很大!
项目中经常会遇到根据某个值查询出一组匹配的数据。此时返回的数据就不是只有一条了,那么Ember有是怎么去实现的呢?
// app/routes/articles.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 返回firebase数据库中的所有article // return this.store.findAll('article'); // return this.store.peekAll('article'); // 使用query方法查询category为Java的数据 return this.store.query('article', { filter: { category: 'java' } }).then(function(item) { // 对匹配的数据做处理 return item; }); }});
查询category
为Java
的数据。如果你只想精确查询到某一条数据可以使用queryRecord()
方法。如下:
this.store.queryRecord('article', { filter: { id: ' -JzyT-VLEWdF6zY3CefO' } }).then(function(item) { // 对匹配的数据做处理});
到此,常用的方法介绍完毕,希望通过介绍上述几个方法起到抛砖引玉的效果,有关于DS.Store类的还有很多很多的方法,使用方式都是类似的,更多方法请自己看API文档学习。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
前一篇介绍了查询方法,本篇介绍新建、更新、删除记录的方法。本篇的示例代码创建在上一篇的基础上。对于整合firebase、创建route
和template
请参看上一篇,增加一个controller:ember g controller articles
。
创建新的记录使用createRecord()
方法。比如下面的代码新建了一个aritcle
记录。修改模板,在模板上增加几个input
输入框用于输入article
信息。
<div class="container"> <div class="row"> <div class="col-md-4 col-xs-4"> <ul class="list-group"> {{#each model as |item|}} <li class="list-group-item"> {{#link-to 'articles.article' item.id}} {{item.title}} -- <small>{{item.category}}</small> {{/link-to}} </li> {{/each}} </ul> <div> title:{{input value=title}}<br> body: {{textarea value=body cols="80" rows="3"}}<br> category: {{input value=category}}<br> <button {{ action "saveItem"}}>保存</button> <font color='red'>{{tipInfo}}</font> </div> </div> <div class="col-md-8 col-xs-8"> {{outlet}} </div> </div></div>
页面的字段分别对应这模型article
的属性。点击“保存”后提交到controller
处理。下面是获取数据保存数据的controller
。
// app/controllers/articles.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { // 表单提交,保存数据到Store。Store会自动更新到firebase saveItem: function() { var title = this.get('title'); if ('undefined' === typeof(title) || '' === title.trim()) { this.set('tipInfo', "title不能为空"); return ; } var body = this.get('body'); if ('undefined' === typeof(body) || '' === body.trim()) { this.set('tipInfo', "body不能为空"); return ; } var category = this.get('category'); if ('undefined' === typeof(category) || '' === category.trim()) { this.set('tipInfo', "category不能为空"); return ; } // 创建数据记录 var article = this.store.createRecord('article', { title: title, body: body, category: category, timestamp: new Date().getTime() }); article.save(); //保存数据的到Store // 清空页面的input输入框 this.set('title', ""); this.set('body', ""); this.set('category', ""); } }});
主要看createRecord
方法,第一个参数是模型名称。第二个参数是个哈希,在哈希总设置模型属性值。最后调用article.save()
方法把数据保存到Store
,再由Store
保存到firebase。运行效果如下图:
输入信息,点击“保存”后数据立刻会显示在列表”no form -- java”之后。然后你可以点击标题查询详细信息,body的信息会在页面后侧显示。
通过这里实例我想你应该懂得去使用createRecord()
方法了!但是如果有两个模型是有关联关系保存的方法又是怎么样的呢?下面再新增一个模型。
ember g model users
然后在模型中增加关联。
// app/models/article.jsimport DS from 'ember-data';export default DS.Model.extend({ title: DS.attr('string'), body: DS.attr('string'), timestamp: DS.attr('number'), category: DS.attr('string'), author: DS.belongsTo('user') //关联user});// app/models/user.jsimport DS from 'ember-data';export default DS.Model.extend({ username: DS.attr('string'), timestamp: DS.attr('number'), articles: DS.hasMany('article') //关联article});
修改模板articles.hbs
在界面上增加录入作者信息字段。
……省略其他代码<div> title:{{input value=title}}<br> body: {{textarea value=body cols="80" rows="3"}}<br> category: {{input value=category}}<br> <br> author: {{input value=username}}<br> <button {{ action "saveItem"}}>保存</button> <font color='red'>{{tipInfo}}</font></div>……省略其他代码
下面看看怎么在controller
中设置这两个模型的关联关系。一共有两种方式设置,一种是直接在createRecord()
方法中设置,另一种是在方法外设置。
// app/controllers/articles.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { // 表单提交,保存数据到Store。Store会自动更新到firebase saveItem: function() { // 获取信息和校验代码省略…… // 创建user var user = this.store.createRecord('user', { username: username, timestamp: new Date().getTime() }); // 必须要执行这句代码,否则user数据不能保存到Store, // 否则article通过user的id查找不到user user.save(); // 创建article var article = this.store.createRecord('article', { title: title, body: body, category: category, timestamp: new Date().getTime(), author: user //设置关联 }); article.save(); //保存数据的到Store // 清空页面的input输入框 this.set('title', ""); this.set('body', ""); this.set('category', ""); this.set('username', ""); } }});
输入上如所示信息,点击“保存”可以在firebase的后台看到如下的数据关联关系。
注意点:与这两个数据的关联是通过数据的id
维护的。那么如果我要通过article
获取user
的信息要怎么获取呢?
直接以面向对象的方式获取既可。
{{#each model as |item|}} <li class="list-group-item"> {{#link-to 'articles.article' item.id}} {{item.title}} -- <small>{{item.category}}</small> -- <small>{{item.author.username}}</small> {{/link-to}} </li>{{/each}}
注意看助手{{ item.author.username }}
。很像EL表达式吧!!前面提到过有两个方式设置两个模型的关联关系。下面的代码是第二种方式:
// 其他代码省略……// 创建articlevar article = this.store.createRecord('article', { title: title, body: body, category: category, timestamp: new Date().getTime() // , // author: user //设置关联});// 第二种设置关联关系方法,在外部手动调用set方法设置article.set('author', user);// 其他代码省略……
运行,重新录入信息,得到的结果是一致的。甚至你可以直接在createRecord
方法里调用方法来设置两个模型的关系。比如下面的代码段:
var store = this.store; // 由于作用域问题,在createRecord方法内部不能使用this.storevar article = this.store.createRecord('article', { title: title, // …… // , // author: store.findRecord('user', 1) //设置关联});// 第二种设置关联关系方法,在外部手动调用set方法设置article.set('author', store.findRecord('user', 1));
这种方式可以直接动态根据user的id属性值获取到记录,再设置关联关系。新增介绍完了,接着介绍记录的更新。
更新相对于新增来说非常相似。请看下面的代码段:首先在模板上增加更新的设置代码,修改子模板articles/article.hbs
:
<h1>{{model.title}}</h1><div class = "body">{{model.body}}</div><div><br><hr>更新测试<br>title: {{input value=model.title}}<br>body:<br> {{textarea value=model.body cols="80" rows="3"}}<br><button {{action 'updateArticleById' model.id}}>更新文章信息</button></div>
增加一个controller
,用户处理子模板提交的修改信息。
ember g controller articles/article
// app/controllers/articles/article.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { // 根据文章id更新 updateArticleById: function(params) { var title = this.get('model.title'); var body = this.get('model.body'); this.store.findRecord('article', params).then(function(art) { art.set('title', title); art.set('body', body); // 保存更新的值到Store art.save(); }); } }});
在左侧选择需要更新的数据,然后在右侧输入框中修改需要更新的数据,在修改过程中可以看到被修改的信息会立即反应到界面上,这个是因为Ember自动更新Store中的数据(还记得很久前讲过的观察者(observer)吗?)。
如果你没有点击“更新文章信息”提交,你修改的信息不会更新到firebase。页面刷新后还是原来样子,如果你点击了“更新文章信息”数据将会把更新的信息提交到firebase。
由于save
、findRecord
方法返回值是一个promises
对象,所以你还可以针对出错情况进行处理。比如下面的代码:
var user = this.store.createRecord('user', { // ……});user.save().then(function(fulfill) { // 保存成功}).catch(function(error) { // 保存失败});this.store.findRecord('article', params).then(function(art) { // ……}).catch(function(error) { // 出错处理代码});
具体代码我就不演示了,请读者自己编写测试吧!!
既然有了新增那么通常就会有删除。记录的删除与修改非常类似,也是首先查询出要删除的数据,然后执行删除。
// app/controllers/articles.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { // 表单提交,保存数据到Store。Store会自动更新到firebase saveItem: function() { // 省略 }, // 根据id属性值删除数据 delById : function(params) { // 任意获取一个作为判断表单输入值 if (params && confirm("你确定要删除这条数据吗??")) { // 执行删除 this.store.findRecord('article', params).then(function(art) { art.destroyRecord(); alert('删除成功!'); }, function(error) { alert('删除失败!'); }); } else { return; } } }});
修改显示数据的模板,增加删除按钮,并传递数据的id
值到controller
。
<div class="container"> <div class="row"> <div class="col-md-4 col-xs-4"> <ul class="list-group"> {{#each model as |item|}} <li class="list-group-item"> {{#link-to 'articles.article' item.id}} {{item.title}} -- <small>{{item.category}}</small> -- <small>{{item.author.username}}</small> {{/link-to}} <button {{action 'delById' item.id}}>删除</button> </li> {{/each}} </ul> // ……省略其他代码 </div></div>
结果如上图,点击第二条数据删除按钮。弹出提示窗口,点击“确定”之后成功删除数据,并弹出“删除成功!”,到firebase后台查看数据,确实已经删除成功。然而与此关联的user却没有删除,正常情况下也应该是不删除关联的user数据的。最终结果只剩下一条数据:
到此,有关新增、更新、删除的方法介绍完毕。已经给出了详细的演示实例,我相信,如果你也亲自在自己的项目中实践过,那么掌握这几个方法是很容易的!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
Ember的Store就像一个缓存池,用户提交的数据以及从服务器获取的数据会首先保存到Store。如果用户再次请求相同的数据会直接从Store中获取,而不是发送HTTP请求去服务器获取。
当数据发生变化,数据首先更新到Store中,Store会理解更新到其他页面。所以当你改变Store中的数据时,会立即反应出来,比如上一篇更新记录小结,当你修改article
的数据时会立即反应到页面上。
你可以调用push()
方法一次性把多条数据保存到Store中。比如下面的代码:
// app/routes/application.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { this.store.push({ data: [ { id: 1, type: 'album', attributes: { // 设置model属性值 title: 'Fewer Moving Parts', artist: 'David Bazan' songCount: 10 }, relationships: {} // 设置两个model的关联关系 }, { id: 2, type: 'album', attributes: { // 设置model属性值 title: 'Calgary b/w I Can't Make You Love Me/Nick Of Time', artist: 'Bon Iver', songCount: 2 }, relationships: {} // 设置两个model的关联关系 } ] }); }});
注意:type
属性值必须是模型的属性名字。attributes
哈希里的属性值与模型里的属性对应。
本篇不是很重要,就简单提一提,如有兴趣请看Pushing Records Into The Store的详细介绍。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
在前面Ember.js 入门指南之三十八定义模型中介绍过模型之前的关系。主要包括一对一、一对多、多对多关系。但是还没介绍两个有关联关系模型的更新、删除等操作。
为了测试新建两个模型类。
ember g model postember g model comment
// app/models/post.jsimport DS from 'ember-data';export default DS.Model.extend({ comments: DS.hasMany('comment')});// app/model/comment.jsimport DS from 'ember-data';export default DS.Model.extend({ post: DS.belongsTo('post')});
设置关联,关系的维护放在多的一方comment
上。
let post = this.store.peekRecord('post', 1);let comment = this.store.createRecord('comment', { post: post});comment.save();
保存之后post
会自动关联到comment
上(保存post
的id
属性值到post
属性上)。
当然啦,你可以在从post
上设置关联关系。比如下面的代码:
let post = this.store.peekRecord('post', 1);let comment = this.store.createRecord('comment', { // 设置属性值});// 手动吧对象设置到post数组中。(post是多的一方,comments属性应该是保存关系的数组)post.get('comments').pushObject(comment);comment.save();
如果你学过Java里的hibernate框架我相信你很容易就能理解这段代码。你可以想象,post
是一的一方,如果它要维护关系是不是要把与其关联的comment
的id
保存到comments
属性(数组)上,因为一个post
可以关联多个comment
,所以comments
属性应该是一个数组。
更新关联关系与创建关联关系几乎是一样的。也是首先获取需要关联的模型在设置它们的关联关系。
let post = this.store.peekRecord('post', 100);let comment = this.store.peekRecord('comment', 1);comment.set('psot', post); // 重新设置comment与post的关系comment.save(); // 保存关联的关系
假设原来comment
关联的post
是id
为1
的数据,现在重新更新为comment
关联id
为100
的post
数据。
如果是从post
方更新,那么你可以像下面的代码这样:
let post = this.store.peekRecord('post', 100);let comment this.store.peekRecord('comment', 1);post.get('comments').pushObject(comment); // 设置关联post.save(); // 保存关联
既然有新增关系自然也会有删除关联关系。如果要移除两个模型的关联关系,只需要把关联的属性值设置为null
就可以了。
let comment = this.store.peekRecord('comment', 1);comment.set('post', null); //解除关联关系comment.save();
当然你也可以从一的一方移除关联关系。
let post = this.store.peekRecord('post', 1);let comment = this.store.peekRecord('comment', 1);post.get('comments').removeObject(comment); // 从关联数组中移除commentpost.save();
从一的一方维护关系其实就是在维护关联的数组元素。
只要Store改变了Handlebars模板就会自动更新页面显示的数据,并且在适当的时期Ember Data会自动更新到服务器上。
有关于模型之间关系的维护就介绍到这里,它们之间关系的维护只有两种方式,一种是用一的一方维护,另一种是用多的一方维护,相比来说,从一的一方维护更简单。但是如果你需要一次性更新多个纪录的关联时使用第二种方式更加合适(都是针对数组操作)。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
元数据是数据与一个特定的模式或类型,而不是一个纪录。
一个很常见的例子是分页。通常会像下面的代码设置分页:
let result = this.store.query(‘post’, { limit: 10, offset: 0});
设置了每页显示数据为10条,但是你不知道总条数,又怎么知道一共有多少页呢?这时候元数据就派上用场了。
{ "post": { "id": 1, "title": "Progressive Enhancement is Dead", "comments": ["1", "2"], "links": { "user": "/people/tomdale" }, // ... }, "meta": { "total": 100 }}
这些数据是从后台返回的JSON格式数据,如果你想获取元数据可以使用this.get('meta')
获取。甚至还可以从query()
方法中获取。
let
和 =>
都是javascript ES6的语法,如果你想了解有关javascript ES6请Google。
对于元数据在项目中的使用会在后面的例子中展现。在介绍完Ember基础知识后我回做一个比较完整的小项目,我会在项目中尽可能的使用所讲过的知识点,敬请期待……_小项目代码:todos_
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
在Ember应用中适配器决定了数据保存到后台的方式,比如URL格式和请求头部。Ember Data默认的适配器是内置的REST API回调。
实际使用中经常会扩展默认的适配器。Ember的立场是应该通过扩展适配器来添加不同的功能,而非添加标识。这样可以使得代码更加容易测试、更加容易理解,同时也降低了可能需要扩展的适配器的代码。
如果你的后端使用的是Ember约定的规则那么可用使用适配器adapters/application.js
。适配器application
优先级比默认的适配器高,但是比指定的模型适配器优先级低。模型适配器定义规则是:adapter-modelName.js
。比如下面的代码定义了一个模型适配器adapter-post
。
// app/adapters/adapter-post.jsimport Ember from 'ember';export default DS.JSONAPIAdapter.extend({ namespace: 'api/v1'});
此时适配器的优先级次序为:JSONAPIAdapter
> application
> 默认内置适配器;
Ember内置的是配有如下几种:
JSONAPIAdapter
适配器通常用于扩展非标准的后台接口。
JSONAPIAdapter
适配器非常智能,它能够自动确定URL链接是那个模型。比如,如果你需要通过id
获取post
:
this.store.find('post', 1).then(function(post) { // 处理post});
JSONAPIAdapter
会自动发送get
请求到/post/1
。
下表是Action、请求、URL三者的映射关系(由于本站markdown解析器不支持表格所以直接使用截图替代了)。
比如在action
中执行find()
方法,会发送get
请求,JSONAPIAdapter
会自动解析成形如/posts/1
的URL。
为了适配复数名字的模型属性名称,可以是使用Ember Inflector绑定别名。
let inflector = Ember.Inflector.inflector;inflector.irregular('formula', 'formulae');inflector.uncountable('advice');
这样绑定之后目的是告诉JSONAPIAdapter
。你可以使用/formulae/1
代替/formulas/1
。但是目前我还没搞清楚这个设置是什么意思?又有什么用?如果读者知道请指教。
使用属性namespace
可以设置URL的前缀。
app/adapters/application.jsimport Ember from 'ember';export default DS.JSONAPIAdapter.extend({ namespace: 'api/1'});
请求person
会自动转发到/api/1/people/1
。
默认情况下适配器会转到当前域名下。如果你想让URL转到新的域名可以使用属性host设置。
app/adapters/application.jsimport Ember from 'ember';export default DS.JSONAPIAdapter.extend({ host: 'http://api.example.com'});
注意:如果你的项目是使用自己的后台数据库这个设置特别重要!!!属性host
所指的就是你服务器接口的地址。
请求person
会自动转发到http://api.example.com/people/1
。
默认情况下Ember会尝试去根据复数的模型类名、中划线分隔的模型类名生成URL。如果需要改变这个默认的规则可以使用属性pathForType
设置。
// app/adapters/application.jsimport Ember from ‘ember’;export default DS.JSONAPIAdapter.extend({ pathForType: function(type) { return Ember.String.underscore(type); }});
修改默认生成路径规则为下滑线分隔。比如请求person
会转向/person/1
。请求user-profile
会转向/user_profile/1
(默认情况转向user-profile/1
)。
有些请求需要设置请求头信息,比如提供API
的key
。可以以键值对的方式设置头部信息,Ember Data会为每个请求都加上这个头信息设置。
app/adapters/application.jsimport Ember from 'ember';export default DS.JSONAPIAdapter.extend({ headers: {'API_KEY': 'secret key','ANOTHER_HEADER': 'some header value' }});
更强大地方是你可以在header
中使用计算属性,动态设置请求头信息。下面的代码是将一个session
对象设置到适配器中。
app/adapters/application.jsimport Ember from ‘ember’;export default DS.JSONAPIAdapter.extend({ session: Ember.inject.service(‘session’); headers: Ember.computed(‘session.authToken’, function() {‘API_KEY’: this.get(‘session.authToken’),‘ANOTHER_HEADER’: ‘some header value’ });});
对于session
应该非常不陌生,特别是在控制用户登录方面使用非常广泛,另外又一个非常好用的插件,专门用于控制用户登录的,这个插件是Ember Simple Auth,另外有一篇博文是介绍如何使用这个插件实现用户登录的,请看使用ember-simple-auth实现Ember.js应用的权限控制的介绍。
你还可以使用volatile()
方法设置计算属性为非缓存属性,这样每次发送请求都会重新计算header
里的值。
// app/adapters/application.jsimport Ember from ‘ember’;export default DS.JSONAPIAdapter.extend({ // …… }).volatile();});
更多有关于适配器的信息浏览下面的网址:
对于适配器主要掌握JSONAPIAdapter
足矣,如果你需要个性化定制URL或者请求的域名可以在适配中配置。不过大部分情况下都是使用默认设置。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
在Ember应用中,序列化器会格式化与后台交互的数据,包括发送和接收的数据。默认情况下会使用JSON API序列化数据。如果你的后端使用不同的格式,Ember Data允许你自定义序列化器或者定义一个完全不同的序列化器。
Ember Data内置了三个序列化器。JSONAPISerializer是默认的序列化器,用与处理后端的JSON API。JSONSerializer是一个简单的序列化器,用与处理单个JSON对象或者是处理记录数组。RESTSerializer是一个复杂的序列化器,支持侧面加载,在Ember Data2.0之前是默认的序列化器。
当你向服务器请求数据时,JSONSerializer会把服务器返回的数据当做是符合下列规范的JSON数据。
注意:特别是项目使用的是自定义适配器的时候,后台返回的数据格式必须符合JSOP API规范,否则无法实现数据的CRUD操作,Ember就无法解析数据,关于自定义适配器这点的知识请看上一篇Ember.js 入门指南之四十四自定义适配器,在文章中有详细的介绍自定义适配器和自定义序列化器是息息相关的。
JSONSerializer期待后台返回的是一个符合JSON API规范和约定的JSON文档。比如下面的JSON数据,这些数据的格式是这样的:
比如请求/people/123
,响应的数据如下:
{ "data": { "type": "people", "id": "123", "attributes": { "first-name": "Jeff", "last-name": "Atwood" } }}
如果响应的数据有多条,那么data
将是以数组形式返回。
{ "data": [ { "type": "people", "id": "123", "attributes": { "first-name": "Jeff", "last-name": "Atwood" } },{ "type": "people", "id": "124", "attributes": { "first-name": "chen", "last-name": "ubuntuvim" } } ]}
数据有时候并不是请求的主体,如果数据有链接。链接的关系会放在included
下面。
{ "data": { "type": "articles", "id": "1", "attributes": { "title": "JSON API paints my bikeshed!" }, "links": { "self": "http://example.com/articles/1" }, "relationships": { "comments": { "data": [ { "type": "comments", "id": "5" }, { "type": "comments", "id": "12" } ] } } }], "included": [{ "type": "comments", "id": "5", "attributes": { "body": "First!" }, "links": { "self": "http://example.com/comments/5" } }, { "type": "comments", "id": "12", "attributes": { "body": "I like XML better" }, "links": { "self": "http://example.com/comments/12" } }]}
从JSON数据看出,id
为5
的comment
链接是"self": http://example.com/comments/5
。id
为12
的comment
链接是"self": http://example.com/comments/12
。并且这些链接是单独放置included
内。
Ember Data默认的序列化器是JSONAPISerializer,但是你也可以自定义序列化器覆盖默认的序列化器。
要自定义序列化器首先要定义一个名为application
序列化器作为入口。
直接使用命令生成:ember g serializer application
// app/serializers/application.jsimport DS from 'ember-data';export default DS.JSONSerializer.extend({});
甚至你还可以针对某个模型定义序列化器。比如下面的代码为post
定义了一个专门的序列化器,在前一篇自定义适配器中介绍过如何为一个模型自定义适配器,这个两个是相关的。
// app/serializers/post.jsimport DS from ‘ember-data’;export default DS.JSONSerializer.extend({});
如果你想改变发送到后端的JSON数据格式,你只需重写serialize
回调,在回调中设置数据格式。
比如前端发送的数据格式是如下结构,
{ "data": { "attributes": { "id": "1", "name": "My Product", "amount": 100, "currency": "SEK" }, "type": "product" }}
但是服务器接受的数据结构是下面这种结构:
{ "data": { "attributes": { "id": "1", "name": "My Product", "cost": { "amount": 100, "currency": "SEK" } }, "type": "product" }}
此时你可以重写serialize
回调。
// app/serializers/application.jsimport DS from 'ember-data';export default DS.JSONSerializer.extend({ serialize: function(snapshot, options) { var json = this._super(...arguments); // ?? json.data.attributes.cost = { amount: json.data.attributes.amount, currency: json.data.attributes.currency }; delete json.data.attributes.amount; delete json.data.attributes.currency; return json; }});
那么如果是反过来呢。如果后端返回的数据格式为:
{ "data": { "attributes": { "id": "1", "name": "My Product", "cost": { "amount": 100, "currency": "SEK" } }, "type": "product" }}
但是前端需要的格式是:
{ "data": { "attributes": { "id": "1", "name": "My Product", "amount": 100, "currency": "SEK" }, "type": "product" }}
此时你可以重写回调方法normalizeResponse
或normalize
,在方法里设置数据格式:
// app/serializers/application.jsimport DS from 'ember-data';export default DS.JSONSerializer.extend({ normalizeResponse: function(store, primaryModelClass, payload, id, requestType) { payload.data.attributes.amount = payload.data.attributes.cost.amount; payload.data.attributes.currency = payload.data.attributes.cost.currency; delete payload.data.attributes.cost; return this._super(...arguments); }});
每一条数据都有一个唯一值作为ID
,默认情况下Ember会为每个模型加上一个名为id
的属性。如果你想改为其他名称,你可以在序列化器中指定。
// app/serializers/application.jsimport DS from 'ember-data';export default DS.JSONSerializer.extend({ primatyKey: '__id'});
把数据主键名修改为__id
。
Ember Data约定的属性名是驼峰式的命名方式,但是序列化器却期望的是中划线分隔的命名方式,不过Ember会自动转换,不需要开发者手动指定。然而,如果你想修改这种默认的方式也是可以的,只需在序列化器中使用属性keyForAttributes
指定你喜欢的分隔方式即可。比如下面的代码把序列号的属性名称改为以下划线分隔:
// app/serializers/application.jsimport DS from 'ember-data';export default DS.JSONSerializer.extend({ keyForAttributes: function(attr) { return Ember.String.underscore(attr); }});
如果你想模型数据被序列化、反序列化时指定模型属性的别名,直接在序列化器中使用attrs
属性指定即可。
// app/models/person.jsexport default DS.Model.extend({ lastName: DS.attr(‘string’)});
指定序列化、反序列化属性别名:
// app/serializers/application.jsimport DS from 'ember-data';export default DS.JSONSerializer.extend({ attrs: { lastName: ‘lastNameOfPerson’ }});
指定模型属性别名为lastNameOfPerson
。
一个模型通过ID
引用另一个模型。比如有两个模型存在一对多关系:
// app/models/post.jsexport default DS.Model.extend({ comments: DS.hasMany(‘comment’, { async: true });});
序列化后JSON数据格式如下,其中关联关系通过一个存放ID
属性值的数组实现。
{ "data": { "type": "posts", "id": "1", "relationships": { "comments": { "data": [ { "type": "comments", "id": "5" }, { "type": "comments", "id": "12" } ] } } }}
可见,有两个comment
关联到一个post
上。如果是belongsTo
关系的,JSON结构与hadMany
关系相差不大。
{ "data": { "type": "comment", "id": "1", "relationships": { "original-post": { "data": { "type": "post", "id": "5" }, } } }}
id
为1
的comment
关联了ID
为5
的post
。
在某些情况下,Ember内置的属性类型(string
、number
、boolean
、date
)还是不够用的。比如,服务器返回的是非标准的数据格式时。
Ember Data可以注册新的JSON转换器去格式化数据,可用直接使用命令创建:ember g transform coordinate-point
// app/transforms/coordinate-point.jsimport DS from 'ember-data';export default DS.Transform.extend({ deserialize: function(v) { return [v.get('x'), v.get('y')]; }, serialize: function(v) { return Ember.create({ x: v[0], y: v[1]}); }});
定义一个复合属性类型,这个类型由两个属性构成,形成一个坐标。
// app/models/curor.jsimport DS from 'ember-data';export default DS.Model.extend({ position: DS.attr(‘coordinate-point’)});
自定义的属性类型使用方式与普通类型一致,直接作为attr
方法的参数。最后当我们接受到服务返回的数据形如下面的代码所示:
{ cursor: { position: [4, 9] }}
加载模型实例时仍然作为一个普通对象加载。仍然可以使用.
操作获取属性值。
var cursor = this.store.findRecord(‘cursor’, 1);cursor.get(‘position.x’); // => 4cursor.get(‘position.y’); // => 9
并不是所有的API都遵循JSONAPISerializer约定通过数据命名空间和拷贝关系记录。比如系统遗留问题,原先的API返回的只是简单的JSON格式并不是JSONAPISerializer约定的格式,此时你可以自定义序列化器去适配旧接口。并且可以同时兼容使用RESTAdapter去序列号这些简单的JSON数据。
// app/serializer/application.jsexport default DS.JSONSerializer.extend({ // ...});
尽管Ember Data鼓励你拷贝模型关联关系,但有时候在处理遗留API时,你会发现你需要处理的JSON中嵌入了其他模型的关联关系。不过EmbeddedRecordsMixin
可以帮你解决这个问题。
比如post
中包含了一个author
记录。
{ "id": "1", "title": "Rails is omakase", "tag": "rails", "authors": [ { "id": "2", "name": "Steve" } ]}
你可以定义里的模型关联关系如下:
// app/serializers/post.jsexport default DS.JSONSerialier.extend(DS.EmbeddedRecordsMixin, { attrs: {author: { serialize: ‘records’, deserialize: ‘records’} }});
如果你发生对象本身需要序列化与反序列化嵌入的关系,你可以使用属性embedded
设置。
// app/serializers/post.jsexport default DS.JSONSerialier.extend(DS.EmbeddedRecordsMixin, { attrs: {author: { embedded: ‘always’ } }});
序列化与反序列化设置有3个关键字:
records
用于标记全部的记录都是序列化与反序列化的ids
用于标记仅仅序列化与反序列化记录的idfalse
用于标记记录不需要序列化与反序列化例如,你可能会发现你想读一个嵌入式记录提取时一个JSON有效载荷只包括关系的身份在序列化记录。这可能是使用serialize: ids
。你也可以选择通过设置序列化的关系 serialize: false
。
export default DS.JSONSerializer.extend(DS.EmbeddedRecordsMixin, { attrs: { author: { serialize: false, deserialize: 'records' }, comments: { deserialize: 'records', serialize: 'ids' } }});
如果你没有重写attrs
去指定模型的关联关系,那么EmbeddedRecordsMixin
会有如下的默认行为:
belongsTo:{serialize: ‘id’, deserialize: ‘id’ }hasMany: { serialize: false, deserialize: ‘ids’ }
如果项目需要自定义序列化器,Ember推荐扩展JSONAIPSerializer或者JSONSerializer来实现你的需求。但是,如果你想完全创建一个全新的与JSONAIPSerializer、JSONSerializer都不一样的序列化器你可以扩展DS.Serializer
类,但是你必须要实现下面三个方法:
知道规范化JSON数据对Ember Data来说是非常重要的,如果模型属性名不符合Ember Data规范这些属性值将不会自动更新。如果返回的数据没有在模型中指定那么这些数据将会被忽略。比如下面的模型定义,this.store.push()
方法接受的格式为第二段代码所示。
// app/models/post.jsimport DS from 'ember-data';export default DS.Model.extend({ title: DS.attr(‘string’), tag: DS.attr(‘string’), comments: hasMany(‘comment’, { async: true }), relatedPosts: hasMany(‘post’)});
{ data: { id: "1", type: 'post', attributes: { title: "Rails is omakase", tag: "rails", }, relationships: { comments: { data: [{ id: "1", type: 'comment' }, { id: "2", type: 'comment' }], }, relatedPosts: { data: { related: "/api/v1/posts/1/related-posts/" } } }}
每个序列化记录必须按照这个格式要正确地转换成Ember Data记录。
本篇的内容难度很大,属于高级主题的内容!如果暂时理解不来不要紧,你可以先使用firebase构建项目,等你熟悉了整个Ember流程以及数据是如何交互之后再回过头看这篇和上一篇Ember.js 入门指南之四十四自定义适配器,这样就不至于难以理解了!!
到本篇为止,有关Ember的基础知识全部介绍完毕!!!从2015-08-26开始到现在刚好2个月,原计划是用3个月时间完成的,提前了一个月,归其原因是后面的内容难度大,理解偏差大!文章质量也不好,感觉时间比较仓促,说以节省了很多时间!(本篇是重新整理发表的,原始版博文发布的时候Ember还是2.0版本,现在已经是2.5了!!)
介绍来打算介绍APPLICATION CONCERNS和TESTING这两章!也有可能把旧版的Ember todomvc案例改成Ember2.0版本的,正好可以拿来练练手速!!!
很庆幸的是目标:把旧版的Ember todomvc案例改成Ember2.0版本的,也完成了!!!并且扩展了很多功能,有关代码情况todos v2,欢迎读者fork学习!如果觉得有用就给我一个star
吧!!谢谢!!!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
英文原文:https://guides.emberjs.com/v2.7.0/testing/
测试是
Ember。js
框架开发环节中很重要的一环。
现在假设你正在利用Ember框架开发一个博客系统,这个系统包含user
和post
模型,有登录及创建博客的操作。最后假设你希望在你的程序里实现自动化测试。
你一共需要下面这3种类型的测试:
验收测试是用来确保程序流程正确,且各类交互特性符合用户预期的测试。
验收测试用于确认项目基本功能,保证项目核心功能没有退化,确保该项目的目标得以实现。测试应用的方式和用户与应用程序的交互方式是一致的(比如填写表单,点击按钮)。
在上述的场景中,可能会做如下的验收测试:
单元测试是针对程序中的最小可测试单元进行的测试,比如一个类或者一个方法。该测试可以编写与程序逻辑相对的语句来测试相关单元
下面是一些单元测试的具体例子:
集成测试是处于单元测试和验收测试之间的测试。集成测试目的是验证客户端与全系统交互,所有单元测试,以及微观层面具体代码的算法逻辑是否都能通过。
集成测试用来验证应用程序各个模块相互关系,比如若干个UI控件之间的行为。也可以用于确认数据和动作在系统不同的部件中被正确的传递和执行,同时在给定假设条件下,可以提供系统各部件配合运行的情况。
我们建议对每个组件都进行集成测试,因为组件各个组件以相同的方式运行在系统的上下文中,并且组件之间也有相互影响,包括从模板中渲染组件、接收组件生命周期回调函数。
集成测试示例如下:
QUnit是本手册的默认测试框架,但是Ember.js也支持其他第三方的测试框架。
在命令行输入ember test
来运行测试。也可以通过ember test -server
命令,在每次文件改动后,重新运行测试。
在本地开发项目的时候可以通过访问/tests/index.html
来运行你的测试,前提是你需要使用命令ember server
运行了你的项目。如果你是使用这种方式有一点需要注意:
ember server
运行的测试,是在开发环境下的测试,调用的是开发环境下的参数ember test --server
运行的测试,是在测试环境下的测试,调用的是测试环境下的参数,比如加载的依赖也是不同的。因此我们推荐你使用ebmer test --server
来运行测试。使用--filter
选项来指定运行部分测试。比如:快速运行当前工作的测试使用命令ember test --filter="dashboard"
、运行指定类型的测试使用命令ember test --filter="integration"
、可以使用!
来排除验收测试ember test --filter="!acceptance"
。
使用ember generate acceptance-test
创建一个验收测试,比如:
ember g acceptance-test login
执行完毕命令之后得到如下文件内容:
//tests/acceptance/login-test.jsimport { test } from 'qunit';import moduleForAcceptance from 'people/tests/helpers/module-for-acceptance';moduleForAcceptance('Acceptance | login');test('visting /login', function(assert) { visit('/login'); andThen(function() { assert.equal(currentURL(), '/login'); });});
moduleForAcceptance
用来启动、终止程序。最后几行test
中包含了一个示例。
几乎所有的测试都有一个路由请求,用于和页面交互(通过helper)并检查DOM是否按照期望值进行改变。
举个例子:
test('should add new post', function(assert) { visit('/posts/new'); fillIn('input.title', 'My new post'); click('button.submit'); andThen(() => assert.equal(find('ul.posts li:first').text(), 'My new post'));});
大体意思为:进入路由/posts/new
,在输入框input.title
填入My new post
,点击button.submit
,期望的结果是: 在对应列表下ul.posts li.first
的文本为My new post
.
在测试web应用中的一个主要的问题是,由于代码都是基于事件驱动的,因此他们有可能是异步
的,会使代码无序
运行。
比如有两个按钮,从不同的服务器载入数据,我们先后点击他们,但可能结果返回的顺序并不是我们点击的顺序。
当你在编写测试的时候,你需要特别注意一个问题,就是你无法确定在发出一个请求后,是否会立刻得到返回的响应。因此,你的断言需要以同步的状态来等待被测试体。例如上面所举的例子,应该等待两个服务器均返回数据后,这时测试代码才会执行其逻辑来检测数据的正确性。
这就是为什么在做断言的时候,Ember测试助手都是被包裹在一个确保同步状态的代码中。这样做避免了对所有这样的代码都去做这样的包裹,并且因为减少了模板代码,从而提高了代码的可读性.
Ember包含多个测试助来辅助进行验收测试。一共有2种类型:异步助手asynchronous
和同步助手synchronous
异步测试助手可以意识到程序中的异步行为,使你可以更方便的编写确切的测试。
同时,这些测试助手会按注册的顺序执行,并且是链式运行。每个测试助手的调用都是在前一个调用结束之后,才会执行下一个。因此,你可以不用当心测试助手的执行顺序。
click(selector)
click
事件,返回一个异步执行成功的promise
。fillIn(selector, value)
作为
时,记得
元素的value
是标签所指定的值,并不是
标签显示的内容。keyEvent(selector, type, keyCode)
keypress
,按键按下keydown
,按键弹起keyup
事件的keyCode
。triggerEvent(selector,type,keyCode)
blur
、ddlclick
等事件...visit(url)
同步测试助手,当触发时会立即执行并返回结果.
currentPath()
currentRouteName()
currentURL()
find(selector, context)
andThen
测试助手将会等待所有异步测试助手完成之后再执行.举个例子:
// tests/acceptance/new-post-appears-first-test.jstese('should add new post', function(assert) { visit('/posts/new'); fillIn('input.title', 'My new post'); click('button.submit'); andThen(() => assert.equal(find('ul.posts li:first').text(), 'My new post')); });
首先,我们访问/posts/new
地址,在有title
css类的input
输入框内内填入内容“My new post”,然后点击有CSS类submit
的按钮。
等待签名的异步测试助手执行完(具体说,andThen
会在路由/posts/new
访问完毕,input
表单填充完毕,按钮被点击之后)毕后会执行andThen
助手。注意andThen
助手只有一个参数,这个参数是一个函数,函数的代码是其实测试助手执行完毕之后才执行的代码。
在andThen
助手中,我们最后会调用assert.equal
断言来判定对应位置的值是否为My new post
。
使用命令ember generate test-helper
来创建自定义测试助手。
下面的代码是执行命令ember g test-helper shouldHaveElementWithCount
得到的测试例子:
//tests/helpers/should-have-element-with-count.jsexport default Ember.Test.registerAsyncHelper( 'shouldHaveElementWithCount', function(app){});
Ember.Test.registerAsyncHelper
和Ember.Test.registerHelper'是当
startApp被调用时,会将自定义测试助手注册。两者的区别在于,前者
Ember.Test.registerHelper`会在之前任何的异步测试助手运行完成之后运行,并且后续的异步测试助手在运行前都会等待他完成.
测试助手方法一般都会以当前程序的第一个参数被调用,其他参数,比如assert
,在测试助手被调用的时候提供。测试助手需要在调用startApp
前进行注册,但ember-cli
会帮你处理,你不需要担心这个问题。
下面是一个非异步的测试助手:
//tests/helpers/should-have-element-with-count.jsexport default Ember.Test.registerHelper('shouldHaveElementWithCount', function(app, assert, selector, n, context){ const el = findWithAssert(selector, context); const count = el.length; assert.equal(n, count, 'found ${count} times'); });//shouldHaveElementWithCount(assert, 'ul li', 3);
下面是一个异步的测试助手:
export default Ember.Test.registerAsynHelper('dblclick', function(app, assert, selector, context){ let $el = findWithAssert(selector, context); Ember.run(() => $el.dblclick()); });//dblclick(assert, '#persion-1')
异步测试助手也可以让你将多个测试助手合并为一个.举个例子:
//tests/helpers/add-contact.jsexport default Ember.Test.registerAsyncHelper('addContact', function(app,name) { fillIn('#name', name); click('button.create'); });//addContact('Bob');//addContact('Dan');
最后, 别忘了将你的测试助手添加进tests/.jshintrc
和tests/helpers/start-app.js
中、在tests/.jshintrc
中,你需要将其添加进predef
块中,不然就会得到jshint测试失败的消息.
{ "predef": [ "document", "window", "locaiton", ... "shouldHaveElementWithCount", "dblclick", "addContact" ], ...}*
你需要在tests/helpers/start-app.js
引入测试助手,这些助手将会被注册到应用中。
import Ember from 'ember';import Application from '../../app';import Router from '../../router';import config from '../../config/environmnet';import './should-have-element-with-count';import './dblclick';import './add-contact';
单元测试一般被用来测试一些小的代码块,并确保它正在做的是什么。与验收测试不同的是,单元测试被限定在小范围内并且不需要Emeber程序运行。
与Ember基本对象一样的,创建单元测试也只需要继承Ember.Object
即可。然后在代码块内编写具体的测试内容,比如控制器、组件。每个测试就是一个Ember.Object
实例对象,你可以设置对象的状态,运行断言。通过下面的例子,让我们一起看看测试如何使用。
创建一个简单的实例,实例内包含一个计算属性computedFoo
,此计算属性依赖普通属性foot
。
//app/models/somt-thing.jsexport default Ember.Object.extend({ foo: 'bar', computedFoo: Ember.compuuted('foo',function() { const foo = this.get('foo'); return `computed ${foo}`; })});
在测试中,我们建立一个实例,然后更新属性foo
的值(这个操作会触发计算属性computedFoo
,使其自动更新),然后给出一个符合预期的断言
:
//tests/unit/models/some-thing-test.jsimport {moduleFor, test} from 'ember-qunit';moduleFor('model:some-thing', 'Unit | some thing', { unit: true});test('should correctly concat foo', function(assert) { const someThing = this.subject(); somtThing.set('foo', 'baz'); //设置属性foo的值 assert.equal(someThing.get('computedFoo'), 'computed baz'); //断言,判断计算属性值是否相等于computed baz});
例子中使用了moduleFor
,它是由Ember-Qunit
提供的单元测试助手。这些测试助手为我们提供了很多便利,比如subject
功能,它可以寻找并实例化测试所用的对象。同时你还可以在subject
方法中自定义初始化的内容,这些初始化的内容可以是传递给主体功能的实例变量。比如在单元测试内初始化属性“foo”你可以这样做:this.subject({foo: 'bar'});
,那么单元测试在运行时属性foo
的值就是bar
。
下面让我们来看一下如何测试对象方法的逻辑。在本例中对象内部有一个设置属性(更新属性foo
值)值的方法testMethod
。
//app/models/some-thing.jsexport default Ember.Object.extend({ foo: 'bar', testMethod() { this.set('foo', 'baz'); }});
要对其进行测试,我们先创建如下实例,然后调用testMethod
方法,然后用断言判断方法的调用结果是否是正确的。
//tests/unit/models/some-thing-test.jstest('should update foo on testMethod', function(assert) { const someThing = this.subject(); someThing.testMethod(); assert.equal(someThing.get('foo'), 'baz');});
如果一个对象方法返回的是一个值,你可以很容易的给予断言进行判定是否正确。假设我们的对象拥有一个calc
方法,方法的返回值是基于对象内部的状态值。代码如下:
//app/models/some-thing.jsexport default Ember.Object.extend({ count: 0, calc() { this.incrementProperty('count'); let count = this.get('count'); return `count: ${count}`; }});
在测试中需要调用calc
方法,并且断言其返回值是否正确。
//tests/unit/models/some-thing-test.jstest('should return incremented count on calc', function(assert) { const someThing = this.subject(); assert.equal(someThing.calc(), 'count: 1'); assert.equal(someThing.calc(), 'count: 2');});
假设我们有一个对象,这个对象拥有一些属性,并且有一个方法在监测着这些属性。
//app/models/some-thing.jsexport default Ember.Object.extend({ foo: 'bar' other: 'no',, doSomething: Ember.observer('foo', function() { this.set('other', 'yes'); })});
为了测试doSomething
方法,我们创建一个SomeThing
对象,更新foo
属性值,然后进行断言是否达到预期结果。
//tests/unit/models/some-thing-test.jstest('should set other prop to yes when foo changes', function(assert) { const someThing = this.subject(); someThing.set('foo', 'baz'); assert.equal(someThing.get('other'), 'yes'); });
Ember JS提供一套自己的类系统,普通的JavaScript
标准类不能自动更新属性值,Ember JS的类会自动触发观察者,自动更新属性值、自动刷新模板上的属性值。如果一个类是Ember JS提供的可以看到前缀命名空间是Ember.Object
。Ember
类定义使用extend()
方法,创建类实例使用create()
方法,可以在方法传入参数,但是参数要以hash
列表方式传入。
Ember JS重写了标准JavaScript
的数组类Array
,并且为了与标准JavaScript
类区别命名为Ember.Enumerable
(API介绍)
Ember JS还扩展了String
属性的特性,提供了一系列特有的处理方法,API介绍。
关于类的命名规则在此不做介绍,自己网上找一份Java
的命名规则的教材看看即可。
开始之前先做好准备工作,首先创建一个HTML文件,并引入Ember JS所必须的文件(后面会介绍一种更加简单的方法去搭建EmberJS
的项目方法,当然如果你有时间也可以提前去了解,这种方式是使用Ember CLI
搭建EmberJS
的项目)。
Ember.js • Guides // 在这里编写Ember代码
上面代码是一个简单的HTML
文件,所需的Ember
库直接使用CDN
。
下面定义一个Person
类,定义方式如下:
Person = Ember.Object.extend({ say(thing) { alert(name); }});
上面代码定义了一个Person
类,并且在类里面还定义了一个方法say
,方法传入一个参数thing
。方法体仅仅是打印了传入的参数。
在子类重写父类的方法,并在方法里调用_super()
方法来调用父类中对应的方法触发父类方法的行为。
Person = Ember.Object.extend({ say(thing) { var name = this.get('name'); alert(name + " says: " + thing); }});Soldier = Person.extend({ say(thing) { // this will call the method in the parent class (Person#say), appending // the string ", sir!" to the variable `thing` passed in this._super(thing + ", sir!"); }});var yehuda = Soldier.create({ name: "Yehuda Katz"});yehuda.say("Yes"); // alerts "Yehuda Katz says: Yes, sir!"
运行代码,刷新浏览器,可以看到如下结果:
结果正确了,但是我们还不知道类是怎么初始化的,它初始化的顺序又是怎么样的呢?其实每个类都有一个默认的初始化方法,555……别急,接着往下看。
要获取一个类的实例只需要调用类的create()
方法即可。
Person = Ember.Object.extend({ show() { console.log("My name is " + this.get('name')); }});var person = Person.create({ name: 'ubuntuvim'});person.show(); // My name is ubuntuvim var person2 = Person.create({ pwd: 'ubuntuvim'});// 由于创建person2的时候没有设置name的值,默认是undefinedperson2.show(); // My name is undefined
注意:处于性能的考虑在使用create()
方法创建实例的时候,不允许新定义、重写计算属性,也不推荐新定义、重写普通方法,Ember
推荐在使用create()
方法时只是传递简单的参数,比如上述代码的{name: 'ubuntuvim'}
。如果你需要新地定义、重写方法请新建一个子类来实现。
在create()
方法内定义计算属性,运行后会直接报如下图的报错信息。
Person = Ember.Object.create({ show() { console.log("My name is " + this.get('name')); }, fullName: Ember.computed(function() { console.log("computed properties."); })});
前面提过,我们在类继承的时候到底类是怎么初始化,这节就介绍类的初始化,Ember
定义了一个init()
方法,此方法在类被实例化的时候自动调用。
Parent = Ember.Object.extend({ init() { console.log("parent init..."); }, show() { console.log("My name is " + this.get('name')); }, others() { console.log("the method in parent class.."); }});//parent = Parent.create({// name: 'parent'//}); Child = Parent.extend({ init() { console.log("child init..."); }, show() { this._super(); }});child = Child.create({ name: 'child'}); child.show();child.others();
注意:init()
方法只有在类的create()
方法被调用的时候才会被自动调用,上面的例子中,如果只是child.others()
这个方法父类并不会调用init()
方法,只有执行Parent.create()
这个调用的时候才会执行init()
方法。上面代码如果把Parent.create()
这几句代码注释掉得到的结果如下:
可见父类的init()
方法没有被调用,然后修改代码,注释掉child.others()
这句,再把Parent.create()
这几句的注释去掉。得到如下结果
可以看到父类的init()
方法被调用了!由此可见init()
方法是在调用create()
方法的时候才调用的。在项目中有可能你需要继承Ember
提供的组件,比如继承Ember.Component
类,此时你就要注意了,在你继承Ember
的组件的时候你必须显式的调用父类方法this._super()
否则你继承得到的类无法获取Component
提供的行为或者得到无法预知的结果。
Ember
建议访问类的属性使用get、set
方法。如果你直接使用obj.prop
这种方式访问也是可以得到类的属性值,但是如果你不是使用访问器操作的就会导致很多问题:计算属性不能被重新计算、无法察觉对象属性的变化、模板也不能自动更新。
Person = Ember.Object.extend({ name: 'ubuntuvim'});// Ember 推荐的访问方式var person = Person.create();console.log("My name is " + person.get('name'));person.set('name', "Tobias Funke");console.log("My name is " + person.get('name')); console.log("---------------------------");// 不推荐的方式var person2 = Person.create(); console.log("My name is " + person2.name);person2.name = "Tobias Funke";console.log("My name is " + person2.name);
Ember为我们封装了get、set
实现细节,开发者直接使用即可。
最后感谢唯獨莪靑睐的指正。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用在github项目上给我个star
吧。您的肯定对我来说是最大的动力!!
reopen
不知道怎么翻译好,如果按照reopen
翻译过来应该是“重新打开”,但是总觉得不顺,所以就译成扩展
了,如果有不妥请指正。
当你想扩展一个类你可以直接使用reopen()
方法为一个已经定义好的类添加属性、方法。如果是使用extend()
方法你需要重新定义一个子类,然后在子类中添加新的属性、方法。前一篇所过,调用create()
方法时候不能传入计算属性并且不推荐在此方法中新定义、重写方法,但是使用reopen()
方法可以弥补create()
方法的补足。与extend()
方法非常相似,下面的代码演示了它们的不同之处。
Parent = Ember.Object.extend({ name: 'ubuntuvim', fun1() { console.log("The name is " + this.name); }, common() { console.log("common method..."); }}); // 使用extend()方法添加新的属性、方法Child1 = Parent.extend({ // 给类Parent为新增一个属性 pwd: '12345', // 给类Parent为新增一个方法 fun2() { console.log("The pwd is " + this.pwd); }, // 重写父类的common()方法 common() { //console.log("override common method of parent..."); this._super(); }}); var c1 = Child1.create();console.log("name = " + c1.get('name') + ", pwd = " + c1.get('pwd')); c1.fun1();c1.fun2(); c1.common();console.log("-----------------------"); // 使用reopen()方法添加新的属性、方法Parent.reopen({ // 给类Parent为新增一个属性 pwd: '12345', // 给类Parent为新增一个方法 fun2() { console.log("The pwd is " + this.pwd); }, // 重写类本身common()方法 common() { console.log("override common method by reopen method..."); //this._super(); }, // 新增一个计算属性 fullName: Ember.computed(function() { console.log("compute method..."); })});var p = Parent.create(); console.log("name = " + p.get('name') + ", pwd = " + p.get('pwd')); p.fun1();p.fun2(); p.common();console.log("---------------------------"); p.get('fullName'); // 获取计算属性值,这里是直接输出:compute method...// 使用extend()方法添加新的属性、方法Child2 = Parent.extend({ // 给类Parent为新增一个属性 pwd: '12345', // 给类Parent为新增一个方法 fun2() { console.log("The pwd is " + this.pwd); }, // 重写父类的common()方法 common() { //console.log("override common method of parent..."); this._super(); }}); var c2 = Child2.create();console.log("name = " + c2.get('name') + ", pwd = " + c2.get('pwd')); c2.fun1();c2.fun2(); c2.common();
从执行结果可以看到如下的差异:
同点: 都可以用于扩展某个类。
异点:
extend
需要重新定义一个类并且要继承被扩展的类;reopen
是在被扩展的类本身上新增属性、方法,可以扩展计算属性(相比create()
方法); 到底用那个方法有实际情况而定,reopen
方法会改变了原类的行为(可以想象为修改了对象的原型对象的方法和属性),就如演示实例一样在reopen
方法之后调用的Child2
类的common
方法的行为已经改改变了,在编码过程忘记之前已经调用过reopen
方法就有可能出现自己都不知道怎么回事的问题!如果是extend
方法会导致类越来越多,继承树也会越来越深,对性能、调试也是一大挑战,但是extend
不会改变被继承类的行为。
使用reopenClass()
方法可以扩展static
类型的属性、方法。
Parent = Ember.Object.extend(); // 使用reopenClass()方法添加新的static属性、方法Parent.reopenClass({ isPerson: true, username: 'blog.ddlisting.com' //,name: 'test' //这里有点奇怪,不知道为何不能使用名称为name定义属性,会提示这个是自读属性,使用username却没问题!!估计name是这个方法的保留关键字});Parent.reopen({ isPerson: false, name: 'ubuntuvim'});console.log(Parent.isPerson);console.log(Parent.name); // 输出空console.log(Parent.create().get('isPerson'));console.log(Parent.create().get('name')); // 输出 ubuntuvim
对于在reopenClass
方法中使用属性name
的问题下面的地址有解释
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用在github项目上给我个star
吧。您的肯定对我来说是最大的动力!!
简单地来说,计算属性就是将函数声明为属性,就类似于调用了一个函数,Ember
会自动调用这个函数。计算属性最大的特点就是能自动检测变化,及时更新数据。
Person = Ember.Object.extend({ firstName: null, lastName: null, // fullName 就是一个计算属性 fullName: Ember.computed('firstName', 'lastName', function() { return this.get('firstName') + ", " + this.get('lastName'); })});// 实例化同时传入参数var piter = Person.create({ firstName: 'chen', lastName: 'ubuntuvim'});console.log(piter.get('fullName')); // output >> chen, ubuntuvim
计算属性其实就是一个函数,如果你接触过就jQuery、Extjs
相信你会非常熟悉,在这两个框架中函数就是这么定义的。只不过在Ember
中,把这种函数当做属性来处理,并且可以通过get获取函数的返回值。
在Ember
程序中,计算属性还能调用另外一个计算属性,形成计算属性链,也可以用于扩展某个方法。在上一实例的基础上增加一个description()
方法。
Person = Ember.Object.extend({ firstName: null, lastName: null, age: null, county: null, // fullName 就是一个计算属性 fullName: Ember.computed('firstName', 'lastName', function() { return this.get('firstName') + ", " + this.get('lastName'); }), description: Ember.computed('fullName', 'age', 'county', function() { return this.get('fullName') + " age " + this.get('age') + " county " + this.get('county'); })});// 实例化同时传入参数var piter = Person.create({ firstName: 'chen', lastName: 'ubuntuvim', age: 25, county: 'china'});console.log(piter.get('description')); // output >> chen, ubuntuvim
当用户使用set
方法改变firstName
的值,然后再调用get('description')
得到的值也是更新后的值。
注意要把重写的属性作为参数传入computed
方法,要区别计算属性的定义方法,定义的时候computed
方法的最后一个参数是一个function
,而重写的时候最后一个参数是一个hash
。
// 重写计算属性的get、set方法Person = Ember.Object.extend({ firstName: null, lastName: null, // 重写计算属性fullName的get、set方法 fullName: Ember.computed('firstName', 'lastName', { get(key) { return this.get('firstName') + "," + this.get('lastName'); }, set(key, value) { // 这个官方文档使用的代码,但是我运行的时候出现 Uncaught SyntaxError: Unexpected token [ 这个错误,不知道是否是缺少某个文件,后续会补上;// console.log("value = " + value);// var [ firstName, lastName ] = value.split(/s+/); var firstName = value.split(/s+/)[0]; var lastName = value.split(/s+/)[1]; this.set('firstName', firstName); this.set('lastName', lastName); } }),// 对于普通的属性无法重写get、set方法// firstName: Ember.computed('firstName', {// get(key) {// return this.get('firstName') + "@@";// },// set(key, value) {// this.set('firstName', value);// }// })}); var jack = Person.create(); jack.set('fullName', "james kobe");console.log(jack.get('firstName'));console.log(jack.get('lastName'));
我们经常会遇到这种情况:某个计算属性值是依赖某个数组或者其他对象的,比如在Ember
的todos
这个例子中有这样的一段代码。
export default Ember.Controller.extend({ todos: [ Ember.Object.create({ isDone: true }), Ember.Object.create({ isDone: false }), Ember.Object.create({ isDone: true }) ], remaining: Ember.computed('todos.@each.isDone', function() { var todos = this.get('todos'); return todos.filterBy('isDone', false).get('length'); })});
计算属性remaining
的值于依赖数组todos
。在这里还有个知识点:在上述代码computed()
方法里有一个todos.@each.isDone
这样的键,里面包含了一个特别的键@each
(后面还会看到更特别的键[]
)。需要注意的是这种键不能嵌套并且是只能获取一个层次的属性。比如todos.@each.foo.name
(获取多层次属性,这里是先得到foo再获取name
)或者todos.@each.owner.@each.name
(嵌套)这两种方式都是不允许的。
在如下4种情况Ember
会自动更新绑定的计算属性值:<br>1.在todos
数组中任意一个对象的isDone
属性值发生变化的时候;2.往todos
数组新增元素的时候;3.从todos
数组删除元素的时候;4.在控制器中todos
数组被改变为其他的数组的时候;
比如下面代码演示的结果;
Task = Ember.Object.extend({ isDone: false // 默认为false}); WorkerLists = Ember.Object.extend({ // 定义一个Task对象数组 lists: [ Task.create({ isDone: false }), Task.create({ isDone: true }), Task.create(), Task.create({ isDone: true }), Task.create({ isDone: true }), Task.create({ isDone: true }), Task.create({ isDone: false }), Task.create({ isDone: true }) ], remaining: Ember.computed('lists.@each.isDone', function() { var lists = this.get('lists'); // 先查询属性isDone值为false的对象,再返回其数量 return lists.filterBy('isDone', false).get('length'); })});// 如下代码使用到的API请查看:http://emberjs.com/api/classes/Ember.MutableArray.htmlvar wl = WorkerLists.create();// 所有isDone属性值未做任何修改console.log('1,>> Not complete lenght is ' + wl.get('remaining')); // output 3var lists = wl.get('lists'); // 得到对象内的数组// ----- 演示第一种情况: 1. 在todos数组中任意一个对象的isDone属性值发生变化的时候;// 修改数组一个元素的isDone的 值var item1 = lists.objectAt(3); // 得到第4个元素 objectAt()方法是Ember为我们提供的// console.log('item1 = ' + item1);item1.set('isDone', false);console.log('2,>> Not complete lenght is ' + wl.get('remaining')); // output 4// --------- 2. 往todos数组新增元素的时候;lists.pushObject(Task.create({ isDone: false })); //新增一个isDone为false的对象console.log('3,>> Not complete lenght is ' + wl.get('remaining')); // output 5// --------- 3. 从todos数组删除元素的时候;lists.removeObject(item1); // 删除了一个元素console.log('4,>> Not complete lenght is ' + wl.get('remaining')); // output 4// --------- 4. 在控制器中todos数组被改变为其他的数组的时候;// 创建一个ControllerTodosController = Ember.Controller.extend({ // 在控制器内定义另外一个Task对象数组 todosInController: [ Task.create({ isDone: false }), Task.create({ isDone: true }) ], // 使用键”@each.isDone“遍历得到的filterBy()方法过滤后的对象的isDone属性 remaining: function() { // remaining()方法返回的是控制器内的数组 return this.get('todosInController').filterBy('isDone', false).get('length'); }.property('@each.isDone') // 指定遍历的属性});todosController = TodosController.create();var count = todosController.get('remaining');console.log('5,>> Not complete lenght is ' + count); // output 1
上述的情况中,我们对数组对象的是关注点是在对象的属性上,但是实际中往往很多情况我们并不关系对象内的属性是否变化了,而是把数组元素作为一个整体对象处理(比如数组元素个数的变化)。相比上述的代码下面的代码检测的是数组对象元素的变化,而不是对象的isDone
属性的变化。在这种情况你可以看看下面例子,在例子中使用键[]
代替键@each
。从键的变化也可以看出他们的不同之处。
Task = Ember.Object.extend({ isDone: false, // 默认为false name: 'taskName', // 为了显示结果方便,重写toString()方法 toString: function() { return '[name = '+this.get('name')+', isDone = '+this.get('isDone')+']'; }}); WorkerLists = Ember.Object.extend({ // 定义一个Task对象数组 lists: [ Task.create({ isDone: false, name: 'ibeginner.sinaapp.com' }), Task.create({ isDone: true, name: 'i2cao.xyz' }), Task.create(), Task.create({ isDone: true, name: 'ubuntuvim' }), Task.create({ isDone: true , name: '1527254027@qq.com'}), Task.create({ isDone: true }) ], index: null, indexOfSelectedTodo: Ember.computed('index', 'lists.[]', function() { return this.get('lists').objectAt(this.get('index')); })});var wl = WorkerLists.create();// 所有isDone属性值未做任何修改var index = 1;wl.set('index', index);console.log('Get '+wl.get('indexOfSelectedTodo').toString()+' by index ' + index);
Ember.computed
这个组件中有很多使用键[]
实现的方法。当你想创建一个计算属性是数组的时候特别适用。你可以使用Ember.computed.map
来构建你的计算属性。
const Hamster = Ember.Object.extend({ chores: null, excitingChores: Ember.computed('chores.[]', function() { //告诉Ember chores是一个数组 return this.get('chores').map(function(chore, index) { //return `${index} --> ${chore.toUpperCase()}`; // 可以使用${}表达式,并且在表达式内可以直接调用js方法 return `${chore}`; //返回元素值 }); })});// 为数组赋值const hamster = Hamster.create({ // 名字chores要与类Hamster定义指定数组的名字一致 chores: ['First Value', 'write more unit tests']});console.log(hamster.get('excitingChores'));hamster.get('chores').pushObject("Add item test"); //add an item to chores arrayconsole.log(hamster.get('excitingChores'));
Ember
还提供了另外一种方式去定义数组类型的计算属性。
const Hamster = Ember.Object.extend({ chores: null, excitingChores: Ember.computed('chores.[]', function() { return this.get('chores').map(function(chore, index) { //return `${index} --> ${chore.toUpperCase()}`; // 可以使用${}表达式,并且在表达式内可以直接调用js方法 return `${chore}`; //返回元素值 }); })});// 为数组赋值const hamster = Hamster.create({ // 名字chores要与类Hamster定义指定数组的名字一致 chores: ['First Value', 'write more unit tests']});console.log(hamster.get('excitingChores'));hamster.get('chores').pushObject("Add item test"); //add an item to chores arrayconsole.log(hamster.get('excitingChores'));
<br>博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用在github项目上给我个star
吧。您的肯定对我来说是最大的动力!!
Ember
可以检测任何属性的变化,包括计算属性。
Ember
可以察觉所有属性的变化,包括计算属性。观察者是非常有用的,特别是计算属性绑定之后需要同步的时候。观察者经常被Ember开发过度使用。Ember
框架本身已经大量使用观察者,但是对于大多数的开发者面对开发问题时使用计算属性是更适合的解决方案。使用方式:可以用Ember.observer
创建一个对象为观察者。
// Observer对于Emberjs来说非常重要,前面你看到的很多代码都是与它有关系,计算属性之所以能更新也是因为它Person = Ember.Object.extend({ firstName: null, lastName: null, fullName: Ember.computed('firstName', 'lastName', function() { return this.get('firstName') + " " + this.get('lastName'); }), // 当fullName被改变的时候触发观察者 fullNameChange: Ember.observer('fullName', function() { console.log("The fullName is changed by caller"); //return this.get('fullName'); })});var person = Person.create({ firstName: 'chen', lastName: 'ubuntuvim'});// 如果被观察的计算属性还没执行过get()方法不会触发观察者console.log('fullName = ' + person.get('fullName')); // fullName是依赖firstName和lastName的,这里改变了firstName的值,计算属性会自动更新,// fullName被改变了所以会触发观察者person.set('firstName', 'change firstName value'); // 观察者会被触发console.log('fullName = ' + person.get('fullName'));
fullName
是依赖firstName
和lastName
的,调用set()
方法改变了firstName
的值,自然的导致fullName
的值也被改变了,fullName
变化了就触发观察者。从执行的结果就可以看出来;
Ember
还为开发者提供了另一种使用观察者的方式。这种方式使你可以在类定义之外为某个计算属性增加一个观察者。
person.addObserver('fullName', function() { // deal with the change…});
目前,观察者在Ember
中是同步的(不是笔误,官网就是这么说的Observers in Ember are currently synchronous.
)。这就意味着只要计算属性一发生变化就会触发观察者。也因为这个原因很容易就会引入这样的bug
在计算属性没有同步的时候。比如下面的代码;
Person.reopen({ lastNameChanged: Ember.observer('lastName', function() { // The observer depends on lastName and so does fullName. Because observers // are synchronous, when this function is called the value of fullName is // not updated yet so this will log the old value of fullName console.log(this.get('fullName')); })});
然而由于同步的原因如果你的的观察者同时观察多个属性,就会导致观察者执行多次。
person = Ember.Object.extend({ firstName: null, lastName: null, fullName: Ember.computed('firstName', 'lastName', function() { return this.get('firstName') + " " + this.get('lastName'); }), // 当fullName被改变的时候触发观察者 fullNameChange: Ember.observer('fullName', function() { console.log("The fullName is changed by caller"); //return this.get('fullName'); })});Person.reopen({ partOfNameChanged: Ember.observer('firstName', 'lastName', function() { // 同时观察了firstName和lastName两个属性 console.log('========partOfNameChanged======'); })});var person = Person.create({ firstName: 'chen', lastName: 'ubuntuvim'});person.set('firstName', '[firstName]');person.set('lastName', '[lastName]');
显然上述代码执行了两次set()
所以观察者也会执行2次,但是如果开发中需要设置只能执行一次观察出呢?Ember提供了一个once()
方法,这个方法会在下一次循环所有绑定属性都同步的时候执行。
Person = Ember.Object.extend({ firstName: null, lastName: null, fullName: Ember.computed('firstName', 'lastName', function() { return this.get('firstName') + " " + this.get('lastName'); }), // 当fullName被改变的时候触发观察者 fullNameChange: Ember.observer('fullName', function() { console.log("The fullName is changed by caller"); //return this.get('fullName'); })});Person.reopen({ partOfNameChanged: Ember.observer('firstName', 'lastName', function() { // 同时观察了firstName和lastName两个属性 // 方法partOfNameChanged本身还是会执行多次,但是方法processFullName只会执行一次 console.log('========partOfNameChanged======'); // Ember.run.once(this, 'processFullName'); }), processFullName: Ember.observer('fullName', function() { // 当你同时设置多个属性的时候,此观察者只会执行一次,并且是发生在下一次所有属性都被同步的时候 console.log('fullName = ' + this.get('fullName')); })});var person = Person.create({ firstName: 'chen', lastName: 'ubuntuvim'});person.set('firstName', '[firstName]');person.set('lastName', '[lastName]');
观察者一直到对象初始化完成之后才会执行。如果你想观察者在对象初始化的时候就执行你必须要手动调用Ember.on()
方法。这个方法会在对象初始化之后就执行。
Person = Ember.Object.extend({ salutation:null, init() { this.set('salutation', 'hello'); console.log('init....'); }, salutationDidChange: Ember.on('init', Ember.observer('salutation', function() { console.log('salutationDidChange......'); }))});var p = Person.create();p.get('salutationDidChange'); // output > init.... salutationDidChange......console.log(p.get('salutation')); // output > hellop.set('salutation'); // output > salutationDidChange......
如果一个计算属性从来没有调用过get()
方法获取的其值,观察者就不会被触发,即使是计算属性的值发生变化了。你可以这么认为,观察者是根据调用get()
方法前后的值比较判断出计算属性值是否发生改变了。如果没调用过get()
之前的改变观察者认为是没有变化。通常我们不需要担心这个问题会影响到程序代码,因为几乎所有被观察的计算属性在触发前都会执行取值操作。如果你仍然担心观察者不会被触发,你可以在init()
方法了执行一次get
操作。这样足以保证你的观察在触发之前是执行过get操作的。
对于初学者来说,属性值的自动更新还是有点难以理解,到底它是怎么个更新法!!!先别急,先放一放,随着不断深入学习你就会了解到这个是多么强大的特性。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用在github项目上给我个star
吧。您的肯定对我来说是最大的动力!!
正如其他的框架一样,Ember
也有它特有的数据绑定方式,并且可以在任何一个对象上使用绑定。而然,数据绑定大多数情况都是使用在Ember
框架本身,对于开发者最好还是使用计算属性更为简单方便。
// 双向绑定Wife = Ember.Object.extend({ householdIncome: 800});var wife = Wife.create();Hasband = Ember.Object.extend({ // 使用 alias方法实现绑定 householdIncome: Ember.computed.alias('wife.householdIncome')});hasband = Hasband.create({ wife: wife});console.log('householdIncome = ' + hasband.get('householdIncome')); // output > 800// 可以双向设置值// 在wife方设置值wife.set('householdIncome', 1000);console.log('householdIncome = ' + hasband.get('householdIncome')); // output > 1000// 在hasband方设置值hasband.set('householdIncome', 10);console.log('wife householdIncome = ' + wife.get('householdIncome'));
需要注意的是绑定并不会立刻更新对应的值,Ember
会等待直到程序代码完成运行完成并且是在同步改变之前,所以你可以多次改变计算属性的值。由于绑定是很短暂的所以也不需要担心开销问题。
单向绑定只会在一个方向上传播变化。相对双向绑定来说,单向绑定做了性能优化,对于双向绑定来说如果你只是在一个方向上设置关联其实就是一个单向绑定。
var user = Ember.Object.create({ fullName: 'Kara Gates'});UserComponent = Ember.Component.extend({ userName: Ember.computed.oneWay('user.fullName')});userComponent = UserComponent.create({ user: user});console.log('fullName = ' + user.get('fullName'));// 从user可以设置user.set('fullName', "krang Gates");console.log('component>> ' + userComponent.get('userName'));// UserComponent 设置值,user并不能获取,因为是单向的绑定userComponent.set('fullName', "ubuntuvim");console.log('user >>> ' + user.get('fullName'));
关于数据绑定的知识点不多,相对来说不是重点,毕竟对象之间的关联关系是越少、越简单越好。关联关系多了反而难以维护。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用在github项目上给我个star
吧。您的肯定对我来说是最大的动力!!
在Ember
中,枚举是包含多个子对象的对象,并且提供了丰富的API(Ember.Enumerable API)去获取所包含的子对象。Ember
的枚举都是基于原生的javascript
数组实现的,Ember
扩展了其中的很多接口。Ember
提供一个标准化接口处理枚举,并且允许开发者完全改变底层数据存储,而无需修改应用程序的数据访问代码。Ember
的Enumerable API
尽可能的遵照ECMAScript
规范。为了减少与其他库不兼容,Ember
允许你使用本地浏览器实现数组。
下面是一些重要而常用的API
列表;请注意左右两列的不同。
标准方法 | 可被观察方法 | 说明 |
pop | popObject | 该函数从从数组中删除最后项,并返回该删除项 |
push | pushObject | 新增元素 |
reverse | reverseObject | 颠倒数组元素 |
shift | shiftObject | 把数组的第一个元素从其中删除,并返回第一个元素的值 |
unshift | unshiftObject | 可向数组的开头添加一个或更多元素,并返回新的长度 |
详细文档请看:http://emberjs.com/api/classes/Ember.Enumerable.html
在列表上右侧的方法是Ember
重写标准的JavaScript
方法而得的,他们最大的不同之处是使用普通的方法(左边部分)操作的数组不会在你的应用程序中自动更新(不会触发观察者),而使用Ember
重写过的方法则可以触发观察者,只要你的数据有变化Ember
就可以观察到,并且更新到模板上。
遍历数组元素使用forEach
方法。
var arr = ['chen', 'ubuntuvm', '1527254027@qq.com', 'i2cao.xyz', 'ubuntuvim.xyz'];arr.forEach(function(item, index) { console.log(index+1 + ", " +item);});
// 获取头尾的元素,直接调用Ember封装好的firstObject和lastObject方法即可console.log('The firstItem is ' + arr.get('firstObject')); // output> chenconsole.log('The lastItem is ' + arr.get('lastObject')); //output> ubuntuvim.xyz
// map方法,转换数组,并且可以在回调函数里添加自己的逻辑// map方法会新建一个数组,并且返回被转换数组的元素var arrMap = arr.map(function(item) { return 'map: ' + item; // 增加自己的所需要的逻辑处理});arrMap.forEach(function(item, index) { console.log(item);});console.log('-----------------------------------------------');
// mapBy 方法:返回对象属性的集合,// 当你的数组元素是一个对象的时候,你可以根据对象的属性名获取对应值var obj1 = Ember.Object.create({ username: '123', age: 25}); var obj2 = Ember.Object.create({ username: 'name', age: 35});var obj3 = Ember.Object.create({ username: 'user', age: 40}); var obj4 = Ember.Object.create({ age: 40}); var arrObj = [obj1, obj2, obj3, obj4]; //对象数组var tmp = arrObj.mapBy('username'); // tmp.forEach(function(item, index) { console.log(index+1+", "+item);}); console.log('-----------------------------------------------');
// filter 过滤器方法,过滤普通数组元素// filter方法可以跟你指定的条件过滤掉不匹配的数据,比如下面的代码:过滤了元素大于4的元素var nums = [1, 2, 3, 4, 5];// 参数self值数组本身var numsTmp = nums.filter(function(item, index, self) { return item < 4;}); numsTmp.forEach(function(item, index) { console.log('item = ' + item); // 1, 2, 3});console.log('-----------------------------------------------');
filter
方法会返回所有判断为true
的元素,会把判断结果为false
或者undefined
的元素过滤掉。
// 如果你想根据对象的某个属性过滤数组你需要用filterBy方法,比如下面的代码根据isDone这个对象属性过滤var o1 = Ember.Object.create({ name: 'u1', isDone: true}); var o2 = Ember.Object.create({ name: 'u2', isDone: false}); var o3 = Ember.Object.create({ name: 'u3', isDone: true}); var o4 = Ember.Object.create({ name: 'u4', isDone: true}); var todos = [o1, o2, o3, o4];var isDoneArr = todos.filterBy('isDone', true); //会把o2过滤掉isDoneArr.forEach(function(item, index) { console.log('name = ' + item.get('name') + ', isDone = ' + item.get('isDone')); // console.log(item);}); console.log('-----------------------------------------------');
filter
和filterBy
不同的地方是前者可以自定义过滤逻辑,后者可以直接使用。
// every、some 方法// every 用于判断数组的所有元素是否符合条件,如果所有元素都符合指定的判断条件则返回true,否则返回false// some 用于判断数组的所有元素只要有一个元素符合条件就返回true,否则返回falsePerson = Ember.Object.extend({ name: null, isHappy: true});var people = [ Person.create({ name: 'chen', isHappy: true }), Person.create({ name: 'ubuntuvim', isHappy: false }), Person.create({ name: 'i2cao.xyz', isHappy: true }), Person.create({ name: '123', isHappy: false }), Person.create({ name: 'ibeginner.sinaapp.com', isHappy: false })];var every = people.every(function(person, index, self) { if (person.get('isHappy')) return true;});console.log('every = ' + every); var some = people.some(function(person, index, self) { if (person.get('isHappy')) return true;});console.log('some = ' + some);
// 与every、some类似的方法还有isEvery、isAny console.log('isEvery = ' + people.isEvery('isHappy', true)); // 全部都为true,返回结果才是trueconsole.log('isAny = ' + people.isAny('isHappy', true)); //只要有一个为true,返回结果就是true
上述方法的使用与普通JavaScript
提供的方法基本一致。学习难度不大……自己敲两边就懂了!
这些方法非常重要,请一定要学会如何使用!!!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用在github项目上给我个star
吧。您的肯定对我来说是最大的动力!!
本篇之前的6篇文章都是第一章的内容,这一章节主要介绍了Ember
的对象模型。其中最重要的是计算属性和枚举这2章,非常之重要,一定要好好掌握!
下一章节是第二章模板,Ember
应用使用的模板库是handlebar
(点我查看更多有关此模板的介绍),这个模板库功能强大,有丰富的标签,包括判断标签if
,if else
,以及遍历标签each
等等。
另外,从下一章开始,我们不再自己手动搭建Ember
项目,也不用手动引入Ember
库文件,而是使用官方提供的一个非常棒的构建工具——Ember CLI
,要使用这个构建工具首先安装并配置。下面两个地址是介绍安装与配置的(推荐第一个):
Ember CLI
是一个非常重要的构建工具,它可以为开发者创建文件并初始化部分固定的代码。它还可以运行、打包、测试Ember
应用。
下面使用这个工具创建一个新的Ember
项目chapter2_tempalte
。
ember new chapter2_tempalte
cd chapter2_template
ember server
如果项目创建成功你可以继续往下看,如果项目创建不成功请重试,因为后面的代码都基于这个项目来演示的!!!对于创建项目后得到的每个文件和目录请你看官网文档,上面会有非常详细的说明。为了方便懒人在此就简单介绍其中几个很重要的文件和目录:
目录 | 说明 |
app | 项目的主要代码都是放在这个目录下 |
app/controllers | 存放C(MVC)层(controller)的代码文件 |
app/helpers | 存放自定义的helper代码文件 |
app/models | 存放M(MVC)层(model)代码文件 |
app/routes | 存放项目路由设置代码文件 |
app/templates | 存放项目模板代码文件 |
bower_components | 存放使用bower命令安装的第三方插件库 |
bower.json | 保存使用bower命令安装的第三方库的配置 |
package.json | 保存使用npm命令安装的第三方库的配置 |
node_modules | 存放使用npm命令安装的第三方插件库 |
ember-cli-build.js | 设置构建规范,引入第三方库 |
dist | 存放编译打包后的项目文件,可以直接复制到服务器中运行 |
上述这些文件或者目录是后面开发过程经常会用到,相对其他目录和文件来说这些目录和文件是很重要的,只要你是使用ember new appName
命令生成的项目都会包括上述这些目录或者文件。其中最重要的就是app
目录下的文件、目录了,从app
里面的目录结果你就可以很清楚的看到这是个MVC
框架的项目。Ember
之所以能找到controller
对应的template
也是根据目录和文件的名称找到的,Ember
是有自己的一套命名规则的,如果你想了解更多有关信息请移步folder-layout。
搭好环境之后开始我们的Ember
之旅吧!!!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
Ember
采用handlebars
模板库作为应用的view
层。Handlebars
模板与普通的HTML
非常相似。但是相比普通的HTML
而言handlebars
提供了非常丰富的表达式。Ember
采用handlebars
模板并且扩展了很多功能,让你使用handlebars
就像使用HTML
一样简单。
在前一篇介绍了一个很重要的构建工具Ember CLI
,从本篇开始后面所创建的文件都是使用这个构建工具来创建,先进入到项目路径下再执行Ember CLI
命令。
创建一个模板命令ember g template application
由于这个模板在创建项目的时候就已经有了,所以会提示你是否覆盖原来的文件,你可以选择覆盖或者不覆盖都行。
<h1>Kittens</h1><p>Kittens are the cutest!!</p>
注意:代码中的第一句注释的内容表明了这个文件的位置已经文件名称,后面的代码片段也会采用这种方式标注。如果没有特别的说明第一句代码都是注释文件的路径及其名称。
上述就是一个模板,非常简单的模板,只有一个h1和p标签,当你保存这个文件的时候Ember CLI会自动帮你刷新页面,不需要你手动去刷新!此时你的浏览器页面应该会看到如下信息:
那么恭喜你,模板定义成功了,至于为什么执行http://localhost:4200就直接显示到这里等你慢慢学到controller
和route
的时候自然会明白,你就当application.hbs
是一个默认的首页,这样你应该明白了吧!
每一个模板都会有一个与之关联的controller
类、route
类、model
类(当然这些类是不是必须有的)。这就是模板能显示表达式的值的原因,你可以在controller
类中设置模板中表达式显示的值,就像java web
开发中在servlet
或者Action
调用request.setAttribute()
方法设置某个属性一样。比如下面的模板代码:
<h2 id="title">Welcome to Ember</h2>Hello, <strong>{{firstName}} {{lastName}}</strong>!<br>My email is <b>{{email}}</b>
下面我们创建一个controller。这次我们用Ember CLI的命令创建: ember generate controller application,这句命令表示会创建一个controller并且名称是application,然后我们会得到如下几个文件:
app/controllers/application.js
--controller
本身tests/unit/controllers/application-test.js
--controller
对应的单元测试文件打开你的文件目录,是不是可以在app/controllers
下面看到了!现在为了演示表达式我们在controller
里添加一些代码:
// app/controllers/application.jsimport Ember from 'ember';/** * Ember会根据命名规则自动找到templates/application.hbs这个模板, * @type {hash} 需要设置的hash对象 */export default Ember.Controller.extend({ // 设置两个属性 firstName: 'chen', lastName: 'ubuntuvim', email: 'chendequanroob@gmail.com'});
然后修改显示的模板如下:
<h2 id="title">Welcome to Ember</h2>Hello, <strong>{{firstName}} {{lastName}}</strong>!<br>My email is <b>{{email}}</b>
保存,然后页面会自动刷新(Ember CLI
会自动检测文件是否改变,然后重新编译项目),我们可以看到在controller
设置的值,可以直接在模板上显示了。
这个就是表达式的绑定,后面你会学习到更多更有趣也更复杂的handlebasr
表达式。随着应用程序的规模不断扩大,会有更多的模板和与之关联的控制器。并且有时候一个模板还可以对应这多个控制器。也就是说模板上表达式的值可能有多个controller
控制。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
handlebars
模板提供了与一般语言类似的条件表达式,比如if
、if……else……
。在介绍这些条件表达式之前,我们先做好演示的准备工作。首先我会使用Ember CLI
命令创建route
、template
,然后在创建的template
上编写handlebars
模板代码。先创建route
:ember generate route handlbars-conditions-exp-route
或者:ember generate route handlbarsConditionsExpRoute
这两个命令创建的文件名都是一样的。最后Ember CLI
会为我们自动创建2个主要的文件:app/templates/handlbars-conditions-exp-route.hbs
和 app/routes/handlbars-conditions-exp-route.js
注意:如果你使用的是驼峰式的名称Ember CLI
会根据Ember
的命名规范自动创建以中划线-
分隔的名称。为什么我是先使用命令创建route
而不是template
呢??因为你创建route
的同时Ember CLI
会自动给你创建一个对应的模板文件,如果你是先创建template
的话,你还需要手动再执行创建route
的命令,即你要执行2条命令(ember generate template handlbars-conditions-exp-route
和ember generate route handlbars-conditions-exp-route
)。
得到演示所需要的文件后回到正题,开始介绍handlebars
的条件判断表达式。为了演示先在route
文件添加模拟条件代码:
// app/routes/handlebars-condition-exp-route.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function () { return {name: 'i2cao.xyz', age: 25, isAtWork: false, isReading: false }; // return { enable: true }; } });
对于handlebars-condition-exp-route.js
这个文件的内容会在后面路由这一章详细介绍,你可以暂且当做是返回了一个对象到模板上。与EL
表达式非常类似,你可以用.
获取对象里的属性值(如:person.name
)。
if
表达式{{#if model}}Welcome back, <b>{{model.name}}</b> !{{/if}}
每个条件表达式都要以#
开头并且要有对应的关闭标签,但是对于if
标签来说不是必须要关闭标签的,这里简单举个例子:
<div class="{{if flag 'show' 'hide'}}">测试内容</div>
这个 运行的时候需要注意两个地方,一个是浏览器执行的 建议:创建之后的路由名字最好不要修改, 结果是输出:This is else block...因为 如果 说白了其实就是一个三目运算。不难理解。不过这个例子与第一点讲没有关闭标签的 上述就是if
标签相当于一个三元运算符
,只是省略了?
和:
,他会根据属性flag
的值判断是显示那个CSS类,如果flag
的值不是false
,不是[]
空数组,也不是null
,也不是undefined
则div
会加上CSS类show
,模板编译之后的标签为hide
模板编译之后的标签为if
判断。没别的难点。。。URL
。如果你也是使用驼峰式的命名方式(创建命名:ember generate route handlbarsConditionsExpRoute
),那你的URL
跟我的是一样的,反正你只要记得执行的URL
跟你创建的route
的名称是一致的。当然这个名字是可以修改的。在app/router.js
里面修改,在Router.map
里的代码也是Ember CLI
自动创建的。我们可以看到有一个this.route('handlebarsConditionsExpRoute');
这个就是你的路由的名称。ember
会根据默认的命名规范查找route
对应的template
,如果你修改了router.js
里的名字你需要同时修改app/routes
和 app/templates
里相对应的文件名。否则URL
上的路由无法找到对应的template
显示你的内容,在router.js
里配置的名字必须与app/routes
目录下的路由文件名字对应,模板的名字不一定要与路由配置名称对应,应该可以在route
类中指定渲染的模板是那个,这个后面的内容会讲到(不是重点内容,了解即可)。说明:可能你看到的我截图给你的有点差别,是因为我修改了主模板(app/index.html
)你可以在这个文件里添加自己喜欢的样式,你一在app/index.html
引入你的样式,或者在ember-cli-build.js
引入第三方样式都可以,自定义的样式放在public/assets/
下,如果没有目录可以自行手动创建,在此就不再赘述,这个不是本文的重点。2,
if……else……
表达式{{#if model.isAtWork}}Ship that code..<br>{{else if model.isReading}}You can finish War and Peace eventually..<br>{{else}}This is else block...{{/if}}
isAtWork
和isReading
都是false
。读者可以自己修改app/routes/handlebars-condition-exp-route.js
里面对应的值然后查看输出结果。3,
unless
表达式unless
表达式类似于非操作,当model.isReading
值为false
的时候会输出表达式里面的内容。{{#unless model.isReading}}unless.....{{/unless}}
isReading
值为false
会输出unless…
否则不进入表达式内。4,在HTML标签内使用表达式
handlebars
表达式可以直接在嵌入到HTML
标签内。<span class="{{if" enable="" 'enable'="" 'disable'}}="">enable or disable</span>
if
例子一致,就当是复习吧=^=。handlebars
中最常用的几个条件表达式,自己作为小例子演示一遍肯定懂了,对于有点惊讶的开发者甚至看一遍即可。非常的简单,可能后面还会有其他的条件判断的表达式,后续会补上。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
采用与上一篇文章一样的方法,使用 ember generate route handlebars-each
命令创建了一个路由文件和一个对应的模板文件。这一篇将为你介绍遍历标签,数组的遍历几乎在任何的常用的开发语言中都能看到,也是使用非常广泛的一个功能。下面我将为大家介绍handlebars
的遍历标签,其使用方式与EL表达式几乎是一样的。我想你看一遍下来肯定也能明白了……废话少说,下面直接上演示代码吧!!
// app/routes/handlebars.jsimport Ember from 'ember';/** * 定义一个用于测试的对象数组 */export default Ember.Route.extend({ // 重写model回调函数,初始化测试数据 model: function() { return [ Ember.Object.create({ name: 'chen', age: 25}), Ember.Object.create({ name: 'i2cao.xyz', age: 0.2}), Ember.Object.create({ name: 'ibeginner.sinaapp.com', age: 1}), Ember.Object.create({ name: 'ubuntuvim.xyz', age: 3}) ]; }});
如上述所示,在route
类里构建了一个用于测试的对象数组,每个对象有2个属性(name
,age
)。下面是显示数据的模板:
{{! 遍历在route里设置的对象数组 }}<ul> {{#each model as |item|}} <li>Hello everyone, My name is {{item.name}} and {{item.age}} year old.</li> {{/each}}</ul>
有没有似曾相似的感觉呢!!跟EL表达式的forEach
标签几乎是一样的。不出意外你应该可以看到如下的结果。
提醒:记得此时运行的URL是刚刚新建的route。操作数组的时候注意使用官方建议的方法(如,新增使用pushObject
而不是push
),请看前面的文章。
有些情况我们可能需要获取数组的下标,比如有些时候可能会下标作为数据的序号。请看下面的演示:
{{! 遍历在route里设置的对象数组 }}
在实际的开发过程中你很有可能需要显示出对象数组的键或者值,如果你需要同时显示出对象的键和值你可以使用{{#each-in}}
标签。
注意:each-in
标签是Ember 2.0
才有的功能,之前的版本是无法使用这个标签的,如果是2.0一下的版本会报错:Uncaught Error: Assertion Failed: A helper named 'each-in' coulad not be found
准备工作:使用Ember CLI
生成一个component
,与此同时会生成一个对应的模板文件。ember generate component store-categories
执行上述命令得到下面的3个文件:
app/components/store-categories.jsapp/templates/components/store-categories.hbstests/integration/components/store-categories-test.js
然后在app/router.js
增加一个路由设置,在map
方法里添加this.route('store-categories');
;此时可以直接访问http://localhost:4200/store-categories;
http://guides.emberjs.com/v2.0.0/templates/displaying-the-keys-in-an-object/
// app/components/store-categories.jsimport Ember from 'ember';export default Ember.Component.extend({ // https://guides.emberjs.com/v2.4.0/components/the-component-lifecycle/ willRender: function() { // 设置一个对象到属性“categories”上,并且设置到categories属性上的对象结构是:key为字符串,value为数组 this.set('categories', { 'Bourbons': ['Bulleit', 'Four Roses', 'Woodford Reserve'], 'Ryes': ['WhistlePig', 'High West'] }); }));
willRender
方法在组件渲染的时候执行,更多有关组件的介绍会在后面章节——组件中介绍,想了解更多有关组件的介绍会在后面的文章中一一介绍,目前你暂且把组件当做是一个提取出来的公共HTML代码。
有了测试数据之后我们怎么去使用each-in
标签遍历出数组的键呢?
<ul> {{#each-in categories as |category products|}} <li>{{category}} <ol> {{#each products as |product|}} <li>{{product}}</li> {{/each}} </ol> </li> {{/each-in}}</ul>
上述模板代码中第一个位置参数category
就是迭代器的键,第二个位置参数product
就是键所对应的值。
为了显示效果,在application.hbs
中调用这个组件,组件的调用非常简单,直接使用{{组件名}}
方式调用。
{{store-categories}}
渲染后结果如下图:
{{each-in}}
表达式不会根据属性值变化而自动更新。上述示例中,如果你给属性categories
增加一个元素值,模板上显示的数据不会自动更新。为了演示这个特性在组件中增加一个触发属性变化的按钮,首先需要在组件类app/components/store-categories.js
中增加一个action
方法(有关action会在后面的章节介绍,暂时把他看做是一个普通的js函数),然后在app/templates/components/store-categories.hbs
中增加一个触发的按钮。
import Ember from 'ember';export default Ember.Component.extend({ // willRender方法在组件渲染的时候执行,更多有关组件的介绍会在后面章节——组件,中介绍 willRender: function() { // 设置一个对象到属性“categories”上,并且设置到categories属性上的对象结构是:key为字符串,value为数组 this.set('categories', { 'Bourbons': ['Bulleit', 'Four Roses', 'Woodford Reserve'], 'Ryes': ['WhistlePig', 'High West'] }); }, actions: { addCategory: function(category) { console.log('清空数据'); let categories = this.get('categories'); // console.log(categories); categories['Bourbons'] = []; // 手动执行重渲染方法更新dom元素,但是并没有达到预期效果 // 还不知道是什么原因 this.rerender(); } }});
<ul> {{#each-in categories as |category products|}} <li>{{category}} <ol> {{#each products as |product|}} <li>{{product}}</li> {{/each}} </ol> </li> {{/each-in}}</ul><button onclick={{action 'addCategory'}}>点击清空数据</button>
但是很遗憾,即使是手动调用了rerender
方法也没办法触发重渲染,界面显示的数据并没有发生变化。后续找到原因后再补上!!
空数组处理与表达式{{each}}
一样,同样是判断属性不是null
、undefined
、[]
就显示出数据,否则执行else
部分。
{{#each-in people as |name person|}} Hello, {{name}}! You are {{person.age}} years old.{{else}} Sorry, nobody is here.{{/each-in}}
可以参考上一篇的{{each}}
标签测试,这里不再赘述。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
简单讲属性绑定其实就是在HTML标签内(是在一个标签的””中使用)直接使用handlebars
表达式。可以直接用handlebars
表达式的值作为HTML标签中某个属性的值。
准备工作:ember generate route binding-element-attributes
<div id="logo"> <img src={{model.imgUrl}} alt='logo' /></div>
在对应的route:binding-element-attributes
里增加测试数据。
import Ember from 'ember';export default Ember.Route.extend({ model: function() { return { imgUrl: 'http://i1.tietuku.com/1f73778ea702c725.jpg' }; }});
运行之后模板会编译成如下代码:
<div id="logo"> <img alt="logo" src="http://i1.tietuku.com/1f73778ea702c725.jpg" rel="external nofollow" ></div>
可以看到{{model.imgUrl}}
会以字符串的形式绑定到src
属性上。
在开发过程中我们经常会根据某个值判断是否给某个标签增加CSS类,或者根据某个值决定按钮是否可用等等……那么ember是怎么做的呢??比如下面的代码演示的是checkbox
按钮根据绑定的属性isEnable
的值决定是否可用。
{{! 当isEnable为true时候,disabled为true,不可用;否则可用}}<input type='checkbox' disabled={{model.isEnable}}>
如果在route
里设置的值是true
那么渲染之后的HTML如下:
<input type="checkbox" disabled="">
否则
<input type="checkbox">
默认情况下,ember不会绑定到data-xxx
这一类属性上。比如下面的绑定结果就得不到你的预期。
{{! 绑定到data-xxx这种属性需要特殊设置}}{{#link-to 'photo' data-toggle='dropdown'}} link-to {{/link-to}}{{input type='text' data-toggle='tooltip' data-placement='bottom' title="Name"}}
模板渲染之后得到下面的HTML代码
<a id="ember455" href="/binding-element-attributes" class="ember-view active"> link-to </a><input id="ember470" title="Name" type="text" class="ember-view ember-text-field">
可以看到data-xxx
的属性都不见了!!!现在很多的前端框架都会用到data-xxx
这个属性,比如bootstrap
。那怎么办呢……你可以在view中指定对应的渲染组件Ember.LinkComponent
和Ember.TextField
(个人理解的)。执行命令得到view文件:ember generate view binding-element-attributes
,
在view中手动绑定,如下:
// app/views/binding-element-attributes.jsimport Ember from 'ember';export default Ember.View.extend({});// 下面是官方给的代码,但很明显看出来这种使用方式不是2.0版本的!!// 2.0版本的写法还在学习中,后续在补上,现在为了演示模板效果暂时这么写!毕竟本文的重点还是在模板属性的绑定上// 绑定inputEmber.TextField.reopen({ attributeBindings: ['data-toggle', 'data-placement']});// // 绑定link-toEmber.LinkComponent.reopen({ attributeBindings: ['data-toggle']});
渲染之后得到的结果符合预期。得到如下HTML代码
<a id="ember398" href="/binding-element-attributes" data-toggle="dropdown" class="ember-view active">link-to</a><input id="ember414" title="Name" type="text" data-toggle="tooltip" data-placement="bottom" class="ember-view ember-text-field">
可以看到data-xxx
的属性正常渲染到HTML上了。
本文介绍了几个常用的属性绑定方式,非常之实用!但是有点遗憾本人能力有限还没理解到最后一个实例在Ember2.0
版中是怎么使用的,现在的代码是根据个人理解把指定组件的代码放在view,官方教程给的也不是Ember2.0
版的,所以binding-element-attributes.js
这个文件的代码有点奇葩了……希望读者们不吝赐教!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
link-to
助手表达式渲染之后就是一个a
标签。而a
标签的href
属性的值是根据路由生成的,与路由的设置是息息相关的。并且每个设置的路由名称都是有着对应的关系的。为了演示效果,用命令生成了一个route
(或者手动创建文件)并获取测试数据。本文结合路由设置,随便串讲了一些路由方面的知识,如果你能看懂最好了,看不懂也不要紧后面会有一整章介绍路由。
// app/routers.jsimport Ember from 'ember';import config from './config/environment';var Router = Ember.Router.extend({ location: config.locationType });Router.map(function() { this.route('posts', function() { this.route('detail', {path: '/:post_id'}); //指定子路由,:post_id会自动转换为数据的id });});export default Router;
如上述代码,在posts
下增加一个子路由detail
。并且指定路由名为/:post_id
,:post_id
是一个动态字段,一般情况下默认为model
的id
属性。经过模板渲染之后会得到类似于posts/1
、posts/2
这种形式的路由。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls'); }});
使用Ember提供的方法直接从远程获取测试数据。测试数据的格式可以用浏览器直接打开上面的URL就可以看到。
<div class="container"> <div class="row"> <div class="col-md-10 col-xs-10"> <div style="margin-top: 70px;"> <ul class="list-group"> {{#each model as |item|}} <li class="list-group-item"> {{#link-to 'posts.detail' item}} {{item.title}} {{/link-to}} </li> {{/each}} </ul> </div> </div> </div></div>
直接用{{#each}}
遍历出所有的数据,并显示在界面上,由于数据比较多可能显示的比较慢,特别是页面刷新之后看到一片空白,请不要急着刷新页面,等待一下即可……。下图是结果的一部分:
我们查看页面的源代码,可以看到link-to
助手渲染之后的HTML代码,自动生成了URL,在router.js
里配置的post_id
渲染之后都被model
的id
属性值代替了。
如果你没有测试数据,你还可直接把link-to
助手的post_id
写死,可以直接把数据的id
值设置到link-to
助手上。在模板文件的ul
标签下一行增加如下代码,在link-to
助手中指定id
为1
:
<li class="list-group-item"> {{#link-to 'posts.detail' 1}}增加一条直接指定id的数据{{/link-to}} </li>
渲染之后的HTML代码如下:
<li class="list-group-item"><a id="ember404" href="/posts/1" class="ember-view">增加一条直接指定id的数据</a></li>
可以看到与前面使用动态数据渲染之后的href
的格式是一样的。如果你想设置某个a
标签是激活状态,你可以直接在标签上增加一个CSS类(class=”active”
)。
开发中,路由的路径经常不是2层的(post/1
)也有可能是多层次的(post/1/comment
、post/1/2
或者post/1/comment/2
等等形式。),如果是这种形式的URL在link-to
助手上又要怎么去定义呢?老样子,在演示模板之前还是需要先构建好测试数据以及修改对应的路由设置,此时的路由设置是多层的,因为link-to
助手渲染之后得到的href属性值就是根据路由生成的!!!这个必须要记得……
// app/routers.jsimport Ember from 'ember';import config from './config/environment';var Router = Ember.Router.extend({ location: config.locationType });Router.map(function() { // this.route('handlebarsConditionsExpRoute'); // this.route('handlebars-each'); // this.route('store-categories'); // this.route('binding-element-attributes'); // link-to实例理由配置 // this.route('link-to-helper-example', function() { // this.route('edit', {path: '/:link-to-helper-example_id'}); // }); this.route('posts', function() { //指定子路由,:post_id会自动转换为数据的id this.route('detail', {path: '/:post_id'}, function() { //增加一个comments路由 this.route('comments'); // 第二个子路由comment,并且路由是个动态字段comment_id this.route('comment', {path: '/:comment_id'}); }); }); });export default Router;
如果是上述配置,渲染之后得到的路由格式posts/detail/comments
。由于获取远程数据比较慢直接注释掉posts.js
里的model
回调方法,就直接使用写死id的方式。注意:上述配置中,在路由detail
下有2个子路由,一个是comments
,一个是comment
,并且这个是一个动态段。由此模板渲染之后应该是有2种形式的URL。一种是posts.detail.comments
(posts/1/comments
),另一种是posts.detail.comment
(posts/1/2
)。如果能理解这个那route
嵌套层次再多应该也能看明白了!
<div class="container"> <div class="row"> <div class="col-md-10 col-xs-10"> <div style="margin-top: 70px;"> <ul class="list-group"> <li class="list-group-item"> {{#link-to 'posts.detail.comments' 1 class='active'}} posts.detail.comments(posts/1/comments)形式 {{/link-to}} </li> <li class="list-group-item"> {{#link-to 'posts.detail.comment' 1 2 class='active'}} posts.detail.comment(posts/1/2)形式 {{/link-to}} </li> </ul> </div> </div> </div></div>
渲染之后的结果如下:
如果是动态段的一般都是model
的id
代替,如果不是动态段的直接显示配置的路由名称。
上面演示了多个子路由的情况,下面接着介绍一个路由有多个层次,并且是有个多个动态段和非动态段组成的情况。首先修改路由配置,把comments
设置为detail
的子路由。并且在comments
下在设置一个动态段的子路由comment_id
。
// app/routers.jsimport Ember from 'ember';import config from './config/environment';var Router = Ember.Router.extend({ location: config.locationType });Router.map(function() { this.route('posts', function() { //指定子路由,:post_id会自动转换为数据的id this.route('detail', {path: '/:post_id'}, function() { //增加一个comments路由 this.route('comments', function() { // 在comments下面再增加一个子路由comment,并且路由是个动态字段comment_id this.route('comment', {path: '/:comment_id'}); }); }); });});export default Router;
模板使用路由的方式posts.detail.comments.comment
。正常情况应该生成类似posts/1/comments/2
这种格式的URL。
<div class="container"> <div class="row"> <div class="col-md-10 col-xs-10"> <div style="margin-top: 70px;"> <ul class="list-group"> <li class="list-group-item"> {{#link-to 'posts.detail.comments.comment' 1 2 class='active'}} posts.detail.comments.comment(posts/1/comments/2)形式 {{/link-to}} </li> </ul> </div> </div> </div></div>
渲染之后得到的HTML如下:
<ul class="list-group"> <li class="list-group-item"> <a id="ember473" href="/posts/1/comments/2" class="active ember-view"> posts.detail.comments.comment(posts/1/comments/2)形式 </a> </li> </ul>
结果正如我们预想的组成了4层路由(/posts/1/comment/2
)。补充内容。对于上述第二点多层路由嵌套的情况你还可以使用下面的方式设置路由和模板,并且可用同时设置了/posts/1/comments
和/posts/1/comments/2
。
this.route('posts', function() { //指定子路由,:post_id会自动转换为数据的id this.route('detail', {path: '/:post_id'}, function() { //增加一个comments路由 this.route('comments'); // 路由调用:posts.detail.comment // 注意区分与前面的设置方式,detai渲染之后会被/:post_id替换,comment渲染之后直接被comments/:comment_id替换了, //会得到如posts/1/comments/2这种形式的URL this.route('comment', {path: 'comments/:comment_id'}); }); });
<div class="container"> <div class="row"> <div class="col-md-10 col-xs-10"> <div style="margin-top: 70px;"> <ul class="list-group"> <li class="list-group-item"> {{#link-to 'posts.detail.comments' 1 class='active'}} posts.detail.comments(/posts/1/comments)形式 {{/link-to}} </li> <li class="list-group-item"> {{#link-to 'posts.detail.comment' 1 2 class='active'}} posts.detail.comments.comment(posts/1/comments/2)形式 {{/link-to}} </li> </ul> </div> </div> </div></div>
渲染之后的结果如下:
两种方式定义的路由渲染的结果是一样的,看个人喜欢了,定义方式也是非常灵活的。第一种定义方式看着比较清晰,看代码就知道是一层层的。但是需要写更多代码。第二种定义方式更加简洁,不过看不出嵌套的层次。对于上述route的设置如果还不能理解也不要紧,后面会有一整章是介绍路由的,然而你能结合link-to助手理解了路由设置对于后面route章节的学习是非常有帮助的。
handlebars
允许你直接在link-to
助手增加额外的属性,经过模板渲染之后a
标签就有了增加的额外属性了。比如你可用为a
标签增加CSS的class
。
{{link-to "show text info" 'posts.detail' 1 class="btn btn-primary"}}{{#link-to "posts.detail" 1 class="btn btn-primary"}}show text info{{/link-to}}
上述两种写法都是可以的,渲染的结果也是一样的。渲染之后的HTML为:
<a id="ember434" href="/posts/1" class="btn btn-primary ember-view">show text info</a>
注意:上述两种方式的写法所设置的参数的位置不能调换。但是官网没有看到这方面的说明,有可能是我的演示实例的问题,如果读者你的可用欢迎给我留言。第一种方式,显示的文字必须放在最前面,并且中间的参数是路由设置,最有一个参数是额外的属性设置,如果你还要其他的属性需要设置仍然只能放在最后面。第二章方式的参数位置也是有要求的,第一个参数必须是路由设置,后面的参数设置额外的属性。对于渲染之后的HTML代码中出现标签id
为ember
,或者ember-xxx
,这些属性都是Ember默认生成的,我们可以暂时不用理它。综合,本来这篇是讲解link-to
的,但是由于涉及到了route
的配置就顺便讲讲,有点难度,主要在路由的嵌套这个知识点上,但是对于后面的route
这一章的学习是很有帮助的,route
的设置几乎都是为URL设置的。这两者是紧密关联的!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
在Ember中路由和模板的执行都是有一定顺序的,它们的顺序为:主路由->子路由1->子路由2->子路由3->……。模板渲染的顺序与路由执行顺序刚好相反,从最后一个模板开始解析渲染。
注意:模板的渲染是在所有路由执行完之后,从最后一个模板开始。关于这一点下面的代码会演示验证,官网教程有介绍,点击查看。
比如有一路由格式为application/posts/detail/comments/comment
,此时路由执行的顺序为:application/posts
-> detail
-> comments
-> comment
,application
是项目默认的路由,用户自定义的所有路由都是application
的子路由(默认情况下),相对应的模板也是这样,所有用户自定义的模板都是application.hbs
的子模板。如果你要修改模板的渲染层次你可以在route
中重写renderTemplate
回调函数,在函数内使用render
方法指定要渲染的模板(如:render('other')
,渲染到other
这个模板上)更多有关信息请查看这里。并且它们对应的文件模板结构如下图:
路由与模板是相对应的,所以模板的目录结构与路由的目录结构是一致的。你有两种方式构建上述目录:
comment.js
使用命令:ember generate route posts/detail/comments/comment
,Ember CLI会自动为我们创建目录和文件。创建好目录结构之后我们添加一些代码到每个文件。运行项目之后你就会一目了然了……。下面我按前面讲的路由执行顺序分别列出每个文件的内容。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { console.log('running in posts...'); return { id: 1, routeName: 'The route is posts'}; // return Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls'); } });
import Ember from 'ember';export default Ember.Route.extend({ model: function(params) { console.log('params id = ' + params.post_id); console.log('running in detail....'); return { id: 1, routeName: 'The route is detail..' }; }});
// app/routes/posts/detail.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { console.log('params id = ' + params.post_id); console.log('running in detail....'); return { id: 1, routeName: 'The route is detail..' }; }});
// app/routes/posts/detail/comments.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { console.log('running in comments...'); return { id: 1, routName: 'The route is comments....'}; }});
// app/routes/posts/detail/comments/comment.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { console.log('params id = ' + params.post_id); console.log('running in comment...'); return { id: 1, routeName: 'The route is comment...'}; }});
下面是模板各个文件的内容。其列出才顺序与路由的顺序一致。
{{model.routeName}} >> {{outlet}}
{{model.routeName}} >> {{outlet}}
{{model.routeName}} >> {{outlet}}
{{model.routeName}} >> {{outlet}}
下图是路由执行的顺序,并且在执行的过程中渲染路由对应的模板。
从上图中可用清楚的看到当你运行一个URL时,与URL相关的路由是怎么执行的。
application
),此时进入到路由的model
回调方法,并且返回了一个对象{ id: 1, routeName: 'The route is application...' }
,执行完回调之后继续转到子路由执行直到最后一个路由执行完毕,所有的路由执行完毕之后开始渲染页面。detail.js
和comments.js
。在代码中加入一个模拟休眠的操作。// app/routes/posts/detail.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function(params) {console.log('params id = ' + params.post_id);console.log('running in detail....');
// 执行一个循环,模拟休眠for (var i = 0; i < 10000000000; i++) {
} console.log('The comment route executed...');
return { id: 1, routeName: 'The route is detail..' };}});
```javascript// app/routes/posts/detail/comments.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { console.log('params id = ' + params.post_id); console.log('running in comment...'); // 执行一个循环,模拟休眠 for (var i = 0; i < 10000000000; i++) { } return { id: 1, routeName: 'The route is comment...'}; }});
刷新页面,注意查看控制台输出信息和页面显示的内容。新开一个窗口,执行URL:http://localhost:4200/posts/2/comments。
控制台输出到这里时处理等待(执行for
循环),此时已经执行了两个路由application
和posts
,并且正在执行detail
,但是页面是空白的,没有任何HTML元素。
在detail
路由执行完成之后转到路由comments
。然后执行到for
循环模拟休眠,此时页面仍然是没有任何HTML元素。然后等到所有route
执行完毕之后,界面才显示model
回调里设置的信息。
每个子路由设置的信息都会渲染到最近一个父路由对应模板的{{outlet}}
上面。
comment
得到的内如为:“comment
渲染完成”comment
最近的父模板comments
得到的内容为:“comment
渲染完成 comments
渲染完成”comments
最近的父模板detail
得到的内容为:“comment
渲染完成 comments
渲染完成 detail
渲染完成”detail
最近的父模板posts
得到的内容为:“comment
渲染完成 comments
渲染完成 detail
渲染完成 posts
渲染完成”posts
最近的父模板application
得到的内容为:“comment
渲染完成 comments
渲染完成 detail
渲染完成 posts
渲染完成 application
渲染完成”只要记住一句话:子模板的都会渲染到父模板的{{outlet}}
上,最终所有的模板都会被渲染到application
的{{outlet}}
上。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
action
助手所现实的功能与javascript
里的事件是相似的,都是通过用户点击元素触发定义在元素上的事件。Ember的action助手还允许你传递参数到对应的controller
、component
类,在controller
或者component
上处理事件的逻辑。准备工作,我们使用Ember CLI命令创建一个名称为myaction
的controller
和同名的route
,如果你不知道怎么使用Ember CLI请看前面的文章Ember.js 入门指南之七第一章对象模型小结,这篇文件讲解了怎么使用Ember CLI构建一个简单的Ember项目。
// apap/routes/myaction.jsimport Ember from 'ember';export default Ember.Route.extend({ // 返回测试数据到页面 model: function() { return { id:1, title: 'ACTIONS', body: "Your app will often need a way to let users interact with controls that change application state. For example, imagine that you have a template that shows a blog title, and supports expanding the post to show the body.If you add the {{action}} helper to an HTML element, when a user clicks the element, the named event will be sent to the template's corresponding component or controller." }; }});
重写model
回调,直接返回一个对象数据。
<h2 {{action ' showDetailInfo '}} style="cursor: pointer;">{{model.title}}</h2>{{#if isShowingBody}}<p>{{model.body}}</p>{{/if}}
默认下只显示文章的标题,当用户点击标题的时候触发事件toggleBody
显示文章的详细信息。
// app/controllers/myaction.jsimport Ember from 'ember';export default Ember.Controller.extend({ // 控制页面文章详细内容是否显示 isShowingBody: false, actions: { showDetailInfo: function() { // toggleProperty方法直接把isShowingBody设置为相反值 // toggleProperty方法详情:http://devdocs.io/ember/classes/ember.observable#method_toggleProperty this.toggleProperty('isShowingBody'); } }});
对于controller
的处理逻辑你还可以直接编写触发的判断。
actions: { showDetailInfo: function() { // toggleProperty方法直接把isShowingBody设置为相反值 // toggleProperty方法详情:http://devdocs.io/ember/classes/ember.observable#method_toggleProperty // this.toggleProperty('isShowingBody'); // 变量作用域问题 var isShowingBody = this.get('isShowingBody'); if (isShowingBody) { this.set('isShowingBody', false); } else { this.set('isShowingBody', true); } } }
如果你不使用toggleProperty
方法改变isShowingBody
的值,你也可用直接编写代码修改它的值。最后执行URL:http://localhost:4200/myaction,默认情况下页面上是不显示文章的详细信息的,当你点击标题则会触发事件,显示详细信息,下面2个图片分别展示的是默认情况和点击标题之后。当我们再次点击标题,详细内容又变为隐藏。
通过上述的小例子可以看到action
助手使用起来也是非常简单的。主要注意下模板上的action
所指定的事件名称要与controller
里的方法名字一致。
就像调用javascript
的方法一样,你也可以为action
助手增加必要的参数。只要在action
名字后面接上你的参数即可。
<p><button {{action 'hitMe' model}}>点击我吧</button></p>
对应的在controller
增加处理的方法selected
。在此方法内打印获取到的参数值。
// app/controllers/myaction.jsimport Ember from 'ember';export default Ember.Controller.extend({ // 控制页面文章详细内容是否显示 isShowingBody: false, actions: { showDetailInfo: function() { // ……同上面的例子 }, hitMe: function(model) { // 参数的名字可以任意 console.log('The title is ' + model.title); console.log('The body is ' + model.body); } }});
Ember规定我们编写的动作处理的方法都是放在actions
这个哈希内。哈希的键就是方法名。在controller
方法上的参数名不要求与模板上传递的名字一致,你可以任意定义。比如方法hitMe
的参数model
你也可以使用m
作为hitMe
方法的参数。
当用户点击按钮“点击我吧”就会触发方法hitMe
,然后执行controller
的同名方法,最后你可以在浏览器的console
下看到如下的打印信息。
看到这些打印结果很好的说明了获取的参数是正确的。
默认情况下action
触发的是click
事件,你可以指定其他事件,比如键盘按下事件keypress
。事件的名字与javascript
提供的名字是相似的,唯一不同的是Ember所识别是事件名字如果是由不同单词组成的需要用中划线分隔,比如keypress
事件在Ember模板中你需要写成key-press
。注意:你指定事件的时候要把事件的名字作为on
的属性。比如on='key-press'
。
<a href="#/myaction" {{action 'triggerMe' on="mouse-over"}}>鼠标移到我身上触发</a>
triggerMe: function() { console.log('触发mouseover事件。。。。');}
action
触发事件的辅助按键甚至你还可以指定按下键盘某个键后点击才触发action
所指定的事件,比如按下键盘的Alt
再点击才会触发事件。使用allowedkeys
属性指定按下的是那个键。
<br><br><button {{action 'pressALTKeyTiggerMe' allowedkeys='alt'}}>按下Alt点击触发我</button>
在action
助手内使用属性preventDefault=false
可以禁止标签的默认行为,比如下面的a标签,如果action
助手内没有定义这个属性那么你点击链接时只会执行执行的action
动作,a
标签默认的行为不会被触发。
<a href="http://www.baidu.com" rel="external nofollow" target="_blank" target="_blank" {{action "showDetailInfo" preventDefault=false}}>点我跳转</a>
controller
handlebars
的action
助手真的是非常强大,你甚至可以把触发的事件作为action
的参数直接传递到controller
。不过你需要把action
助手放在javascript
的事件里。比如下面的代码当失去焦点时触发,并且通过action
指定的dandDidChange
把触发的事件blur
传递到controller
。
失去焦点时候触发<input type="text" value={{textValue}} onblur={{action 'bandDidChange'}} />
// app/controllers/myaction.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { bandDidChange: function(event) { console.log('event = ' + event); } } });
从控制台输出结果看出来event
的值是一个对象并且是一个focus
事件。但是如果你在action
助手内增加一个属性value='target.value'
(别写错只能是target.value
)之后,传递到controller
的则是输入框本身的内容。不再是事件对象本身。
<input type="text" value={{textValue}} onblur={{action 'bandDidChange' value="target.value"}} />
这个比较有意思,实现的功能与前面的参数传递类似的。
action
助手用在非点击元素上`action`助手可以用在任何的`DOM`元素上,不仅仅是用在能点击的元素上(比如`a`、`button`),但是用在其他非点击的元素上默认情况下是不可用的,也就是说点击也是无效的。比如用在`div`标签上,但是你点击`div`元素是没有反应的。如果你需要让`div`元素也能触发单击事件你需要给元素添加一个CSS类'cursor:pointer;`。
总的来说Ember的action
助手与普通的javascript的事件是差不多的。用法基本上与javascript的事件相似。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
Ember提供的表单元素都是经过封装的,封装成了view
组件。经过解析渲染之后就会生成普通的HTML标签。更多详细信息你可以查看他们的实现源码:Ember.TextField、Ember.Chechbox、Ember.TextArea。
按照惯例,先创建一个route
:ember generate route form-helper
。
input
助手{{! //app/templates/form-helper.hbs }}{{input name="username" placeholder="your name"}}
其中可以使用在input
助手上的属性有很多,包括readonly
、value
、disabled
、name
等等,更多有关的属性介绍请移步官网介绍。
注意:对于使用在input
助手上的属性是不是使用双引号括住是有区别的。比如value='helloworld'
和value=helloworld
渲染之后的结果是不一样的,第一种写法是直接把"helloworld"这个字符串赋值设置到value
上,第二种写法是从上下文获取变量helloworld的值再设置到value
上,通常是在controller
或者route
设置的值。看下面2行代码的演示结果:
{{input name="username" placeholder="your name" value="model.helloworld"}}<br><br>{{input name="username" placeholder="your name" value=model.helloworld}}
修改对应的route
类,重写model
回调,返回一个字符串;或者你可以在模板对应的controller
类设置。比如下面的第二段代码(使用命令ember generate controller form-helper
得到模板对应的controller
类。)。
// app/routes/form-helper.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return { helloworld: 'The value from route...' } }});
在controller
类初始化测试数据。
// app/controllers/form-helper.jsimport Ember from 'ember';export default Ember.Controller.extend({ helloworld: 'The value from route...'});
对应的,如果你使用的是controller
初始化测试数据,那么你的模板获取数据的方式就要稍微修改下。需要去掉前缀model.
。controller
不需要在回调中初始化测试数据,可用直接定义成controller
的属性。
{{input name="username" placeholder="your name" value=helloworld}}
input
助手上指定触发事件你可以想想下,我们平常写过的javascript代码,不是可用直接在input
输入框上使用javascript的函数,同理的,input
助手上可以使用javascript函数,不过使用方式有点差别,请看下面示例。比如按enter
键触发指定的事件、失去焦点触发事件等等。首先编写input
输入框,获取input
输入框的值有点不按常理=^=。在controller
类获取input
输入框的值是通过不用双引号的value
属性获取的。
按enter键触发{{input value=getValueKey enter="getInputValue" name=getByName placeholder="请输入测试的内容"}}
// app/controllers/form-helper.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { getInputValue: function() { var v = this.get('getValueKey'); console.log('v = ' + v); var v2 = this.get('getByName'); console.log('v2 = ' + v2); } }});
输入测试内容后按enter
键。
最后的输出结果有那么一点点意外。v
的值是正确的,v2
却是undefined
。可见在controller
层获取页面的值是通过value
这个属性而不是name
这个属性。跟我们平常HTML的input
有点不一样了!!这个需要注意下。
checkbook
助手checkbox
这个表单元素也是经过Ember封装了,作为一个组件使用。使用过程需要注意的问题与前面的input
是一样的,属性是不是使用双引号所起的作用是不一样的。
checkbox{{input type="checkbox" checked=isChecked }}
你可以在controller
增加一个属性isChecked
并设置为true
,checkbox
将默认为选中。
// app/controllers/form-helper.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { // …… }, isChecked: true});
textarea
助手{{textarea value=key cols="80" rows="3" enter="getValueByV"}}
同样的也是通过value
属性获取输入的值。
本篇简单的介绍了常用的表单元素,使用的方式比较简单,唯一需要注意的是获取的输入框输入值的方式与平常使用的HTML表单元素有点差别。其他的基本上与普通的HTML表单元素没什么差别。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
Ember不仅提供了专门用于调试Ember程序的谷歌、火狐浏览器插件Ember Inspector( 安装插件可能需要翻墙,如果你也是一个程序员我想翻墙对于你来说应该不是什么难事!!!),还提供了用于调试的helper
。按照惯例,先做好准备工作,分别执行Ember CLI命令创建controller
、route
和模板:
ember generate controller dev-helperember generate route dev-helper
{{log}}
{{log}}
可以把从controller
、route
类传递到页面上的值以日志的形式直接输出在浏览器的控制台上。下面代码在controller
类添加测试数据。
// app/controllers/dev-helper.jsimport Ember from 'ember';export default Ember.Controller.extend({ testName: 'This is a testvalue...'});
我们可以在模板上直接显示字符串testName
的值,也可以使用{{log}}
助手以日志形式输出在控制台。当然你也可以直接使用{{log 'xxx'}}
在控制台打印"xxxx"。第二点断点助手的示例中将为你演示{{log 'xxx'}}
用法。
直接显示在页面上:{{testName}}{{log testName}}
运行http://localhost:4200/dev-helper之后我们可以在页面上看到字符串testName
的值。打开谷歌或者火狐的控制台(console标签下)可以看到也打印的字符的值。比较简单我就不再截图了……
{{debugger}}
当你需要调试的时候,你可以在模板上需要添加断点的地方添加这个助手,运行的时候会自动停在添加这个助手的地方。
{{log '这句话在断点前面'}}{{debugger}}<br>{{log '这句话在断点后面'}}
不出意外程序会停在有{{debugger}}
这一行。控制台应该会打印“这句话在断点前面”。然后通过点击下一步跳过断点,然后继续打印“这句话在断点后面”。
运行结果不好截图,请读者自己试试吧!!!
当你使用了{{debugger}}
,并且程序停止进入debug状态的时候,你可以直接在浏览器控制台的命令行输入get('key')
来获取controller
设置的值。
在箭头所指的位置输入get('testName')
,然后按enter
键执行。会得到如下结果:
可以看到正确的获取到了前面在controller
类里设置的值。如果你不是在调试模式下输入get('testName')
那么会提示如下错误。
你还可以在遍历助手{{each}}
中使用{{debugger}}
,点击一次“下一步”就会执行一次循环。
首先重写route
类的model
回调,在里面添加测试数据。
// app/routes/dev-helper.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return [ { id: 1, name: 'chen', age: 25 }, { id: 2, name: 'ibeginner.sinaapp.com', age: 2 } ]; }});
在模板的each
助手中使用{{debugger}}
助手。
{{#each model as |item|}} {{debugger}} <li>item</li>{{/each}}
运行,浏览器自动进入debug模式(如果不能自动进入debug模式可以手动按F12
进入debug)。此时你可以在浏览器控制台命令输入get('item.name')
来获取本次循环对象的属性值。然后你几点“下一步”或者按F8
,程序自动进入到下一次循环,然后你再输入get('item.name')
,此时得到的是本次循环对象属性值。然后点击下一步或者按F8进入第三次循环,由于route
类设置返回的数组只有2个元素,第三次已经没有元素。所以这次会自动退出debug模式。如果运行正常你可会得到下图所示的输出信息。
在调试状态下你还可以直接在浏览器控制台命令行输入context
获取上下文信息。会输出本页面所包含的所有类和属性。
上述介绍的就是Ember提供的调试助手的所有使用方法。在你开发Ember应用的时候应该是很有用的,特别是在each
循环遍历的时候。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
本篇主要介绍格式转换、自定义helper
、自定义helper
参数、状态helper
、HTML标签转义这几个方面的东西。
按照文章惯例先准备好测试所需要的数据、文件。仍然是使用Ember CLI命令,这次我们创建的是helper
、controller
、route
(创建route
会自动创建template
)。
ember generate helper my-helperember generate controller tools-helperember generate route tools-helper
helper
自定义助手非常简答直接使用Ember CLI命令生成就可以了。当然你也可以手动创建,自定义的助手都是放在app/helpers
目录下。Ember会根据模板上使用的助手自动到这个目录查找。定义了helper
之后你就可以直接在模板上使用。
my-helper: {{my-helper}}
程序没有报错,但是什么也没有显示。是的什么也没有显示。没有显示就对了。因为我们对于刚刚创建的app/helpers/my-helper.js
没有做任何的修改。你可以看这个文件的代码。直接返回了params
,目前来说这个参数是空的。修改这个文件,直接返回一个字符串。
// app/helpers/my-helper.jsimport Ember from 'ember';export function myHelper(params/*, hash*/) { return "hello world!";}export default Ember.Helper.helper(myHelper);
此时可以在页面上看到直接打印了“hello world!”这个字符串。这就是一个最简单的自定义helper
,不过这么简单helper
显然是没啥用的。Ember的作者肯定不会这么傻的,接着下面介绍helper
的参数。注意:使用模板的名字跟文件名是一致的。不同单词使用-
分隔,虽然这个命名规则不是强制性的但是Ember建议你这么做,Ember会自动根据helper
的名字找到对应的自定义的helper
,然后执行helper
里名字为myHelper
(名字是文件名的驼峰式命名)的方法,在这个方法里你可以实现你需要的逻辑。这些工作Ember自动帮你做了,不需要你编写解析的代码。
helper
无名参数上面的代码定义了一个最简单的helper,不过没啥用,Ember允许在自定义的helper上添加自定义的参数。
my-helper-param: {{my-helper 'chen' 'ubuntuvim'}}
在这个自定义的helper
中增加了两个参数,既然有了参数那么又有什么用呢?当然是有用的,你可以在自定义的helper
中获取参数,获取模板的参数有两种方式。
写法一
// app/helpers/my-helper.jsimport Ember from 'ember';export function myHelper(params/*, hash*/) { // 获取模板上的参数 var p1 = params[0]; var p2 = params[1]; console.log('p1 = ' + p1 + ", p2 = " + p2); return p1 + " " + p2;}export default Ember.Helper.helper(myHelper);
写法二
// app/helpers/my-helper.jsimport Ember from 'ember';export function myHelper([arg1, arg2]) { console.log('p1 = ' + arg1 + ", p2 = " + arg2); return arg1 + " " + arg2;}export default Ember.Helper.helper(myHelper);
参数很多的情况使用第一种方式用循环获取参数比较方便,参数少情况第二种方式更加简便,直接使用!
注意:参数的顺序与模板传入的顺序一致。
页面刷新之后可以在页面或者浏览器控制台看到在helper
上设置的参数值了吧!!如果你的程序没有错误在浏览器上你也会得到下图的结果:
第一行因为在模板上没有传入参数所以是undefined
,第二行传入了参数,并且直接从helper
返回显示。
helper
命名参数上一点演示了在模板中传递无名的参数,这一小节讲为你介绍有名字的参数。
my-helper-named-param: {{my-helper firstName='chen' lastName='ubuntuvim'}}
相比于第一种使用方式给每个参数增加了参数名。那么helper
处理类有要怎么去获取这些参数呢?
// app/helpers/my-helper.jsimport Ember from 'ember';// 对于命名参数使用namedArgs获取export function myHelper(params, namedArgs) { console.log('namedArgs = ' + namedArgs); console.log('params = ' + params); console.log('========================='); return namedArgs.firstName + ", " + namedArgs.lastName; }export default Ember.Helper.helper(myHelper);
获取命名参数使用namedArgs
,其实你也可以按照前面的方法使用params
获取参数值。你在第一行打印语句上打上断点,是浏览器进入debug模式,但不执行,你会发现params
一开始是有值namedArgs
没有值,但是执行到最后正好相反,params
的值被置空了,namedArgs
却有了模板设置的值,你可以猜想下,Ember可能是把params
的值赋值到namedArgs
上了,不同之处是namedArgs
是以对象属性的方式取值并且不用关心参数的顺序问题,params
是以数组的方式取值需要关心参数的顺序。
做开发的都应该遇到过数字或者时间格式问题,特别是时间格式问题应该是最普遍遇到的。不同的项目时间格式往往不同,有yyyy-DD-mm
类型的有yyyyMMdd
类型以及其他类型。
同样的Ember模板也给我们提供了类似的解决办法,那就是自定义格式化方法。通过自定义helper
实现数据的格式化。
helper
:ember generate helper format-date
controller
初始化一个时间数据。// app/controllers/tools-helper.jsimport Ember from 'ember';export default Ember.Controller.extend({ currentDate: new Date()});
默认情况下显示数据currentDate
。
{{ currentDate}}`
此时显示的默认的数据格式。运行http://localhost:4200/tools-helper,可以在页面看到:Mon Sep 21 2015 23:46:03 GMT+0800 (CST)
这种格式的时间。显然不太合法我们的习惯,看着都觉得不舒服。那下面使用自定义的helper
格式化日期格式。
// app/helpers/format-data.jsimport Ember from 'ember';/** * 注意:方法名字是文件名去掉中划线变大写,驼峰式写法 * 或者你也可以直接作为helper的内部函数 * @param {[type]} params 从助手{{format-data}}传递过来的参数 */export function formatDate(params/*, hash*/) { console.log('params = ' + params); var d = Date.parse(params); var dd = new Date(parseInt(d)).toLocaleString().replace(/:d{1,2}$/,' '); // 2015/9/21 下午11:21 var v = dd.replace("/", "-").replace("/", "-").substr(0, 9); return v;}export default Ember.Helper.helper(formatDate);
或者你也可以这样写。
export default Ember.Helper.helper(function formatDate(params/*, hash*/) { var d = Date.parse(params); var dd = new Date(parseInt(d)).toLocaleString().replace(/:d{1,2}$/,' '); // 2015/9/21 下午11:21 var v = dd.replace("/", "-").replace("/", "-").substr(0, 9); return v;});
为了简便,直接就替换字符,修改时间分隔字 /
为-
。 然后修改显示的模板,使用自定义的helper
。
{{format-date currentDate}}
此时页面上显示的时间是我们熟悉的时间格式:
上面介绍的是简答的用法,Ember还允许你传入时间的格式(format
),以及本地化类型(locale
)。
helper
:ember generate helper format-date-time
controller
类里新增两个用于测试的属性cDate
和currentTime
。// app/controllers/tools-helper.jsimport Ember from 'ember';export default Ember.Controller.extend({ currentDate: new Date(), cDate: '2015-09-22', currentTime: '00:22:32'});
<br><br><br>format-date-time: {{format-date-time currentDate cDate currentTime format="yyyy-MM-dd h:mm:ss"}}<br><br><br>format-date-time-local: {{format-date-time currentDate cDate currentTime format="yyyy-MM-dd h:mm:ss" locale="en"}}
在助手format-date-time
上一共有4个属性。cDate
和currentTime
是从上下文获取值的变量,format
和locale
是Ember专门提供用于时间格式化的属性。
下面看看format-date-time
这个助手怎么获取页面的数据。
// app/helpers/format-date-time.jsimport Ember from 'ember';export function formatDateTime(params, hash) { // 参数的顺序跟模板{{format-date-time currentDate cDate currentTime}}上使用顺序一致, // cDate比currentTime先,所以第一个参数是cDate console.log('params[0] = ' + params[0]); //第一个参数是cDate, console.log('params[1] = ' + params[1]); // 第二个是currentTime console.log('hash.format = ' + hash.format); console.log('hash.locale = ' + hash.locale); console.log('------------------------------------'); return params;}export default Ember.Helper.helper(formatDateTime);
我只是演示怎么获取页面format-date-time
助手设置的值,得到页面设置的值你想干嘛就干嘛……最后看看浏览器控制台的输出信息。
因为页面使用了两次这个助手,所以自然也就打印了两次。
官方的解释是:为了保护你的应用免受跨点脚本攻击(XSS),Ember会自动把helper
返回值中的HTML标签转义。
新建一个helper
:ember generate helper escape-helper
// app/helpers/escape-helper.jsimport Ember from 'ember';export function escapeHelper(params/*, hash*/) { // return Ember.String.htmlSafe(`<b>${params}</b>`); return `<b>${params}</b>`;}export default Ember.Helper.helper(escapeHelper);
escape-helper: {{escape-helper "helloworld!"}}
此时页面上会直接显示“helloworld!”而不是“helloworld”被加粗了!如果你确定你返回的字符串是安全的你可用使用htmlSafe
方法,这个方法不会把HTML代码转义,HTML代码仍然能起作用,那么页面显示的将是加粗的“helloworld!”。
到此模板这一章全部讲完了!!!但愿你能从中得到一点收获!!后面的文章将开始讲route
,route
在Ember.js 入门指南之十三{{link-to}} 助手这一篇已经讲过一点,但不是很详细。接下来的一章将会为你详细解释route
。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
真快,第二章模板(template
)已经介绍完毕了!这个章节相对来说是比较简单,只有是有点HTML基础的学习起来并不会很难,几乎也不需要去记忆,自己动手实践实践就能理解。其中比较重要的是{{link-to}}
和{{action}}
这两篇。特别是{{link-to}}
,这个标签几乎都是与路由结合使用的,要注意与路由配置一一对应。
在下一章将为读者介绍第三章路由,如果你是看官网文档的你会发现路由是在模板之前介绍的,我稍微做了下调整,因为根据我自己学习的ember的经验我觉得先介绍模板更好学习。很多东西结合显示效果讲会容易很多。
在介绍路由这一章之前,重新创建了一个项目用于演示,依然是使用Ember CLI创建项目。下面是创建命名并且运行项目,测试项目是否创建成功。
ember new chapter3_routescd chapter3_routesember server
在浏览器运行:http://localhost:4200/,在界面上能看到Welcome to Ember说明项目搭建成功了!!
如果你还不知道怎么使用Ember CLI创建项目,请自行根据提供的地址安装配置Ember CLI命令环境,在第一章的小节已经详细介绍过,这里不再赘述。
下面开始路由的学习之旅吧~~~
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
当你的应用启动的时候,路由器就会匹配当前的URL到你定义的路由上。然后按照定义的路由层次逐个加载数据、设置应用程序状态、渲染路由对应的模板。
在app/router.js
的map
方法里定义的路由会映射到当前的URL。当map
方法被调用的时候方法体内的route
方法就会创建路由。
下面使用Ember CLI命令创建两个路由:
ember generate route aboutember generate route favorites
命令执行完之后你可在你的项目目录app/routes
下面看到已经创建好的两个路由文件已经app/templates
下面路由对应的模板文件。此时在app/router.js
的map
方法中已经存在了刚刚创建的路由配置。这个是Ember CLI自动为你创建了。
// app/router.jsimport Ember from 'ember';import config from './config/environment';var Router = Ember.Router.extend({ location: config.locationType});Router.map(function() { this.route('about'); this.route('favorites');});export default Router;
现在分别修改app/templates
下面的两个模板文件如下:
这个是about模板!<br>{{outlet}}
这个是favorites模板!<br>{{outlet}}
然后访问http://localhost:4200/about或者http://localhost:4200/favorites,如果你的程序没有问题你也会得到如下显示结果:
如果你觉得favorites
这个路由名字太长是否可以修改成其他名字呢?答案是肯定的,你只要修改router.js
中map
方法的配置即可。
Router.map(function() { this.route('about'); // 注意:访问的URL可以写favs但是项目中如果是使用route的地方仍然是使用favorites this.route('favorites', { path: '/favs' });});
此时访问:http://localhost:4200/favs,界面显示的结果与之前是一样的。
说明:默认情况下访问的URL与路由名字是一致的,比如this.route('about')
与this.route('about', { path: ‘/about’ })
是同一个路由,如果URL与路由不同名则需要使用{path: '/xxx'}
设置映射的URL。
在handlebars模板中可以使用{{link-to}}
助手在不同的路由间切换,使用时需要在link-to
助手内指定路由名称。比如下面的代码使用link-to
助手实现在about
和favs
两个路由间切换。为了页面能美观一点引入bootstrap,使用npm命令安装:bower install bootstrap
,如果安装成功你可以在bower_components目录下看到bootstrap相关的文件。安装成功之后引入到项目中,修改chapter3_routes/ember-cli-build.js
。在return
语句前加入如下两行代码(作用就是引入bootstrap框架):
app.import("bower_components/bootstrap/dist/css/bootstrap.css");app.import("bower_components/bootstrap/dist/js/bootstrap.js");
修改application.hbs
,增加一个导航菜单。
<div class="container-fluid"> <div class="navbar-header" href="#"> {{#link-to 'index' class="navbar-brand"}}Home{{/link-to}} </div> <ul class="nav navbar-nav"> <li>{{#link-to 'about'}}about{{/link-to}}</li> <li>{{#link-to 'favorites'}}favorites{{/link-to}}</li> </ul> <ul class="nav navbar-nav navbar-right"> <li><a href="#">Login</a></li> <li><a href="#">Logout</a></li> </ul> </div><div class="container-fluid" style="margin-top: 70px;">{{outlet}}</div>
如果看到页面没有bootstrap效果请重新启动项目。如果运行项目后再浏览器控制台出现如下错误。
如果出现上图错误需要在config/environment.js
中加上一些安全策略设置代码,有关设置的解释请看下面网址的文章介绍。
, contentSecurityPolicy: { 'default-src': "'none'", 'script-src': "'self' 'unsafe-inline' 'unsafe-eval' use.typekit.net connect.facebook.net maps.googleapis.com maps.gstatic.com", 'font-src': "'self' data: use.typekit.net", 'connect-src': "'self'", 'img-src': "'self' www.facebook.com p.typekit.net", 'style-src': "'self' 'unsafe-inline' use.typekit.net", 'frame-src': "s-static.ak.facebook.com static.ak.facebook.com www.facebook.com"}
上图是我的演示项目配置。然后点击about
会得到如下界面
可以看到浏览器地址栏的URL变为about
了,并且页面显示的内容也是about
模板的内容。同理你点击favorites
地址栏就变为http://localhost:4200/favs并且显示的内容是favorites
的(为什么URL是favs
而不是favorites
呢,因为前面已经修改了route
和URL的映射关系,路由favorites
对应的URL是favs
)。上述演示的就是路由的切换!!!
可以看到浏览器地址栏的URL变为about了,并且页面显示的内容也是about模板的内容。同理你点击“favorites”地址栏就变为http://localhost:4200/favs并且显示的内容是favorites的(为什么URL是favs而不是favorites呢,因为前面已经修改了route和URL的映射关系,路由favorites对应的URL是favs)。上述演示的就是路由的切换!!!
还记得在前面的Ember.js 入门指南之十三{{link-to}} 助手这篇文章的内容吗?在这篇文章中比较详细的介绍了路由的嵌套与怎么使用嵌套的路由。不妨回过头去看看。在这里打算就不讲了……如果有不明白的请看官网的教程。
application
路由是默认的路由,是程序的入口,所有其他自定义的路由都先进过application
才到自定义的路由。并且application
路由对应的application.hbs
模板是所有自定义模板的父模板,所有自定义的模板都会渲染到application.hbs模板的{{outlet}}
上。有关于路由的执行顺序以及模板的渲染顺序在前面的Ember.js 入门指南之十三{{link-to}} 助手也讲过了,在此也不打算在做过多的介绍了。你可以回头看之前的文章或者到官网查看。
对于所有的嵌套的路由,包括最顶层的路由Ember会自动生成一个访问URL为/
对应路由名称为index
的路由。
比如下面的两种路由设置是等价的。
// app/router.js// ……Router.map(function() { this.route('about'); // 注意:访问的URL可以写favs但是项目中如果是使用route的地方仍然是使用favorites this.route('favorites', { path: '/favs' });});export default Router;
// app/router.js// ……Router.map(function() { this.route('index', { path: '/' }); this.route('about'); // 注意:访问的URL可以写favs但是项目中如果是使用route的地方仍然是使用favorites this.route('favorites', { path: '/favs' });});export default Router;
index
路由会渲染到application.hbs
模板的{{outlet}}
上。这个是Ember默认设置。当用户访问/about
时Ember会把index
模板替换为about
模板。
对于路由嵌套的情况也是如此。
// app/router.js// ……Router.map(function() { this.route('posts', function() { this.route('new'); });});export default Router;
// app/router.js// ……Router.map(function() { this.route('index', { path: '/' }); this.route('posts', function() { this.route('index', { path: '/' }); this.route('new'); });});export default Router;
两种设置方式都会得到如下图的路由表。打开浏览器的“开发者工具”点开“Ember”选项卡,在点开“/#Routes”你就可以看到如下路由表(显示是顺序有可能跟你的不一样)。
注:loading
和error
这两个路由是ember自动生成的,他们的用法会在后面的文章介绍。
当用户访问/posts
时实际进入的路由是posts.index
对应的模板是posts/index.hbs
,但是实际中我并没有创建这个模板,因为Ember默认把这个模板渲染到posts.hbs
的{{outlet}}
上。由于这个模板不存在也就相当于什么都没做。当然你也可以创建这个模板。
使用命令:ember generate template posts/index
然后在这个模板中添加以下显示的内容:
<h2>这里是/posts/index.hbs。。。</h2>
再此访问http://localhost:4200/posts,是不是可以看到增加的内容了。
你可以这么理解对于每一个有子路由的路由都有一个名为index
的子路由并且这个路由对应的模板为index.hbs
,如果把有子路由的路由当做一个模块看待那么index.hbs
就是这个模块的首页。特别是做过一些信息系统的朋友应该是很熟悉的,基本上没个子模块都会有一个首页,这个首页现实的内容就是一进入这个模块时就显示的内容。既然是子模板当然也不会例外它也会渲染到父模板的{{outlet}}
上。比如上面的例子当用户访问http://localhost:4200/posts实际进入的是http://localhost:4200/posts/(后面多了一个/
,这个/
对应的模板就是index
),当用户访问的是http://localhost:4200/posts/new,那么进入的就是posts/new.hbs
这个模板(也是渲染到posts.hbs
的{{outlet}}
上)。
关于动态段在前面的Ember.js 入门指南之十三{{link-to}} 助手也介绍过了,在这里就再简单补充下。
路由最主要的任务之一就是加载model
。
例如对于路由this.route('posts');
会加载项目中所有的posts
下的model
。但是当你只想加载其中一个model
的时候怎么处理呢?而且大多数情况我们是不需要一次性加载完全部数据的,一般情况都是加载其中一小部分。这个时候就需要动态段了!
动态段以:
开头,并且后面接着model
的id
属性。
// app/router.js// ……Router.map(function() { this.route('about'); // 注意:访问的URL可以写favs但是项目中如果是使用route的地方仍然是使用favorites // this.route('favorites', { path: '/favs' }); this.route('posts', function() { this.route('post', { path: '/:post_id'}); }); });export default Router;
此时你可以访问http://localhost:4200/posts/1,不过我们还没创建model
所以会报错。这个我们暂时不管,后面会有一章是介绍model
的。现在只要知道你访问http://localhost:4200/posts/1就相当于获取id
值唯一的model
。
Ember也同样运行你使用*
作为URL通配符。有了通配符你可以设置多个URL访问同一个路由。
this.route('about', { path: '/*wildcard' });
然后访问:http://localhost:4200/wildcard或者访问http://localhost:4200/2423432ffasdfewildcard或者http://localhost:4200/2333都是可以进入到about
这个路由,但是http://localhost:4200/posts仍然进入的是posts
这个路由。因为可以匹配到这个路由。
在有路由嵌套的情况下,一般情况我们访问URL的格式都是父路由名/子路由名
,Ember提供了一个resetNamespace:true
选项可以用户重置子路由的命名空间,使用这个设置的路由可以直接这样访问/子路由名
,不需要写父路由名。
this.route('posts', function() { this.route('post', { path: '/:post_id'}); this.route('comments', { resetNamespace: true}, function() { this.route('new'); });});
此时如果你想问的new
这个路由你可以直接不写comments
。http://localhost:4200/posts/new,而不需要http://localhost:4200/posts/comments/new,不过模板渲染的顺序没变,new
模板仍然是渲染到comments
的{{outlet}}
上。
不过个人觉得还是不使用这个设置比较好,特别是在开发的时候你可以看到访问的URL的层次,对你调试代码还是很有帮助的。
以上的内容就是定义路由的全部内容。都是非常重要的知识,希望你能好好掌握,对于路由的嵌套请看之前的文章。如果有疑问请给我留言或者访问官网看原教程。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
路由其中一个很重要的职责就是加载适合的model
,初始化数据,然后在模板上显示数据。
// app/router.js// ……Router.map(function() { this.route('posts');});export default Router;
对于posts
这个路由如果要加载名为post
的model
要怎么做呢?代码实现很简单,其实在前面的代码也已经写过了。你只需要重写model
回调,在回调中返回获取到的model
即可。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // return Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls'); // 加载post(是一个model) return this.store.query('post'); }});
model
回调可以返回一个Ember Data记录,或者返回任何的promise对象(Ember Data也是promise
对象),又或者是返回一个简单的javascript对象、数组都可以。但是需要等待数据加载完成才会渲染模板,所以如果你是使用Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls');
获取远程数据页面上会有一段时间是空白的,其实就是在加载数据,不过这样的用户体验并不好,不过你也不需要担心这个问题,Ember已经提供了解决办法。还记得上一篇的截图的路由表吗?是不是每个路由都有一个xxx_loading
路由,这个路由就是在数据加载的时候执行的,有关xxx_loading
更多详细的信息在后面的博文介绍。
一个route
有时候只加载同一个model
,比如路由/photos
就通常是加载模型photo
。如果用户离开或者是重新进入这个路由模型也不会改变。然而,有些情况下路由所加载的model
是变化的。比如在一个图片展示的APP中,路由/photos
会加载一个photo
模型集合并渲染到模板photos
上。当用户点击其中一幅图片的时候路由只加载被点击的model
数据,当用户点击另外一张图片的时候加载的又是另外一个model
并且渲染到模板上,而且这两次加载的model
数据是不一样的。
在这种情形下,访问的URL就包含了很重要的信息,包括路由和模型。
在Ember应用中可以通过定义动态段实现加载不同的模型。有关动态段的知识在前面的Ember.js 入门指南之十三{{link-to}} 助手和Ember.js 入门指南之二十路由定义已经做过介绍。
一旦在路由中定义了动态段Ember就会从URL中提取动态段的值作为model
回调的第一个参数。
// app/router.js// ……Router.map(function() { this.route('posts', function() { this.route('post', { path: '/:post_id'}); }); });export default Router;
这段代码定义了一个动态段:post_id
,记得动态段是以:
”开头。然后在model
回调中使用动态段获取数据。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { return this.store.findRecord('post', params.post_id); }});
可以看到在model
回调中也是使用在路由中定义的动态段,并把这个动态段作为参数传递给Ember的方法findRecord
,Ember会把URL对应位置上的数据解析到这个动态段上。
注意:在model
中的动态段只在通过URL访问的时候才会被解析。如果你是通过其他方式(比如使用link-to
进入路由)转入路由的,那么路由中model
回调方法里的动态不会被解析,所请求的数据会直接从上下文中获取(你可以把上下文想象成ember的缓存)。下面的代码将为你演示这个说法:
import DS from 'ember-data';export default DS.Model.extend({ title: DS.attr('string'), body: DS.attr('string'), timestamp: DS.attr('number')});
定义了3个属性,id
属性不需要显示定义,ember会默认加上。
Router.map(function() { this.route('posts', function() { this.route('post', { path: '/:post_id'}); }); });
然后用Ember CLI命令(ember g route posts/post
)在创建路由post
,同时也会自动创建出子模板post.hbs
。创建完成之后会得到如下两个文件:
1.app/routes/posts/post.js2.app/templates/posts/post.hbs
修改路由posts.js
,在model
回调中返回设定的数据。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { return [ { "id":"-JzySrmbivaSSFG6WwOk", "body" : "testsssss", "timestamp" : 1443083287846, "title" : "test" }, { "id":"-JzyT-VLEWdF6zY3CefO", "body" : "33333333", "timestamp" : 1443083323541, "title" : "test33333" }, { "id":"-JzyUqbJcT0ct14OizMo" , "body" : "body.....", "timestamp" : 1443083808036, "title" : "title1231232132" } ]; }});
修改posts.hbs
,遍历显示所有的数据。
<ul> {{#each model as |item|}} <li> {{#link-to 'posts.post' item}}{{item.title}}{{/link-to}} </li> {{/each}}</ul><hr>{{outlet}}
修改子路由post.js
,使得子路由根据动态段返回匹配的数据。
// app/routes/posts/post.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { console.log('params = ' + params.post_id); return this.store.findRecord('post', params.post_id); }});
注意打印信息语句console.log()
;,然后接着修改子模板post.hbs
。
<h2>{{model.title}}</h2><p>{{model.body}}</p>
到此,全部所需的测试数据和代码已经编写完毕。下面执行http://localhost:4200/posts,可以看到界面上显示了所有在路由posts的model回调中设置的测试数据。查看页面的HTML代码:
可以看到每个连接的动态段都被解析成了数据的id属性值。注意:随便点击任意一个,注意看浏览器控制台打印的信息。我点击了以第一个连接,浏览器的URL变为
看浏览器的控制台是不是并没有打印出params = -JzySrmbivaSSFG6WwOk
,在点击其他的连接结果也是一样的,浏览器控制台没有打印出任何信息。
下面我我们直接在浏览器地址栏上输入:http://localhost:4200/posts/-JzyUqbJcT0ct14OizMo然后按enter执行,注意看浏览器控制台打印的信息!!!此时打印了params = -JzyUqbJcT0ct14OizMo
,你可以用同样的方式执行另外两个链接的地址。同样也会打印出params = xxx
(xxx
为数据的id
值)。
我想这个例子应该能很好的解释了Ember提示用户需要的注意的问题。只有直接用过浏览器访问才会执行包含了动态段的model
回调,否则不会执行包含有动态段的回调;如果没有包含动态段的model
回调不管是通过URL访问还是通过link-to
访问都会执行。你可以在路由posts
的model
回调中添加一句打印日志的代码,然后通过点击首页上的about
和posts
切换路由,你可以看到控制台打印出了你在model
回调中添加的日志信息。
对于在一个mode
l回调中同时返回多个模型的情况也是时常存在的。对于这种情况你需要在model
回调中修改返回值为Ember.RSVP.hash
对象类型。比如下面的代码就是同时返回了两个模型的数据:一个是song
,一个是album
。
// app/routes/favorites.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return Ember.REVP.hash({ songs: this.store.find('song'), albums: this.store.find('slbum') }); }});
然后在模板favorites.hbs
中就可以使用{{#each}}
把两个数据集遍历出来。遍历的方式与普通的遍历方式一样。
<h2>Song list</h2><ul>{{#each model.songs as |item|}} <li>{{item.name}}</li>{{/each}}</ul><hr><h2>Album list</h2><ul>{{#each model.albums as |item|}} <li>{{item.name}}</li>{{/each}}</ul>
到此所有路由的model
回调的情况介绍完毕,model
回调其实就是把模型绑定到路由上。实现数据的初始化,然后把数据渲染到模板上显示。这也是Ember推荐这么做的——就是把操作数据相关的处理放在route
而不是放在controller
。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
路由的另一个重要职责是渲染同名字的模板。
比如下面的路由设置,posts
路由渲染模板posts.hbs
,路由new
渲染模板posts/new.hbs
。
Router.map(function() { this.route('posts', function() { this.route('new'); });});
每一个模板都会渲染到父模板的{{outlet}}
上。比如上面的路由设置模板posts.hbs
会渲染到模板application.hbs
的{{outlet}}
上。application.hbs
是所有自定义模板的父模板。模板posts/new.hbs会渲染到模板posts.hbs
的{{outlet}}
上。
如果你想渲染到另外一个模板上也是允许的,但是要在路由中重写renderTemplate
回调方法。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ // 渲染到favorites模板上 renderTemplate: function() { this.render('favorites'); }});
模板的渲染这个知识点比较简单,内容也很少,在前面的Ember.js 入门指南之十四番外篇,路由、模板的执行、渲染顺序已经介绍过相关的内容。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
声明:对于transition这个词直译是“过渡”的意思,但是总觉得“路由的过渡”读起来总有那么一点别扭,想了下于是就用“切换”替代吧,如有不妥欢迎指正。
我们熟知的Java、PHP等语言都提供了URL的重定向,那么Ember的重定向又是怎么去实现的呢?
如果是从路由重定向到另外一个路由你可以调用transitionTo
方法,如果是从controller
重定向到一个route
则调用transitionToRoute
方法。transitionTo
方法所实现的功能与link-to
的作用是一样的,都可以实现路由的切换。如果重定向之后的路由包含有动态段你需要解析model
数据或者指定动态段的值。由于不是直接执行URL所以不会执行重定向之后的路由的model
回调。
如果你想在路由切换的时候不加载model
你可以调用beforeModel
回调,在这个回调中实现路由的切换。
beforeModel() { this.transitionTo('posts');}
有些情况下你需要先根据model
回调获取到的数据然后判断跳转到某个路由上。此时你可以使用afterModel
回调方法。
afterModel: function(model, transition) { if (model.get(‘length’) === 1) { this.transitionTo('post', model.get('firstObject')); }}
切换路由,并初始化数据为model
的第一个元素数据。
Router.map(function() { this.route('posts', function() { this.route('post', { path: '/:post_id'}); }); });
子路由的重定向有些许不同,如果你需要重定向到上面这个段代码的子路由posts.post
上,如果是使用beforeModel
、model
、afterModel
回调重定向到posts.post
父路由posts
会重新在执行一次,再次执行父路由这种方式就显得有点多余了,特别父路由需要加载的数据比较多的时候,会影响到加载的效率。如果是这种情况我们可以使用redirect
回调,此回调不会再次执行父路由。仅仅是实现路由切换而已。
redirect: function(model, transition) { if (model.get('length') === 1) { this.transitionTo('posts.post', model.get('firstObject')); }}
重定向到子路由,解析之后会得到的类似于posts/2
这种形式的URL。
以上就是全部路由的重定向方式,主要有4个回调:beforeModel
、model
、afterModel
、redirect
。前面三种使用场景差别不大,redirect
主要用于重定向到子路由。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
在路由的转换过程中,Ember路由器会通过回调(beforeModel
、model
、afterModel
、redirect
)解析一个transition
对象到转换的下一路由中。任何一个回调都可以通过传递过来的transition
参数获取transition
对象,然后使用这个对象调用transition.abort()
方法立即终止路由的转换,如果你的程序保存了这个对象(transition
对象)之后你还可以在需要的地方取出来并调用transition.retry()
方法激活路由转换这个动作,最终实现路由的转换。
当用户通过{{link-to}}
助手、transition
方法或者直接执行URL来转换路由,当前路由会自动执行willTransition
方法。每个活动的路由都可以决定是否执行转换路由。
想象一下,在当前路由所渲染的页面是一个比较复杂的表单,并且用户已经填写了很多信息,但是用户很可能无意中点击了返回或者关闭页面,这就导致了用户填写的信息直接丢失了,这样的用户体验并不好。此时我们可以通过使用willTransition
方法阻止用户的行为并提示用户是否确认离开本页面。
为了验证这个特性我们需要创建好测试所需的文件。
ember g controller formember g route form
首先在controller
增加测试数据。
// app/controllers/form.jsimport Ember from 'ember';export default Ember.Controller.extend({ firstName: 'chen', lastName: 'ubuntuvim'});
再创建一个模拟用户填写信息的模板。
<div class="form-group"> FirstName {{input type="text" class="form-control" id="exampleInputEmail1" placeholder="FirstName" value=firstName}} </div> <div class="form-group"> LashName {{input type="text" class="form-control" id="exampleInputPassword1" placeholder="LashName" value=lastName}} </div> <button type="submit" class="btn btn-primary">Submit</button><br><br>{{#link-to 'about'}}<b>转到about</b>{{/link-to}}
关键部分来了,我们在路由里添加willTransition
方法。
// app/routes/form.jsimport Ember from 'ember';export default Ember.Route.extend({ actions: { willTransition: function(transition) { // 如果是使用this.get('key')获取不了页面输入值,因为不是通过action提交表单的 var v = this.controller.get('firstName'); // 任意获取一个作为判断表单输入值 if (v && !confirm("你确定要离开这个页面吗??")) { transition.abort(); } else { return true; } } }});
运行:http://localhost:4200/form,先点击submit
提交表单,可以看到表单顺利提交没有任何问题,然后再点击转到about
,你可以看到会弹出如下提示框。
接着,点击“取消”页面没有跳转,如果是点击“确定”页面会跳转到about
页面。再接着,把FirstName
这个输入框的内容清空然后点击“转到about”页面直接跳转到了about
页面。
很多博客网站都是有这个功能的!!
beforeModel(transition) { if (new Date() > new Date('January 1, 1980')) { alert('Sorry, you need a time machine to enter this route.'); transition.abort(); }}
这段代码演示的就是在beforeModel
回调中使用abort
方法阻止路由的转换。代码比较简单我就不做例子演示了!
对于使用abort
方法终止的路由可以调用retry
方法重新激活。一个很典型的例子就是登陆。如果登陆成功就转到首页,否则跳转回登陆页面。文件准备工作:
ember g controller authember g route authember g controller loginember g route login
下面是演示用到的代码。
// app/controllers/login.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { login: function() { // 获取跳转过来之前路由中设置的transition对象 var transitionObj = this.get('transitionObj'); console.log('transitionObj = ' + transitionObj); if (transitionObj) { this.set("transitionObj", null); transitionObj.retry(); } else { // 转回首页 this.transitionToRoute('index'); } } }});
// app/controllers/auth.jsimport Ember from 'ember';export default Ember.Controller.extend({ userIsLogin: false});
// app/routes/auth.jsimport Ember from 'ember';export default Ember.Route.extend({ beforeModel(transition) { // 在名为auth的controller设置了userIsLogin为false,默认是未登录 if (!this.controllerFor("auth").get('userIsLogin')) { var loginController = this.controllerFor("login"); // 保存transition对象 loginController.set("transitionObj", transition); this.transitionTo("login"); // 跳转到路由login } }});
这个是登陆页面
login
页面,结果显示如下:可以看到URL确实是转到login
了。
transitionObj
是undefined
。由于没有经auth
这个路由的跳转所以获取不到transition
对象。自然就跳转回index
这个路由。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
在前面的Ember.js 入门指南之二十路由定义提过loading
、error
子路由,它们是Ember默认创建的,并在beforeModel
、model
、afterModel
这三个回调执行完毕之前会先渲染当前路由的loading
和error
模板。
Router.map(function() { this.route('posts', function() { this.route('post', { path: '/:post_id'}); });});
对于上述的路由设置Ember会生成如下的路由列表:
每个路由都会自动生成一个loading
、error
路由,下面我将一一演示这两个路由的作用。图片前面loading
、error
路由对应的application
路由。posts_loading
和posts_error
对应的是posts
路由。
Ember建议数据放在beforeModel
、model
、afterModel
回调中获取并传递到模板显示。但是只要是加载数据就需要时间,对于Ember应用来说,在model
等回调中加载完数据才会渲染模板,如果加载数据比较慢那么用户看到的页面就是一个空白的页面,用户体验很差!
Ember提供的解决办法是:在beforeModel
、model
、afterModel
回调还没返回前先进入一个叫loading
的子状态,然后渲染一个叫routeName-loading
的模板(如果是application
路由则对应的直接是loading
、error
不需要前缀)。
为了演示这效果在app/templates
下创建一个posts-loading
模板。如果程序正常,在渲染模板posts
之前会先渲染这个模板。
<img src="assets/images/loading/loading.gif" />
然后修改路由posts.js
,让model
回调执行时间更长一些。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 模拟一个延时操作, for (var i = 0; i < 10000000;i++) { } return Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls'); }});
执行http://localhost:4200/posts,首先会看到执行的loading
模板的内容,然后才看到真正要显示的数据。有一个加载过程,如下面2幅图片所示。
在beforeModel
、model
、afterModel
回调没有立即返回之前,会先执行一个名为loading的事件。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 模拟一个延时操作, for (var i = 0; i < 10000000;i++) { } return Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls'); }, actions: { loading: function(transition, originRoute) { alert("Sorry this is taking so long to load!!"); } }});
页面刷新后会弹出一个提示框,先不点击“确定”。打开浏览器的“开发者 -> 开发者工具”,切换到Network标签下。找到“pulls”这个请求,点击它。
从图中可以看到此时model
回调并没有返回。此时响应的内容是空的,说明loading
事件实在model
回调返回之前执行了。
然后点击弹出框的“确定”,此时可以看到Response有数据了。说明model
回调已经执行完毕。注意:如果当前的路由没有显示定义loading
事件,这个时间会冒泡到父路由,如果父路由也没有显示定义loading
事件,那么会继续向上冒泡,一直到最顶端的路由application
。
与loading
子状态类似,error
子状态会在beforeModel
、model
、afterModel
回调执行过程中出现错误的时候触发。
命名方式与loading
子状态也是类似的。现在定义一个名为posts-error.hbs
的模板。
<p style="color: red;">posts回调解析出错。。。。</p>
然后在model
回调中手动添加一个错误代码。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 模拟一个延时操作, for (var i = 0; i < 10000000;i++) { } var e = parseInt(value); return Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls'); }});
注意var e = parseInt(value);
这句代码,由于value
没有定义所以应该会报错。那么此时页面会显示什么呢??
如果你的演示程序没有其他问题那么你也会得到上图的结果。但是如果没有定义这个模板,那么界面上将是什么都不显示。
如果你想在xxx-error.hbs
模板上看到是什么错误信息,你可以在模板上打印model
的值。如下:
<p style="color: red;">posts回调解析出错。。。。<br>{{model}}</p>
此时页面会显示出你的代码是什么错误。
不过相比于浏览器控制台打印的错误信息简单很多!!!
error
事件与第一点讲的loading事件也是相似的。使用方式与loading
一样。个人觉得这个事件非常有用,我们可以在这个事件中根据error
状态码的不同执行不同的逻辑,比如跳转到不同的路由上。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls____'); }, actions: { error: function(error, transition) { console.log('error = ' + error.status); // 打印error对象里的所有属性和方法名 for(var name in error){ console.log(name); // console.log('属性值或者方法体==》' + error[name]); } alert(names); if (error && error.status === 400) { return this.transitionTo("about"); } else if (error.status === 404) { return this.transitionTo("form"); } else { console.log('else......'); } } }});
注意getJSON
方法里的URL,我在URL后面随机加了一些字符,目的是让这个URL不存在。此时请求应该会找不到这个地址error
的响应码应该是404。然后直接跳转到form
这个路由上。运行http://localhost:4200/posts之后,浏览器控制台打印信息如下:
页面也会跳转到form
。
到此路由加载数据过程中涉及的两个状态loading
和error
的内容全部介绍完,这两个状态在优化用户体验方面是非常有用的,希望想学习Ember的同学好好掌握!!!=^=
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
查询参数是在URL的问号(?)右边部分,通常是键值对形式出现。
http://example.com/articles?sort=ASC&page=2
比如这个URL的查询参数有两个,一个是sort
,一个是page
,它们的值分别是ASC
和2
。
查询参数通常是声明为controller
类中。比如在当前活动路由articles
下,你需要根据文章的类型category
过滤,此时你必须要在controller
内声明过滤参数category
。
使用Ember CLI新建一个controller
、route
:
ember g controller article;ember g route articles;
// app/controllers/articles.jsimport Ember from 'ember';export default Ember.Controller.extend({ queryParams: ['category'], category: null});
绑定一个查询参数到URL,并且参数的值为null
。当你进入路由articles
时,如果参数category
的值发生变化会自动更新到controller
中的category
;反之亦然。你可以设置一个默认值,比如把category
设置为Java
。可以在模板上获取这个值。
{{outlet}}category = {{category}}
执行http://localhost:4200/articles,页面会显示出 category = Java
。如果执行http://localhost:4200/articles?category=PHP,那么页面会显示category = PHP
。
下面代码演示了怎么使用查询参数:
// app/controllers/articles.jsimport Ember from 'ember';export default Ember.Controller.extend({ queryParams: ['category'], category: null, // 定义一个返回数组的计算属性,可以直接在模板上遍历 filteredArticles: Ember.computed('category', 'model', function() { var category = this.get('category'); var articles = this.get('model'); if (category) { return articles.filterBy('category', category); } else { return articles; } })});
创建一个计算属性,这个计算属性是一个数组类型。由于是计算属性,并且这个计算属性关联了另外两个属性category
和model
,只要这两个属性其中之一发生改变都会导致filteredArticles
发生改变,所以返回的数组元素也会跟着改变。
在route
初始化测试数据。
// app/routes/article.jsimport Ember from 'ember';export default Ember.Route.extend({ model(params) { return [ { id: 1, title: 'Bower: dependencies and resolutions new', body: "In the bower.json file, I see 2 keys dependencies and resolutionsWhy is that so? I understand Bower has a flat dependency structure. So has it got anything to do with that ?", category: 'java' }, { id: 2, title: 'Highly Nested JSON Payload - hasMany error', body: "Welcome to the Ember.js discussion forum. We're running on the open source, Ember.js-powered Discourse forum software. They are also providing the hosting for us. Thanks guys! Please use this space for discussion abo… read more", category: 'php' }, { id: 3, title: 'Passing a jwt to my REST adapter new ', body: "This sets up a binding between the category query param in the URL, and the category property on controller:articles. In other words, once the articles route has been entered, any changes to the category query param in the URL will update the category property on controller:articles, and vice versa.", category: 'java' } ]; }});
下面看看怎么在模板显示数据,并且根据category
显示不同数据。
<div class="col-md-4 col-xs-4"><ul> 输入分类:{{input value=category placeholder ='查询的分类'}}</ul><ul> {{#each filteredArticles as |item|}} <li> {{#link-to 'articles.article' item}} {{item.title}}--{{item.category}} {{/link-to}} </li> {{/each}}</ul></div><div class="col-md-8 col-xs-8">{{outlet}}</div>
精彩的时刻到了!!先执行http://localhost:4200/articles,此时显示的是所有类型的数据。如下图:
接着你就可以做点好玩的事了,直接在输入框输入分类名。由于计算属性的特性会自动更新数组filteredArticles
。所以我们可以看到随着你输入字符的变化显示的数据也在变化!这个例子也说明了Ember计算属性自动更新变化的强大!!用着确实爽啊!!官网教程没有说怎么在模板中使用,讲得也不是很明白,就给了一句
Now we just need to define a computed property of our category-filtered array that the articles template will render:”
也有可能是我看不懂,反正摸索好一阵子才知道要这么用!!
link-to
助手使用query-params
子表达式直接指定查询参数,需要注意的是这个表达式需要放在括号内使用,切记别少了这个括号。
……<ul> {{#link-to 'articles' (query-params category='java')}} java {{/link-to}} <br> {{#link-to 'articles' (query-params category='php')}} php {{/link-to}} <br> {{#link-to 'articles' (query-params category='')}} all {{/link-to}}</ul>……
在显示数据的ul标签后面新增上述两个link-to
助手。它们的作用分别是指定分类类型为java、php、全部。但用户点击三个连接直接显示与连接指定类型匹配的数据(并且查询的输入框也变为链接指定的类型值)。比如我点击了第一个链接,输入显示如下图:
route
对象的transitionTo
方法和controller
对象的transitionToRoute
方法都可以接受final
类型的参数。并且这个参数是一个包括一个key
为queryParams
的对象。
修改前面已经创建好的路由posts.js
。
// app/routes/posts.jsimport Ember from 'ember';export default Ember.Route.extend({ beforeModel: function(params) { // 转到路由articles上,并且传递查询参数category,参数值为Java this.transitionTo('articles', { queryParams: { category: 'java' }}); }});
执行http://localhost:4200/posts后,可以看到路由直接跳转到http://localhost:4200/articles?category=java,实现了路由切换的同时也指定了查询的参数。界面显示的数据我就不截图了,程序不出错,显示的都是category
为java
的数据。
另外还有三种切换路由的方式。
// 可以传递一个object过去this.transitionTo('articles', object, { queryParams: { category: 'java' }});// 这种方式不会改变路由,只是起到设置参数的作用,如果你在//路由articles中使用这个方法,你的路由仍然是articles,只是查询参数变了。this.transitionTo({ queryParams: { direction: 'asc' }});// 直接指定跳转的URL和查询参数this.transitionTo('/posts/1?sort=date&showDetails=true');
上面的三种方式请读者自己编写例子试试吧。光看不练假把式……
transitionTo
和link-to
提供的参数仅会改变查询参数的值,而不会改变路由的层次结构,这种路由的切换被认为是不完整的,这也就意味着比如model
和setupController
回调方法就不会被执行,只是使得controller
里的属性值为新的查询参数值以及更新URL。
但是有些情况是查询参数改变需要从服务器重新加载数据,这种情况就需要一个完整的路由切换了。为了能在查询参数改变的时候切换到一个完整的路由你需要在controller
对应的路由中配置一个名为queryParams
哈希对象。并且需要设置一个名为refreshModel
的查询参数,这个参数的值为true
。
queryParams: { category: { refreshModel: true }},model: function(params) { return this.store.query('article', params);}
关于这段代码演示实例请查看官方提供的代码!
默认情况下,Ember使用pushState
更新URL来响应controller
类中查询参数属性的变化,但是如果你想使用replaceState
来替换pushState
你可以在route
类中的queryParams
哈希对象中设置replace
为true
。设置为true
表示启用这个设置。
queryParams: {category: { replaceState:true}}
默认情况下,在controller
类中指定的查询属性foo
会绑定到名为foo
的查询参数上。比如:?foo=123
。你也可以把查询属性映射到不同的查询参数上,语法如下:
// app/controllers/articles.jsimport Ember from 'ember';export default Ember.Controller.extend({ queryParams: { category: 'articles_category' } category: null});
这段代码就是把查询属性category
映射到查询参数articles_category
上。对于有多个查询参数的情况你需要使用数组指定。
// app/controllers/articles.jsimport Ember from 'ember';export default Ember.Controller.extend({ queryParams: ['page', 'filter', { category: 'articles_category' }], category: null, page: 1, filter: 'recent'});
上述代码定义了三个查询参数,如果需要把属性映射到不同名的参数需要手动指定,比如category
。
export default Ember.Controller.extend({ queryParams: 'page', page: 1});
在这段代码中设置了查询参数page
的默认值为1
。
这样的设置会有两种默认的行为:
1.查询的时候查询属性值会根据默认值的类型自动转换,所以当用户输入http://localhost:4200/articles?page=1的时候page
的值1
会被识别成数字1
而不是字符'1'
,应为设置的默认值1
是数字类型。2.当查询的值正好是默认值的时候,该值不会被序列化到URL中。比如查询值正好是?page=1
这种情况URL可能是/articles
,但是如果查询值是?page=2
,URL肯定是/articles?page=2
。
默认情况下,在Ember中查询参数是“粘性的”,也就是说如果你改变了查询参数或者是离开页面又回退回来,新的查询值会默认在URL上,而不会自动清除(几乎所见的URL都差不多是这种情况)。这是个很有用的默认设置,特别是当你点击后退回到原页面的时候显示的数据没有改变。
此外,粘性的查询参数值会被加载的route
存储或者回复。比如,包括了动态段/:post_id
的路由posts
,以及路由对应的controller
包含了查询属性filter
。如果你导航到/badgers
并且根据reookies
过滤,然后再导航到/bears
并根据best
过滤,然后再导航到/potatose
并根据lamest
过滤。如下面的链接:
<ul> {{#link-to 'posts' 'badgers'}}Badgers{{/link-to}}<br> {{#link-to 'posts' 'bears'}}Bears{{/link-to}}<br> {{#link-to 'posts' 'potatoes'}}Potatoes{{/link-to}}<br></ul>
模板编译之后得到如下HTML代码:
<ul> <a href="/badgers?filter=rookies">Badgers</a> <a href="/bears?filter=best">Bears</a><a href="/potatoes?filter=lamest">Potatoes</a></ul>
可以看到一旦你改变了查询参数,查询参数就会被存储或者是关联到route
所加载的model
上。如果你想重置查询参数你有如下两种方式处理:
1.在link-to
或者transitionTo
上显式指定查询参数的值;2.使用Route.resetController
回调设置查询参数的值并回退到切换之前的路由或者是改变model
的路由。
下面的代码片段演示了一个查询参数在controller
中重置为1
,同时作用于切换前ActiclesRoute
的model
。结果就是当返回到当前路由时查询值已经被重置为1
。
// app/routes/article.jsimport Ember from 'ember';export default Ember.Route.extend({ resetController(controller, isExiting, transition) { // 只有model发生变化的时候isExiting才为false if (isExiting) { // 重置查询属性的值 controller.set('page', 1); } }});
某些情况下,你不想是用查询参数值限定路由模式,而是让查询参数值改变的时候路由也跟着改变并且会重新加载数据。这时候你可用在对应的controller
类中设置queryParams
哈希对象,在这对象中配置一个参数scope
为controller
。如下:
queryParams: [{ showMagnifyingGlass: { scope: 'controller' }}]
粘性的查询参数值这个只是点理解起来好难的说,看下一遍下来都不知道这个有何用!!!现在还是学习阶段还没真正在项目中使用这个特性,所以我也不知道怎么解释更容易理解,建议直接看官网教程吧!!
说明:本文是基于官方2.0参考文档缩写,相对于其他版本内容会有出入。
以上的内容就是有关查询参数的全部了,主要是理解了查询参数的设置使用起来也就没什么问题。有点遗憾的是没能写出第4点的演示实例!能力有限只能遇到或者明白其使用的时候再补上了!!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
本文将为你介绍路由的高级特性,这些高级特性可以用于处理项目复杂的异步逻辑。
关于单词promises,直译是承诺,但是个人觉得还是使用原文吧。读起来顺畅点。
Ember的路由处理异步逻辑的方式是使用Promise。简而言之,Promise就是一个表示最终结果的对象。这个对象可能是fulfill
(成功获取最终结果)也可能是reject
(获取结果失败)。为了获取这个最终值,或者是处理Promise失败的情况都可以使用then
方法,这个方法接受两个可选的回调方法,一个是Promise获取结果成功时执行,一个是Promise获取结果失败时执行。如果promises获取结果成功那么获取到的结果将作为成功时执行的回调方法的参数。相反的,如果Promise获取结果失败,那么最终结果(失败的原因)将作为Promise失败时执行的回调方法的参数。比如下面的代码段,当Promise获取结果成功时执行fulfill
回调,否则执行reject
回调方法。
// app/routes/promises.jsimport Ember from 'ember';export default Ember.Route.extend({ beforeModel: function() { console.log('execute model()'); var promise = this.fetchTheAnswer(); promise.then(this.fulfill, this.reject); }, // promises获取结果成功时执行 fulfill: function(answer) { console.log("The answer is " + answer); }, // promises获取结果失败时执行 reject: function(reason) { console.log("Couldn't get the answer! Reason: " + reason); }, fetchTheAnswer: function() { return new Promise(function(fulfill, reject){ return fulfill('success'); //如果返回的是fulfill则表示promises执行成功 //return reject('failure'); //如果返回的是reject则表示promises执行失败 }); }});
上述这段代码就是promises的一个简单例子,promises的then
方法会根据promises的获取到的最终结果执行不同的回调,如果promises获取结果成功则执行fulfill
回调,否则执行reject
回调。
promises的强大之处不仅仅如此,promises还可以以链的形式执行多个then
方法,每个then方法都会根据promises的结果执行fulfill
或者reject
回调。
// app/routes/promises.jsimport Ember from 'ember';export default Ember.Route.extend({ beforeModel() { // 注意Jquery的Ajax方法返回的也是promises var promiese = Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls'); promiese.then(this.fetchPhotoOfUsers) .then(this.applyInstagramFilters) .then(this.uploadThrendyPhotAlbum) .then(this.displaySuccessMessage, this.handleErrors); }, fetchPhotoOfUsers: function(){ console.log('fetchPhotoOfUsers'); }, applyInstagramFilters: function() { console.log('applyInstagramFilters'); }, uploadThrendyPhotAlbum: function() { console.log('uploadThrendyPhotAlbum'); }, displaySuccessMessage: function() { console.log('displaySuccessMessage'); }, handleErrors: function() { console.log('handleErrors'); }});
这种情况下会打印什么结果呢??
在前的文章已经使用过Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls');
获取数据,是可以成功获取数据的。所以promises获取结果成功,应该执行的是获取成功对应的回调方法。浏览器控制台打印结果如下:
fetchPhotoOfUsersapplyInstagramFiltersuploadThrendyPhotAlbumdisplaySuccessMessage
但是如果我把Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls');
改成一个不存在的URL,比如改成Ember.$.getJSON('https://www.my-example.com');
执行代码之后控制台会提示出404
错误,并且打印'handleErrors'。说明promises获取结果失败,执行了then里的reject回调。为了验证每个回调的reject
方法再修改修改代码,如下:
// app/routes/promises.jsimport Ember from 'ember';export default Ember.Route.extend({ beforeModel() { // 注意Jquery的Ajax方法返回的也是promises var promiese = Ember.$.getJSON(' https://www.my-example.com '); promiese.then(this.fetchPhotoOfUsers, this.fetchPhotoOfUsersError) .then(this.applyInstagramFilters, this.applyInstagramFiltersError) .then(this.uploadThrendyPhotAlbum, this.uploadThrendyPhotAlbumError) .then(this.displaySuccessMessage, this.handleErrors); }, fetchPhotoOfUsers: function(){ console.log('fetchPhotoOfUsers'); }, fetchPhotoOfUsersError: function() { console.log('fetchPhotoOfUsersError'); }, applyInstagramFilters: function() { console.log('applyInstagramFilters'); }, applyInstagramFiltersError: function() { console.log('applyInstagramFiltersError'); }, uploadThrendyPhotAlbum: function() { console.log('uploadThrendyPhotAlbum'); }, uploadThrendyPhotAlbumError: function() { console.log('uploadThrendyPhotAlbumError'); }, displaySuccessMessage: function() { console.log('displaySuccessMessage'); }, handleErrors: function() { console.log('handleErrors'); }});
由于promises获取结果失败故执行其对应的失败处理回调。这种调用方式有点类似于try……catch……
,但是本文的重点不是讲解promises,更多有关promises的教材请读者自行Google或者百度吧,在这里介绍一个js库RSVP.js,它可以让你更加简单的组织你的promises代码。在附上几个promises的参考网站:
极力推荐看第二个网站的教材,这个网站可以直接运行js代码。还有源码和PDF。非常棒!!!
当发生路由切换的时候,在model
回调(或者是beforeMode
、afterModel
)中获取的数据集会在切换完成的时候传递到路由对应的controller
上。如果model回调返回的是一个普通的对象(非promises对象)或者是数组,路由的切换会立即执行,但是如果model回调返回的是一个promises对象,路由的切换将会被中止直到promises执行完成(返回fulfill
或者是reject
)才切换。
路由器任务任何一个包含了then方法的对象都是一个promises。
如果promises获取结果成功则会从被中止的地方继续往下执行或者是执行路由链的下一个路由,如果promises返回的依然是一个promises,那么路由依然再次被中止,等待promises的返回结果,如果是fulfill
则从被中止的地方开始往下执行,以此类推,一直到获取到model
回调所需的结果。
传递到每个路由的setupController
回调的值都是promises返回fulfill
时的值。如下代码:
// app/routes/tardy.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return new Ember.RSVP.Promise(function(resolver) { console.log('start......'); Ember.run.later(function() { resolver({ msg: 'Hold your horses!!'}); }, 3000); }); }, setupController(controller, model) { console.log('msg = ' + model.msg); }});
一进入路由tardy
,model
回调就会被执行并且返回一个延迟3秒才执行的promises,在这期间路由会中止。当promises返回fulfill
路由会继续执行,并将model
返回的对象传递到setupController
方法中。
虽然这种中止的行为会影响响应速度但是这是非常必要的,特别是你需要保证model
回调得到的数据是完整的数据的时候。
文章前面主要讲的是promises获取结果成功的情况,但是如果是获取结果失败的情况又是怎么处理呢??
默认情况下,如果model
回调返回的是一个promises对象并且此promises返回的是reject
,此时路由切换将被终止,也不会渲染对应的模板,并且会在浏览器控制台打印出错误日志信息,例子promises-ret-reject.js
会演示。
你可以自定义处理出错信息的逻辑,只要在route
的actions
哈希对象中配置即可。当promises获取结果失败的默认情况下会执行一个名为error
的处理事件,否则会执行你自定义的处理事件。
// app/routes/promises-ret-reject.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 为了测试效果直接返回reject return Ember.RSVP.reject('FAIL'); }, actions: { error: function(reason) { console.log('reason = ' + reason); // 如果你想让这个事件冒泡到顶级路由application只需要返回true // return true; } }});
如果没有不允许事件冒泡打印结果仅仅是reason = FAIL
。并且页面上什么都不显示(不渲染模板)。
如果去掉最后一行代码的注释,让事件冒泡到顶级路由application
中的默认方法处理,那么结果又是什么呢?
结果是先打印了处理结果,然后再打印出提示错误的日志信息。并且页面上什么都不显示(不渲染模板)。
在前面第3点介绍了promises获取结果失败时会终止路由转换,但是如果model
返回是一个promises链呢?程序能到就这样死了!!!显然是不行的,做法是把model回调中返回的reject
转换为fulfill
。这样就可以继续执行或者切换到下一个路由了!
// app/routes/funky.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { var promises = Ember.RSVP.reject('FAIL'); // 由于已经知道promises返回的是reject,所以fulfill回调直接写为null return promises.then(null, function() { return { msg: '恢复reject状态:其实就是在reject回调中继续执行fulfill状态下的代码。' }; }); }});
为了验证model
回调的结果,直接在模板上显示msg
。
funky模板<br>{{model.msg}}
执行URL:http://localhost:4200/funky,得到如下结果:
说明model回调进入到reject回调中,并正确返回了预期结果。
到本文为止有关路由这以整章的内容也全部介绍完毕了!!难点在Ember.js 入门指南之二十六查询参数这一篇。能力有限没有把这篇的内容讲明白,暂时搁下待日后完善!
总的来说路由主要职责是获取数据,根据逻辑处理数据。有点MVC架构的dao层,专门做数据的CRUD操作。当然另外一个重要职责就是路由的切换,以及切换的时候参数的设置问题。
结束完这一章下一章接着介绍组件(Component)。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能又出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
不得不说,Ember的更新是在是太快了!!本教程还没写到一半就又更新到v2.1.0
了!!!!不过为了统一还是使用官方v2.0.0
的参考文档!!
从本篇开始进入新的一章——组件。这一章将用6篇文章介绍Ember的组件,从它的定义开始知道它的使用方式,我将为你一一解答!
准备工作:本章代码统一访问项目chapter4_components下,项目代码可以在以下网址上找到:https://github.com/ubuntuvim/my_emberjs_code
与之前的文章一样,项目仍然是使用Ember CLI命令创建项目和各个组件文件。
创建项目并测试运行,首先执行如下四条命令,最后在浏览器执行:http://localhost:4200/。
ember new chapter4_componentscd chapter4_componentsember server
如果你能在页面上看到Welcome to Ember说明项目框架搭建成功!那么你可以继续往下看了,否则想搭建好项目再往下学习~~~
创建组件方法很简单:ember generate component my-component-name
。一条命令即可,但是需要注意的是组件的名称必须要包含中划线-
,比如blog-post
、test-component
、audio-player-controls
这种格式的命名是合法,但是post
、test
这种方式的命名是不合法的!其一是为了防止用户自定义的组件名与W3C规定的元素标签名重复;其二是为了确保Ember能自动检测到用户自定义的组件。
下面定义一个组件,ember g component blog-post
。Ember CLI会自动为你创建组件对应的的模板,执行这条命令之后你可以在app/components
和app/templates/components
下看到创建的文件。
<h1>{{title}}</h1> <p>{{yield}}</p> <p>Edit title: {{input type="text" value=title}}</p>
为了演示组件的使用需要做些准备工作:ember g route index
// app/routes/index.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return [ { id: 1, title: 'Bower: dependencies and resolutions new', body: "In the bower.json file, I see 2 keys dependencies and resolutionsWhy is that so? I understand Bower has a flat dependency structure. So has it got anything to do with that ?", category: 'java' }, { id: 2, title: 'Highly Nested JSON Payload - hasMany error', body: "Welcome to the Ember.js discussion forum. We're running on the open source, Ember.js-powered Discourse forum software. They are also providing the hosting for us. Thanks guys! Please use this space for discussion abo… read more", category: 'php' }, { id: 3, title: 'Passing a jwt to my REST adapter new ', body: "This sets up a binding between the category query param in the URL, and the category property on controller:articles. In other words, once the articles route has been entered, any changes to the category query param in the URL will update the category property on controller:articles, and vice versa.", category: 'java'} ]; }});
{{#each model as |item|}} {{#blog-post title=item.title}} {{item.body}} {{/blog-post}}{{/each}}
在这段代码中,使用了自定义的组件来显示数据。最后页面显示如下:
自定义的组件被渲染到了模板index.hbs
使用blog-post
的地方。并且自定义组件的HTML标签没有变化。到这里大概应该知道怎么去使用组件了,至于它是怎么就渲染到了使用组件的地方,以及它是怎么渲染上去的。别急~~后面的文章会为你一一解答。
说明:默认情况下,自定义的组件会被渲染到div
标签内,当然这种默认情况也是可以修改的,比较简单在此不过多介绍,请自行学习,网址:customizing-a-components-element。
用户自定义的组件类都需要继承Ember.Component
类。
通常情况下我们会把经常使用的模板片段封装成组件,只需要定义一次就可以在项目任何一个模板中使用,而且不需要编写任何的javascript代码。比如上述第一点“自定义组件及使用”中描述的一样。
但是如果你想你的组件有特殊的行为,并且这些行为是默认组件类无法提供的(比如:改变包裹组件的标签、响应组件模板初始化某个状态等),那么此时你可以自定义组件类,但是要继承Ember.Component
,如果你自定义的组件类没有继承这个类,你自定义的组件就很有可能会出现一些不可预知的问题。
Ember所能识别的自定义组件类的名称是有规范的。比如,你定义了一个名为blog-post
的组件,那么你的组件类的名称应该是app/components/blog-post.js
。如果组件名为audio-player-controls
那么对应的组件类名为app/components/audio-player-controls.js
。即:组件类名与组件同名,这个是v2.0
的命名方法,请区别就版本的Ember,旧版本的组件命名规则是驼峰式的命名规则。
举个简单的例子,在第一点“自定义组件及使用”中讲过,组件默认会被渲染到div
标签内,你可以在组件类中修改这个默认标签。
// app/components/blog-post.jsimport Ember from 'ember';export default Ember.Component.extend({ tagName: 'nav'});
这段代码修改了包裹组件的标签名,页面刷新后HTML代码如下:
可以看到组件的HTML代码被包含在nav
标签内。
组件的动态渲染与Java的多态有点相似。{{component}}
助手会延迟到运行时才决定使用那个组件渲染页面。当程序需要根据数据不同渲染不同组件的时,这种动态渲染就显得特别有用。可以使你的逻辑和试图分离开。
那么要怎么使用呢?非常简单,只需要把组件名作为参数传递过去即可,比如:使用{{component 'blog-post'}}
与{{blog-post}}
结果是一致的。我们可以修改第一点“自定义组件及使用”实例中模板index.hbs
的代码。
{{#each model as |item|}} {{component 'blog-post' title=item.title}} {{item.body}}{{/each}}
页面刷新之后,可以看到结果是一样的。
下面为读者演示如何根据数据不同渲染不同的组件。
按照惯例,先做好准备工作,使用Ember CLI命令创建2个不同的组件。
ember g component foo-componentember g component bar-component
<h1>Hello from bar</h1><p>{{post.body}}</p>
为何能用post
获取数据,因为在使用组件的地方传递了参数。在模板index.hbs
中可以看到。
<h1>Hello from foo</h1><p>{{post.body}}</p>
修改显示的数据,注意数据的最后增加一个属性pn
,pn
的值就是组件的名称。
// app/routes/index.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return [ { id: 1, title: 'Bower: dependencies and resolutions new', body: "In the bower.json file, I see 2 keys dependencies and resolutionsWhy is that so? I understand Bower has a flat dependency structure. So has it got anything to do with that ?", pn: 'bar-component' }, { id: 2, title: 'Highly Nested JSON Payload - hasMany error', body: "Welcome to the Ember.js discussion forum. We're running on the open source, Ember.js-powered Discourse forum software. They are also providing the hosting for us. Thanks guys! Please use this space for discussion abo… read more", pn: 'foo-component' }, { id: 3, title: 'Passing a jwt to my REST adapter new ', body: "This sets up a binding between the category query param in the URL, and the category property on controller:articles. In other words, once the articles route has been entered, any changes to the category query param in the URL will update the category property on controller:articles, and vice versa.", pn: 'bar-component'} ]; }});
修改调用组件的模板index.hbs
。
{{#each model as |item|}} {{component item.pn post=item}}{{/each}}
模板编译之后会得到形如{{component foo-component post}}
的组件调用代码。
相信你应该了解了动态渲染组件是怎么回事了!自己动手试试吧~~
到此组件的定义与使用介绍完毕了,不知道你有没有学会呢?如果你有疑问请给我留言或者直接看官方教程学习。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
每个组件都是相对独立的,因此任何组件所需的数据都需要通过组件的属性把数据传递到组件中。
比如上篇Ember.js 入门指南之二十八组件定义的第三点{{component item.pn post=item}}
就是通过属性post把数据传递到组件foo-component
或者bar-component
上。如果在index.hbs
中是如下方式调用组件那么渲染之后的页面是空的。{{component item.pn}}
请读者自己修改index.hbs
的代码后演示效果。
传递到组件的参数也是动态更新的,当传递到组件上的参数变化时组件渲染的HTML也会随之发生改变。
传递的属性参数不一定要指定参数的名字。你可以不指定属性参数的名称,然后根据参数的位置获取对应的值,但是要在组件对应的组件类中指定位置参数的名称。比如下面的代码:
准备工作:
ember g route passing-properties-to-componentember g component passing-properties-to-component
调用组件的模板,传入两个位置参数,分别是item.title
、item.body
。
!-- apptemplatespassing-properties-to-component.hbs --{{#each model as item}} !-- 传递到组件blog-post第一个参数为数据的title值,第二个为body值 -- {{passing-properties-to-component item.title item.body}}{{each}}
准备需要显示的数据。
approutespadding-properties-to-component.jsimport Ember from 'ember';export default Ember.Route.extend({ model function() { return [ { id 1, title 'Bower dependencies and resolutions new', body In the bower.json file, I see 2 keys dependencies and resolutionsWhy is that so }, { id 2, title 'Highly Nested JSON Payload - hasMany error', body Welcome to the Ember.js discussion forum. We're running on the open source, Ember.js-powered Discourse forum software. }, { id 3, title 'Passing a jwt to my REST adapter new ', body This sets up a binding between the category query param in the URL, and the category property on controllerarticles. } ]; }});
在组件类中指定位置参数的名称。
appcomponentspadding-properties-to-component.jsimport Ember from 'ember';export default Ember.Component.extend({ 指定位置参数的名称 positionalParams ['title', 'body']});
注意:属性positionalParams指定的参数不能在运行期改变。
组件直接使用组件类中指定的位置参数名称获取数据。
!-- apptemplatescomponentspassing-properties-to-component.hbs --article h1{{title}}h1 p{{body}}particle
注意:获取数据的名称必须要与组件类指定的名称一致,否则无法正确获取数据。显示结果如下:
Ember还允许你指定任意多个参数,但是组件类获取参数的方式就需要做点小修改。比如下面的例子:
调用组件的模板
!-- apptemplatespassing-properties-to-component.hbs --{{#each model as item}} !-- 传递到组件blog-post第一个参数为数据的title值,第二个为body值 -- {{passing-properties-to-component item.title item.body 'third value' 'fourth value'}}{{each}}
指定参数名称的组件类,获取参数的方式可以Ember.js 入门指南之三计算属性这章。
appcomponentspadding-properties-to-component.jsimport Ember from 'ember';export default Ember.Component.extend({ 指定位置参数为参数数组 positionalParams 'params', title Ember.computed('params.[]', function() { return this.get('params')[0]; 获取第一个参数 }), body Ember.computed('params.[]', function() { return this.get('params')[1]; 获取第二个参数 }), third Ember.computed('params.[]', function() { return this.get('params')[2]; 获取第三个参数 }), fourth Ember.computed('params.[]', function() { return this.get('params')[3]; 获取第四个参数 })});
下面看组件是怎么获取传递过来的参数的。
!-- apptemplatescomponentspassing-properties-to-component.hbs --article h1{{title}}h1 p{{body}}p pthird {{third}}p pfourth {{fourth}}particle
显示结果如下:
到此组件参数传递的内容全部介绍完毕。总的来说没啥难度。Ember中参数的传递与获取方式基本是相似的,比如link-to助手、action助手。
br博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
准备工作:
ember g route wrapping-content-in-component-routeember g component wrapping-content-in-component
有些情况下,你需要定义一个包裹其他模板提供的数据的组件。比如下面的例子:
<h1>{{title}}</h1><div class='body'>{{body}}</div>
上述代码定义了一个普通的组件。
{{wrapping-content-in-component title=model.title body=model.body}}
调用组件,传入两个指定名称的参数,更多有关组件参数传递问题请看Ember.js 入门指南之二十九属性传递。
下面在route
中增加一些测试数据。
// app/routes/wrapping-content-in-component-route.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { return { id: 1, title: 'test title', body: 'this is body ...', author: 'ubuntuvim' }; }});
如果程序代码没写错,界面应该会显示如下信息。
在上述例子中组件正常显示出model
回调中初始化的数据。但是如果你定义的组件需要包含自定义的HTML内容呢??
除了上述这种简单的数据传递之外,Ember还支持使用block form
(块方式),换句话说你可以直接传递一个模板到组件中,并在组件中使用{{yield}}
助手显示传入进来的模板。
为了能使用块方式传递模板到组件中,在调用组件的时候必须使用#
开始的方式(两种调用方式:{{component-name}}
或者{{#component-name}}……{{/component-name}}
),注意一定要有关闭标签!
稍加改造前面的例子,这时候不只是传递一个简单的数据,而是传入一个包含HTML标签的简单模板。
<h1>{{title}}</h1><div class='body'>{{yield}}</div>
注意此时div
标签内使用的是{{yield}}
助手,而不是直接使用{{body}}
。
下面是调用组件的模板。
{{!wrapping-content-in-component title=model.title body=model.body}}{{#wrapping-content-in-component title=model.title}} {{model.body}} <small>by {{model.author}}</small>{{/wrapping-content-in-component}}
页面加载之后效果如下:
查看页面HTML源代码,可以看到在
按照惯例,先做好准备工作,使用Ember CLI命令生成演示所需的文件:
ember g route customizing-component-elementember g component customizing-component-elementember g route homeember g route about
默认情况下,组件会被包裹在div
标签内。比如,组件渲染之后得到下面的代码:
<div id="ember180" class="ember-view"> <h1>My Component</h1></div>
h1
标签就是组件的内容。以ember
开头的id
和class
都是Ember自动生成的。如果你需要修改渲染之后生成的HTML不是被包裹在div
标签,或者修改id
和class
等属性值为自定义的值,你可以在组件类中设置。
默认情况下,组件会被包裹在div
标签内,如果你需要修改这个默认值你可以在组件类中指定这个包裹的HTML标签。
// app/components/customizing-component-element.jsimport Ember from 'ember';export default Ember.Component.extend({ // 使用tabName属性指定渲染之后HTML标签 // 注意属性的值必须是标准的HTML标签名 tagName: 'nav'});
下面自定义一个组件。
<ul> <li>{{#link-to 'home'}}Home{{/link-to}}</li> <li>{{#link-to 'about'}}About{{/link-to}}</li></ul>
下面是调用组件的模板代码。注意组件被包裹在那个HTML标签内,正确情况下应该是被包裹在nav
标签中。
{{customizing-component-element}}
页面加载之后查看页面的源代码。如下:
可以看到组件customizing-component-element
的内容确实是被包裹在nav
标签之中,如果在组件类中没有使用属性tagName
指定包裹的HTML标签,默认是div
,你可以把组件类中tagName
属性删除之后再查看页面的HTML源码代码。
默认情况下,Ember会自动为包裹组件的HTML元素增加一个以ember
开头的类名,如果你需要增加自定义的CSS类,可以在组件类中使用className
数组属性指定,可以一次性指定多个CSS类。比如下面的代码例子:
// app/components/customizing-component-element.jsimport Ember from 'ember';export default Ember.Component.extend({ // 使用tabName属性指定渲染之后HTML标签 // 注意属性的值必须是标准的HTML标签名 tagName: 'nav', classNames: ['primary', 'my-class-name'] //指定包裹元素的CSS类});
页面重新加载之后查看源代码,可以看到nav
标签中多了两个CSS类,一个是primary
,一个是my-class-name
。
……
如果你想根据某个数据的值决定是否增加CSS类也是可以做到的,比如下面的代码,当urgent
为true
的时增加一个CSS类urgent
,否则不增加这个类。要达到这个目的可以通过属性classNameBindings
设置。
// app/components/customizing-component-element.jsimport Ember from 'ember';export default Ember.Component.extend({ // 使用tabName属性指定渲染之后HTML标签 // 注意属性的值必须是标准的HTML标签名 tagName: 'nav', classNames: ['primary', 'my-class-name'], //指定包裹元素的CSS类 classNameBindings: ['urgent'], urgent: true});
页面重新加载之后查看源代码,可以看到nav
标签中多了一个CSS类urgent
,如果属性urgent
的值为false
,CSS类urgent
将不会渲染到nav
标签上。
……
注意:classNameBindings
指定的属性值必须要跟用于判断数据的属性名一致,比如这个例子中classNameBindings
指定的属性值是urgent
,用户判断是否增加类的属性也是urgent
。如果这个属性只是驼峰式命名的那么渲染之后CSS类名将是以中划线-
分隔,比如classNameBindings
指定一个名为secondClassName
,渲染后的CSS类为second-class-name
。比如下面的演示代码:
// app/components/customizing-component-element.jsimport Ember from 'ember';export default Ember.Component.extend({ // 使用tabName属性指定渲染之后HTML标签 // 注意属性的值必须是标准的HTML标签名 tagName: 'nav', classNames: ['primary', 'my-class-name'], //指定包裹元素的CSS类 classNameBindings: ['urgent', 'secondClassName'], urgent: true, secondClassName: true});
页面重新加载之后查看源代码,可以看到nav
标签中多了一个CSS类second-class-name
。
……
如果你不想渲染之后的CSS类名被修改为中划线分隔形式,你可以值classNameBindings
属性中指定渲染之后的CSS类名。比如下面的代码:
// app/components/customizing-component-element.jsimport Ember from 'ember';export default Ember.Component.extend({ // 使用tabName属性指定渲染之后HTML标签 // 注意属性的值必须是标准的HTML标签名 tagName: 'nav', classNames: ['primary', 'my-class-name'], //指定包裹元素的CSS类 classNameBindings: ['urgent', 'secondClassName:scn'], //指定secondClassName渲染之后的CSS类名为scn urgent: true, secondClassName: true});
页面重新加载之后查看源代码,可以看到nav
标签中原来CSS类为second-class-name
的变成了scn
。
……
有没有感觉Ember既灵活又强大!!Ember的设计理念是“约定优于配置”!所以很多的属性默认的设置都是我们平常开发中最常用的格式。
除了上述可以指定CSS类名之外,还可以在classNameBindings
增加简单的逻辑,特别是在处理一些动态效果的时候上述特性是非常有用的。
// app/components/customizing-component-element.jsimport Ember from 'ember';export default Ember.Component.extend({ // 使用tabName属性指定渲染之后HTML标签 // 注意属性的值必须是标准的HTML标签名 tagName: 'nav', classNames: ['primary', 'my-class-name'], //指定包裹元素的CSS类 classNameBindings: ['urgent', 'secondClassName:scn', 'isEnabled:enabled:disabled'], urgent: true, secondClassName: true, isEnabled: true //如果这个属性为true,类enabled将被渲染到nav标签上,如果属性值为false类disabled将被渲染到nav标签上,类似于三目运算});
正如代码的注释所说的,isEnabled:enabled:disabled
可以理解为一个三目运算,会根据isEnabled
的值渲染不同的CSS类到nav
上。
下面的HTML代码是isEnabled
为true
的情况,对于isEnabled
为false
的情况请读者自己试试:
……
注意:如果用于判断的属性值不是一个Boolean
值而是一个字符串那么得到的结果与上面的结果是不一样的,Ember会直接把这个字符串的值作为CSS类名渲染到包裹的标签上。比如下面的代码:
// app/components/customizing-component-element.jsimport Ember from 'ember';export default Ember.Component.extend({ // 使用tabName属性指定渲染之后HTML标签 // 注意属性的值必须是标准的HTML标签名 tagName: 'nav', classNames: ['primary', 'my-class-name'], //指定包裹元素的CSS类 classNameBindings: ['urgent', 'secondClassName:scn', 'isEnabled:enabled:disabled', 'stringValue'], urgent: true, secondClassName: true, isEnabled: true, //如果这个属性为true,类enabled将被渲染到nav标签上,如果属性值为false类disabled将被渲染到nav标签上,类似于三目运算 stringValue: 'renderedClassName'});
此时页面的HTML源码就有点不一样了。renderedClassName
作为CSS类名被渲染到了nav
标签上。
……
对于这点需要特别注意。Ember对于Boolean
值和其他值的判断结果是不一样的。
在前面两点介绍了包裹组件的HTML元素的标签名、CSS类名,在HTML标签上出来CSS类另外一个最常用的就是属性,那么Ember同样提供了自定义包裹HTML元素的属性的方法。使用attributeBindings
属性指定,这个属性的属性方式与classNameBindings
基本一致。为了与前面的例子区别开新建一个组件link-items
,使用命令ember g component link-items
创建。
这是个组件
在模板中调用组件。
{{customizing-component-element}}<br><br>{{link-items}}
下面设置组件类,指定包裹的HTML标签为a
标签,并增加一个属性href
。
// app/components/link-items.jsimport Ember from 'ember';export default Ember.Component.extend({ tagName: 'a', attributeBindings: ['href'], href: 'http://www.google.com.hk'});
页面重新加载之后得到如下结果:
比较简单,对于渲染之后的结果我就不过多解释了,请参考classNameBindings
属性理解。
到此,有关于组件渲染之后包裹组件的HTML标签的相关设置介绍完毕。内容不多,classNameBindings
和attributeBindings
这两个属性的使用方式基本相同。如有疑问欢迎给我留言或者直接查看官方教程。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
你可以在组件中响应事件,比如用户的双击、鼠标滑过、键盘的按下等等事件。只需要在组件类中增加Ember提供的处理事件,然后Ember会自动判断用户的操作执行相应的事件,只要在组件类中添加的事件不冲突你甚至一次性增加多个事件,事件执行次序根据触发的次序执行。
1,简单事件处理准备工作,使用Ember CLI创建演示所需文件:
ember g component handle-eventsember g route component-route
生成的组件模板不做任何修改。
{{yield}}
注意看组件类的实现:
// app/components/handle-events.jsimport Ember from 'ember';export default Ember.Component.extend({ click: function() { alert('click...'); return true; // 返回true允许事件冒泡到父组件 }, mouseLeave: function() { alert("mouseDown...."); return true; }});
在组件类中增加了两个事件click
和mouseLeaver
,一个是单击事件一个是鼠标移开事件,更多Ember支持的事件请看handling-events。
调用组件的模板如下:
{{#handle-events}}<span style="cursor: pointer;">从我身上飘过或者点我都会触发事件~</span>{{/handle-events}}
当用户只是把鼠标从文字“从我身上飘过或者点我都会触发事件~”上划过市只执行mouseLeave
事件,当用户点击文字时先执行click
事件再执行mouseLeave
事件,因为用户点击文字时鼠标还没移开。
但是如果你增加的事件是有冲突的可能会得到无法预知的结果,比如在组件类中增加了双击和单击事件,此时只会执行单击事件,双击事件就无法触发。
某些情况下,你的组件需要支持拖放事件。比如组件可能要发送一个id
到drop
事件中。
{{drop-target action=”didDrop”}}
你可以定义组件的事件处理器去管理drop
事件。如果有需要可以通过返回false
防止事件冒泡。
// app/components/drop-target.jsimport Ember from 'ember';export default Ember.Component.extend({ attribuBindings: ['draggable'], draggable: 'true', dragOver: function() { return false; }, didDrop: function(event) { let id = event.dataTransfer.getData('text/data'); this.sendAction('action', id); }});
本章内容不多,重点是第一点的内容,第二点的内容就简单介绍,更多详细信息请移步官网文档。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
组件就像一个相对独立的盒子。在前面的文章中介绍过组件是怎么通过属性传递参数,并且这个属性值你可以在模板或者js代码中获取。
但是到目前为止还没介绍过子组件从父组件中获取数组,在Ember应用中组件之间的通信是通过actions
实现的。
跟着下面的步骤来,创建一个组件之间通信的示例。
创建组件的方法不用我多说,直接使用Ember CLI命令创建即可。
ember g component button-with-confirmationember g component user-profileember g route button-with-confirmation-route
为了测试方便多增加了一个路由。
下面是组件user-profile
的定义,调用组件button-with-confirmation
,那么此时user-profile
作为父组件,button-with-confirmation
作为子组件:
{{button-with-confirmation text="Click OK to delete your account"}}
要想action
能执行需要作如下两步:
下面是实现代码:
实现父组件动作(action)
在父组件中,定义好当用户点击“确认”之后触发的动作。在这个例子中的动作(action
)是先找出用户账号再删除。
在Ember应用中,每个组件都有一个名为actions
的属性。这个属性值是函数,是可以被用户或者子组件执行的函数。
// app/components/user-profile.jsimport Ember from 'ember';export default Ember.Component.extend({ actions: { userDidDeleteAccount: function() { console.log(“userDidDeleteAccount…”); } }});
现在已经实现了父组件逻辑,但是并没有告诉Ember这个动作什么时候触发,下一步将实现这个功能。
实现子组件动作(action)
这一步我们将实现当用户点击“确定”之后触发事件的逻辑。
// app/components/button-with-confirmation.jsimport Ember from 'ember';export default Ember.Component.extend({ tagName: 'button', click: function() { if (confirm(this.get('text'))) { // 在这里获取父组件的事件(数据)并触发 } }});
现在我们在user-profile
组件中使用onConfirm()
方法触发组件button-with-confirmation
类中的userDidDeleteAccount
事件。
{{#button-with-confirmation text="Click OK to delete your account" onConfirm=(action 'userDidDeleteAccount')}}执行userDidDeleteAccount方法{{/button-with-confirmation}}
这段代码的意思是告诉父组件,userDidDeleteAccount
方法会通过onConfirm
方法执行。
现在你可以在子组件中使用onConfirm
方法执行父组件的动作。
// app/components/button-with-confirmation.jsimport Ember from 'ember';export default Ember.Component.extend({ tagName: 'button', click: function() { if (confirm(this.get('text'))) { // 在父组件中触发动作 this.get('onConfirm')(); } }});
this.gete(“onConfirm”)
从父组件返回一个值onConfirm
,然后与()
组合成了一个个方法onConfirm()
。
在模板button-with-confirmation-route.hbs
中调用组件。
{{user-profile}}
点击这个button
,会触发事件。弹出对话框。再点击“确认”后执行方法userDidDeleteAccount
,可以看到浏览器控制台输出了userDidDeleteAccount…,未点击“确认”前或者点击“取消”不会输出这个信息,说明不执行这个方法userDidDeleteAccount
。
像普通属性,actions
可以组件的一个属性,唯一的区别是,属性设置为一个函数,它知道如何触发的行为。
在组件的actions
属性中定义的方法,允许你决定怎么去处理一个事件,有助于模块化,提高组件重用率。
到此,组件这一章节的内容全部介绍完毕了,不知道你看懂了多少?如果有疑问请给我留言一起交流学习,获取是直接去官网学习官方教程。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
对于组件这一章是非常重要的,组件会在3.0
之后的版本替代控制器。
这一章最重要的内容包括如下几篇博文:
要想学好组件必须多看上述几篇文章。特别是第四篇,介绍了组件的生命周期,对于理解组件的原理是非常有帮助的。
组件到此也介绍完毕了,其中很重要的两个知识点没有放到博客中,请自行从官方参考文档学习。在接下来的一章将为大家介绍控制器(controller
),虽然控制器会在3.0
版本中被移除,但是目前仍然是支持的,所以还需要简单讲解。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
从本篇开始进入第五章控制器,controller
在Ember2.0
开始越来越精简了,职责也更加单一——处理逻辑。
下面是准备工作。重新创建一个Ember项目,仍旧使用的是Ember CLI命令创建。
ember new chapter5_controllerscd chapter5_controllersember server
在浏览器执行项目,看到如下信息说明项目搭建成功。Welcome to Ember。
控制器与组件非常相似,由此,在未来的新版本中很有可能组件将会完全取代控制器,很可能随着Ember版本的更新控制器将退出Ember。目前的版本中组件还不能直接通过路由访问,需要通过模板调用才能使用组件,但是未来的版本会解决这个问题,到时候controller
可能就真的从Ember退出了!
正因如此,模块化的Ember应用很少用到controller
。即便是使用了controller
也是为了处理下面的两件事情:
controller
主要是为了维持当前路由状态。一般来说,model的属性会保存到服务器,但是controller
的属性却不会保存到服务器。controller
层转到route
层。模板上下文的渲染是通过当前controller
的路由处理的。Ember所追随的理念是“约定优于配置”,这也就意味着如果你只需要一个controller
你就创建一个,而不是一切为了“便于工作”。
下面的例子是演示路由显示blog post
。假设模板blog-post
用于展示模型blog-post
的数据,并在这个模型包含如下属性(隐含属性id
,因为在model
中不需要手动指定id
属性):
model
定义如下:
// app/models/blog-post.jsimport DS from 'ember-data';export default DS.Model.extend({ title: DS.attr('string'), // 属性默认为string类型,可以不指定 intro: DS.attr('string'), body: DS.attr('string'), author: DS.attr('string')});
在route
层增加测试数据,直接返回一个model
对象。
// app/routes/blog-post.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { var blogPost = this.store.createRecord('blog-post', { title: 'DEFINING A COMPONENT', // 属性默认为string类型,可以不指定 intro: "Components must have at least one dash in their name. ", body: "Components must have at least one dash in their name. So blog-post is an acceptable name, and so is audio-player-controls, but post is not. This prevents clashes with current or future HTML element names, aligns Ember components with the W3C Custom Elements spec, and ensures Ember detects the components automatically.", author: 'ubuntuvim' }); // 直接返回一个model,或者你可以返回promises, return blogPost; }});
显示信息的模板如下:
<h1>{{model.title}}</h1><h2>{{model.author}}</h2><div class="intro"> {{model.intro}}</div><hr><div class="body"> {{model.body}}</div>
如果你的代码没有编写错误那么也会得到如下结果:
Welcome to Ember是主模板的信息,你可以在application.hbs
中删除,但是记得不要删除{{outlet}}
,否则什么信息也不显示。
这个例子中没有显示任何特定的属性或者指定的动作(action
)。此时,控制器的model属性所扮演的角色仅仅是模型属性的pass-through
(或代理)。注意:控制器获取的model
是从route
得到的。
下面为这个例子增加一个功能:用户可以点击标题触发显示或者隐藏post
的内容。通过一个属性isExpanded
控制,下面分别修改模板和控制器的代码。
// app/controllers/blog-post.jsimport Ember from 'ember';export default Ember.Controller.extend({ isExpanded: false, //默认不显示body actions: { toggleBody: function() { this.toggleProperty('isExpanded'); } }});
在controller
中增加一个属性isExpanded
,如果你不在controller
中定义这个属性也是可以的。对于这个controller
代码的解释请看Ember.js 入门指南之十五{{action}} 助手。
<h1>{{model.title}}</h1><h2>{{model.author}}</h2><div class="intro"> {{model.intro}}</div><hr>{{#if isExpanded}} <button {{action 'toggleBody'}}>hide body</button> <div class="body"> {{model.body}} </div>{{else}} <button {{action 'toggleBody'}}>Show body</button>{{/if}}
在模板中使用if
助手判断isExpanded
的值,如果为true
则显示body
,否则不显示。
页面加载之后结果如下,首先是不显示body
内容,点击按钮“Show body”则显示内容,并且按钮变为“hide body”。然后在点击这个按钮则不显示body
内容。
到此controller
的职责你应该大致了解了,其主要的作用是逻辑的判断、处理,比如这里例子中判断body
内容的显示与否,其实你也可以把controller
类中的处理代码放在route
类中也可以实现这个效果,但是要作为model
的属性返回(把isExpanded
当做model
的属性处理),请读者自己动手试试,但是把逻辑放到route
又会使得route
变得“不专一”了,route
的主要职责是初始化数据的。我想这也是Ember还留着controller
的原因之一吧!!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
在有路由嵌套的情况下,你可能需要在两个不同的controller
之间通信。按照惯例先做准备工作:
ember g route postember g route post/commentsember g model post
比如下面的路由设置:
// router.jsimport Ember from 'ember';import config from './config/environment';var Router = Ember.Router.extend({ location: config.locationType});Router.map(function() { this.route('blog-post'); this.route('post', { path: '/posts/:post_id' }, function() { this.route('comments'); });});export default Router;
对于这个路由配置生成的路由表请看Ember.js 入门指南之十三{{link-to}} 助手。
如果用户访问/posts/1/comments
。模型post
就会加载到postController
,并不会直接加载到commentsController
。然后如果你想在一篇post
中显示comment
信息呢?
为了实现这个功能,可以把postController
注入到commentController
中。
// app/controllers/comments.jsimport Ember from 'ember';export default Ember.Controller.extend({ postController: Ember.inject.controller('post')});
一旦comments
路由被访问,postController
就会获取控制器对应的model
,并且这个model
是只读的。为了能获取到模型post
还需要增加一个引用postController.model
。
// app/controllers/comments.jsimport Ember from 'ember';export default Ember.Controller.extend({ postController: Ember.inject.controller('post'), post: Ember.computed.reads('postController.model')});
最后可以直接在comment
模板中显示模型post
和comment
的信息。
<h1>Comments for {{post.title}}</h1><ul> {{#each model as |comment|}} <li>{{comment.text}}</li> {{/each}}</ul>
有关更多别名的介绍请移步这里查看API文档的介绍。如果你想了解更多关于注入的问题请看这里的教程(新版官网已经没有这个地址的文档了)。
controller
这章的内容到此也全部介绍完毕了,只有寥寥的2篇教程,可见controller
在Ember未来版本会被组件替代已成必然。
那么下一章将为大伙介绍模型,模型对于Ember来说是一块非常重要的内容,内容也比较多!我回用9篇文章来给你介绍模型,从定义到其使用等等内容。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
Ember官网用了大篇幅来介绍model
,相比之前的controller
简直就是天壤之别啊!
从本篇开始学习Ember的模型,这一章也是Ember基础部分的最后一章内容,非常的重要(不管你信不信反正我是信了)。
在开始学习model
之前先做好准备工作:重新创建一个Ember项目,仍旧使用的是Ember CLI命令创建。
ember new chapter6_modelscd chapter6_modelsember server
在浏览器执行项目,看到如下信息说明项目搭建成功。Welcome to Ember
本章演示所用到的代码都可以从https://github.com/ubuntuvim/my_emberjs_code/tree/master/chapter6_models获取。
在介绍model
之前先在项目中引入firebase。相关的配置教材请移步这里(如果无法加载页面请先在https://www.firebase.com/注册用户)。firebase的官网提供了专门用于Ember的版本,还提供了非常简单的例子。从安装到整合都给出了非常详细代码教程。下面是我的整合步骤(命令都是在项目目录下执行的):
ember install emberfire
安装完成之后会自动创建adapter(app/adapters/application.js)
,对于这个文件不需要做任何修改,官网提供的代码也许跟你的项目的代码不同,应该是官网的版本是旧版的。
config/environment.js
修改第八行firebase: 'https://YOUR-FIREBASE-NAME.firebaseio.com/'
。这个地址是你注册用户时候得到的。你可以从这里查看你的地址。比如下图所示位置config/enviroment.js
的APP:{}
(大概第20行)后面新增如下代码APP: { // Here you can pass flags/options to your application instance // when it is created},contentSecurityPolicy: { 'default-src': "'none'", 'script-src': "'self' 'unsafe-inline' 'unsafe-eval' *", 'font-src': "'self' *", 'connect-src': "'self' *", 'img-src': "'self' *", 'style-src': "'self' 'unsafe-inline' *", 'frame-src': "*"}
然后再注释掉第7行原有属性(安装firebase
自动生成的,但是配置不够完整):contentSecurityPolicy
。
或者你可以参考我的配置文件:
/* jshint node: true */module.exports = function(environment) { var ENV = { modulePrefix: 'chapter6-models', environment: environment, // contentSecurityPolicy: { 'connect-src': "'self' https://auth.firebase.com wss://*.firebaseio.com" }, firebase: '你的firebase连接', baseURL: '/', locationType: 'auto', EmberENV: { FEATURES: { // Here you can enable experimental features on an ember canary build // e.g. 'with-controller': true } }, APP: { // Here you can pass flags/options to your application instance // when it is created }, contentSecurityPolicy: { 'default-src': "'none'", 'script-src': "'self' 'unsafe-inline' 'unsafe-eval' *", 'font-src': "'self' *", 'connect-src': "'self' *", 'img-src': "'self' *", 'style-src': "'self' 'unsafe-inline' *", 'frame-src': "*" } }; // 其他代码省略…… return ENV;};
如果不做这个配置启动项目之后浏览器会提示一堆的错误。主要是一些访问权限问题。配置完之后需要重启项目才能生效!
model
是一个用于向用户呈现底层数据的对象。不同的应用有不同的model
,这取决于解决的问题需要什么样的model
就定义什么样的model
。model
通常是持久化的。这也就意味着用户关闭了浏览器窗口model
数据不应该丢失。为了确保model
数据不丢失,你需要存储model
数据到你所指定的服务器或者是本地数据文件中。
一种非常常见的情况是,model
数据会以JSON
的格式通过HTTP
发送到服务器并保存在服务中。Ember还未开发者提供了一种更加简便的方式:使用IndexedDB(使用在浏览器中的数据库)。这种方式是把model
数据保存到本地。或者使用Ember Data,又或者使用firebase,把数据直接保存到远程服务器上,后续的文章我将引入firebase,把数据保存到远程服务器上。
Ember使用适配器模式连接数据库,可以适配不同类型的后端数据库而不需要修改任何的网络代码。你可以从emberobserver上看到几乎所有Ember支持的数据库。
如果你想把你的Ember应用与你的远程服务器整合,几遍远程服务器API
返回的数据不是规范的JSON
数据也不要紧,Ember Data可以配置任何服务器返回的数据。
Ember Data还支持流媒体服务器,比如WebSocket。你可以打开一个socket连接远程服务器,获取最新的数据或者把变化的数据推送到远程服务器保存。
Ember Data为你提供了更加简便的方式操作数据,统一管理数据的加载,降低程序复杂度。
对于model
与Ember Data的介绍就到此为止吧,官网用了大量篇幅介绍Model,在此我就不一一写出来了!太长了,写出来也没人看的!!!如果有兴趣自己看吧!点击查看详细信息。
下面先看一个简单的例子,由这个例子延伸出有关于model
的核心概念。这些代码是旧版写法,仅仅是为了说明问题,本文也不会真正执行。
// app/components/list-of-drafts.jsexport default Ember.Component.extend({ willRender() { // ECMAScript 6语法 $.getJSON('/drafts').then(data => { this.set('drafts', data); }); }});
定义了一个组件类。并在组件类中获取json
格式数据。下面是组件对应的模板文件。
<ul> {{#each drafts key="id" as |draft|}} <li>{{draft.title}}</li> {{/each}}</ul>
再定义另外一个组件类和模板
// app/components/list-button.jsexport default Ember.Component.extend({ willRender() { // ECMAScript 6语法 $.getJSON('/drafts').then(data => { this.set('drafts', data); }); }});
{{#link-to ‘drafts’ tagName=’button’}}Drafts ({{drafts.length}}){{/link-to}}
组件list-of-drafts
类和组件list-button
类是一样的,但是他们的对应的模板却不一样。但是都是从远程服务器获取同样的数据。如果没有Store
(model
核心内容之一)那么每次这两个模板渲染都会是组件类调用一次远程数据。并且返回的数据是一样的。这无形中增加了不必要的请求,暂用了不必要的宽带,用户体验也不好。但是有了Store
就不一样了,你可以把Store
理解为仓库,每次执行组件类时先到Store
中获取数据,如果没有再去远程获取。当在其中一个组件中改变某些数据,数据的更改也能理解反应到另一个获取此数据的组件上(与计算属性自动更新一样),而这个组件不需要再去服务请求才能获取最新更改过的数据。
下面的内容将为你一一介绍Ember Data最核心的几个东西:models
、records
、adapters
、store
。
声明:下面简介内摘抄至http://www.emberjs.cn/guides/models/#toc_。
store
是应用存放记录的中心仓库。你可以认为store
是应用的所有数据的缓存。应用的控制器和路由都可以访问这个共享的store
;当它们需要显示或者修改一个记录时,首先就需要访问store
。
DS.Store
的实例会被自动创建,并且该实例被应用中所有的对象所共享。
store
可以看做是一个缓存。在下面的cache
会结合store
介绍。
下面的例子结合firebase演示:创建路由和model
:
ember g route store-exampleember g model article
// app/models/article.jsimport DS from 'ember-data';export default DS.Model.extend({ title: DS.attr('string'), body: DS.attr('string'), timestamp: DS.attr('number'), category: DS.attr('string')});
这个就是model
,本章要讲的内容就是它!为何没有定义id属性呢?Ember
会默认生成id
属性。
我们在路由的model
回调中获取远程的数据,并显示在模板上。
// app/routes/store-example.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 从store中获取id为JzySrmbivaSSFG6WwOk的数据,这个数据是我在我的firebase中初始化好的 return this.store.find('article', '-JzySrmbivaSSFG6WwOk'); }});
find
方法的第一个参数是model
类名,第二个参数对象的id
属性值。记得id属性不需要在model
类中手动定义,Ember会自动为你定义。
<h1>{{model.title}}</h1><div class="body">{{model.body}}</div>
页面加载之后可以看到获取到的数据。
下面是我的firebase上的部分数据截图。
可以看到成功获取到id
为-JzySrmbivaSSFG6WwOk
的数据。更多关于数据的操作在后面会详细介绍。
有关model
的概念前面的简介已经介绍了,这里不再赘述。model
定义:
model
是由若干个属性构成的。attr
方法的参数指定属性的类型。
export default DS.Model.extend({ title: DS.attr('string'), // 字符串类型 flag: DS.attr('boolean'), // 布尔类型 timestamp: DS.attr('number'), // 数字类型 birth: DS.attr(‘date’) //日期类型});
模型也声明了它与其他对象的关系。例如,一个Order
可以有许多LineItems
,一个LineItem
可以属于一个特定的Order
。
App.Order = DS.Model.extend({ lineItems: DS.hasMany('lineItem')});App.LineItem = DS.Model.extend({ order: DS.belongsTo('order')});
这个与数据的表之间的关系是一样的。
record
是model
的实例,包含了从服务器端加载而来的数据。应用本身也可以创建新的记录,以及将新记录保存到服务器端。
记录由以下两个属性来唯一标识:
比如前面的实例article
就是通过find
方获取。获取到的结果就是一个record
。
适配器是一个了解特定的服务器后端的对象,主要负责将对记录(record
)的请求和变更转换为正确的向服务器端的请求调用。
例如,如果应用需要一个ID
为1
的person
记录,那么Ember Data是如何加载这个对象的呢?是通过HTTP,还是Websocket?如果是通过HTTP,那么URL会是/person/1
,还是/resources/people/1
呢?
适配器负责处理所有类似的问题。无论何时,当应用需要从store
中获取一个没有被缓存的记录时,应用就会访问适配器来获取这个记录。如果改变了一个记录并准备保存改变时,store
会将记录传递给适配器,然后由适配器负责将数据发送给服务器端,并确认保存是否成功。
store
会自动缓存记录。如果一个记录已经被加载了,那么再次访问它的时候,会返回同一个对象实例。这样大大减少了与服务器端的往返通信,使得应用可以更快的为用户渲染所需的UI。
例如,应用第一次从store
中获取一个ID
为1
的person
记录时,将会从服务器端获取对象的数据。
但是,当应用再次需要ID
为1
的person
记录时,store
会发现这个记录已经获取到了,并且缓存了该记录。那么store
就不会再向服务器端发送请求去获取记录的数据,而是直接返回第一次时候获取到并构造出来的记录。这个特性使得不论请求这个记录多少次,都会返回同一个记录对象,这也被称为Identity Map
(标识符映射)。
使用标识符映射非常重要,因为这样确保了在一个UI上对一个记录的修改会自动传播到UI其他使用到该记录的UI。同时这意味着你无须手动去保持对象的同步,只需要使用ID
来获取应用已经获取到的记录就可以了。
应用第一次从store
获取一个记录时,store
会发现本地缓存并不存在一份被请求的记录的副本,这时会向适配器发请求。适配器将从持久层去获取记录;通常情况下,持久层都是一个HTTP服务,通过该服务可以获取到记录的一个JSON
表示。
如上图所示,适配器有时不能立即返回请求的记录。这时适配器必须向服务器发起一个异步的请求,当请求完成加载后,才能通过返回的数据创建的记录。
由于存在这样的异步性,store
会从find()
方法立即返回一个承诺(promise
)。另外,所有请求需要store
与适配器发生交互的话,都会返回承诺。一旦发给服务器端的请求返回被请求记录的JSON数据时,适配器会履行承诺,并将JSON
传递给store
。store
这时就获取到了JSON
,并使用JSON
数据完成记录的初始化,并使用新加载的记录来履行已经返回到应用的承诺。
下面将介绍一下当store
已经缓存了请求的记录时会发生什么。
在这种情形下,store
已经缓存了请求的记录,不过它也将返回一个承诺,不同的是,这个承诺将会立即使用缓存的记录来履行。此时,由于store
已经有了一份拷贝,所以不需要向适配器去请求(没有与服务器发生交互)。
models
、records
、adapters
、store
是你必须要理解的概念。这是Ember Data最核心的东西。
有关于上述的概念将会在后面的文章一一用代码演示。理解了本文model
这一整章的内容都不是问题了!!!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
模型也是一个类,它定义了向用户展示的属性和数据行为。模型的定义非常简单,只需要继承DS.Model类即可,或者你也可以直接使用Ember CLI命令创建。比如使用命令模型 ember g model person
定义了一个模型类person
。
// app/models/person.jsimport DS from 'ember-data';export default DS.Model.extend({ });
这个是个空的模型,没有定义任何属性。有了模型类你就可以使用find
方法查找数据了。
上面定义的模型类person
还没有任何属性,下面为这个类添加几个属性。
// app/models/person.jsimport DS from 'ember-data';export default DS.Model.extend({ firstName: DS.attr(), lastName: DS.attr(), birthday: DS.attr() });
上述代码定义了3个属性,但是还未给属性指定类型,默认都是string
类型。这些属性名与你连接的服务器上的数据key
是一致的。甚至你还可以在模型中定义计算属性。
// app/models/person.jsimport DS from 'ember-data';export default DS.Model.extend({ firstName: DS.attr(), lastName: DS.attr(), birthday: DS.attr(), fullName: Ember.computed('firstName', 'lastName', function() { return `${this.get('firstName')} ${this.get('lastName')}`; })});
这段代码在模型类中定义了一个计算属性fullName
。
前面定义的模型类是没有指定属性类型的,默认情况下都是string
类型,显然这是不够的,简单的模型属性类型包括:string
,number
,boolean
,date
。这几个类型我想不用我解释都应该知道了。
不仅可以指定属性类型,你还可以指定属性的默认值,在attr()方法的第二个参数指定。比如下面的代码:
// app/models/person.jsimport DS from 'ember-data';export default DS.Model.extend({ username: DS.attr('string'), email: DS.attr('string'), verified: DS.attr('boolean', { defaultValue: false }), //指定默认值是false // 使用函数返回值作为默认值 createAt: DS.attr('string', { defaultValue(){ return new Date(); } })});
正如代码注释所述的,设置默认值的方式包括直接指定或者是使用函数返回值指定。
Ember的模型也是有类似于数据库的关联关系的。只是相对于复制的数据库Ember的模型就显得简单很多,其中包括一对一,一对多,多对多关联关系。这种关系是与后台的数据库是相统一的。
声明一对一关联使用DS.belongsTo设置。比如下面的两个模型。
// app/models/user.jsimport DS from 'ember-data';export default DS.Model.extend({ profile: DS.belongsTo(‘profile’);});
// app/models/profile.jsimport DS from ‘ember-data’;export default DS.Model.extend({ user: DS.belongsTo(‘user’);});
声明一对多关联使用DS.belongsTo(多的一方使用)和DS.hasMany(少的一方使用)设置。比如下面的两个模型。
// app/models/post.jsimport DS from ‘ember-data’;export default DS.Model.extend({ comments: DS.hasMany(‘comment’);});
这个模型是一的一方。下面的模型是多的一方;
// app/models/comment.jsimport DS from ‘ember-data’;export default DS.Model.extend({ post: DS.belongsTo(‘post’);});
这种设置的方式与Java 的hibernate非常相似。
声明一对多关联使用DS.hasMany设置。比如下面的两个模型。
// app/models/post.jsimport DS from ‘ember-data’;export default DS.Model.extend({ tags: DS.hasMany(‘tag’);});
// app/model/tag.jsimport DS from ‘ember-data’;export default DS.Model.extend({ post: DS.hasMany(‘post’);});
多对多的关系设置都是使用DS.hasMany,但是并不需要“中间表”,这个与数据的多对多有点不同,如果是数据的多对多通常是通过中间表关联。
Ember Data会尽力去发现两个模型之间的关联关系,比如前面的一对多关系中,当comment
发生变化的时候会自动更新到post
,因为每一个comment
只对应一个post
,可以有comment
确定到某个一个post
。
然而,有时候同一个模型中会有多个与此关联模型。这时你可以在反向端用DS.hasMany的inverse
选项指定其关联的模型:
// app/model/comment.jsimport DS from 'ember-data';export default DS.Model.extend({ onePost: DS.belongsTo(‘post’), twoPost: DS.belongsTo(‘post’), redPost: DS.belongsTo(‘post’), bluePost: DS.belongsTo(‘post’)});
在一个模型中同时与3个post
关联了。
// app/models/post.jsimport DS from ‘ember-data’;export default DS.Model.extend({ comments: hasMany(‘comment’, { inverse: ‘redPost’ });});
当comment
发生变化时自动更新到redPost
这个模型。
当你想定义一个自反关系的模型时(模型本身的一对一关系),你必须要显式使用inverse
指定关联的模型。如果没有逆向关系则把inverse
值设置为null
。
// app/models/folder.jsimport DS from ‘ember-data’;export default DS.Model.extend({ children: DS.hasMany(‘folder’, { reverse: ‘parent’ }); parent: DS.hasMany(‘folder’, { reverse: ‘children’ });});
一个文件夹通常有父文件夹或者子文件夹。此时父文件夹和子文件夹与本身都是同一个类型的模型。此时你需要显式使用inverse
属性指定,比如这段代码所示,“children……”这行代码意思是这个模型有一个属性children
,并且这个属性也是一个folder
,模型本身作为父文件夹。同理“parent……”这行代码的意思是这个模型有个属性parent
,并且这个属性也是一个folder
,模型本身是这个属性的子文件夹。比如下图结构:
这个有点像数据结构中的链表。你可以把children
和parent
想象成是一个指针。
如果仅有关联关系没有逆向关系直接把inverse
设置为null
。
// app/models/folder.jsimport DS from ‘ember-data’;export default DS.Model.extend({ parent: DS.belongsTo(‘folder’, { inverse: null });});
// app/models/user.jsimport DS from ‘ember-data’;export default DS.Model.extend({ bestFriend: DS.belongsTo(‘folder’, { inverse: ‘bestFriend’ });});v
这个关系与数据库设置设计中双向一对一很类似。
有些模型可能会包含深层嵌套的数据对象,如果也是使用上述的关联关系定义那么将是个噩梦!对于这种情况最好是把数据定义成简单对象,虽然增加点冗余数据但是降低了层次。另外一种是把嵌套的数据定义成模型的属性(也是增加冗余但是降低了嵌套层次)。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
store提供了统一的获取数据的接口。包括创建新记录、修改记录、删除记录等,更多有关Store API请点击网址看详细信息。
为了演示这些方法的使用我们结合firebase,关于firebase与Ember的整合前面的文章已经介绍,就不过多介绍了。做好准备工作:
ember g route articlesember g route articles/article
首先配置route
,修改子路由增加一个动态段article_id
,有关动态的介绍请看Dynamic Segments。
// app/router.js// 其他代码略写,Router.map(function() { this.route('store-example'); this.route('articles', function() { this.route('article', { path: '/:article_id' }); });});
下面是路由代码,这段代码直接调用Store的find方法,返回所有数据。
// app/routes/articles.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 返回firebase数据库中的所有article return this.store.findAll('article'); }});
为了界面看起来舒服点我引入了bootstrap框架。引入的方式:bower install bootstrap
安装插件。然后修改ember-cli-build.js
,在return
之前引入bootstrap:
app.import("bower_components/bootstrap/dist/js/bootstrap.js");app.import("bower_components/bootstrap/dist/css/bootstrap.css");
重启项目使其生效。
下面是显示数据的模板articles.hbs
。
<div class="container"> <div class="row"> <div class="col-md-4 col-xs-4"> <ul class="list-group"> {{#each model as |item|}} <li class="list-group-item"> {{#link-to 'articles.article' item.id}} {{item.title}} {{/link-to}} </li> {{/each}} </ul> </div> <div class="col-md-8 col-xs-8"> {{outlet}} </div> </div></div>
在浏览器运行:http://localhost:4200/articles/。稍等就可以看到显示的数据了,等待时间与你的网速有关。毕竟firebase不是在国内的!!!如果程序代码没有写错那么你会看到如下图的结果:
但是右侧是空白的,下面点击任何一条数据,可以看到右侧什么都不显示!下面在子模板中增加显示数据的代码:
<h1>{{model.title}}</h1><div class = "body">{{model.body}}</div>
在点击左侧的数据,右侧可以显示对应的数据了!但是这个怎么就显示出来了呢??其实Ember自动根据动态段过滤了,当然你也可以显示使用findRecord
方法过滤。
// app/routes/articles/article.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { console.log('params = ' + params.article_id); // 'chendequanroob@gmail.com' return this.store.findRecord('article', params.article_id); }});
此时得到的结果与不调用findRecord
方法是一致的。为了验证是不是执行了这个方法,我们把动态段params.article_id
的值改成一个不存在的值’ ubuntuvim’,可以确保的是在我的firebase数据中不存在id
为这个值的数据。此时控制台会出现下面的错误信息,从错误信息可以看出来是因为记录不存在的原因。
在上述的例子中,我们使用了findAll()
方法和findRecord()
方法,还有两个方法与这两个方法是类似的,分别是peekRecord()
和peekAll()
方法。这两个方法的不同之处是不会发送请求,他们只会在本地缓存中获取数据。
下面分别修改articles.js
和article.js
这两个路由。使用peekRecord()
和peekAll()
方法测试效果。
// app/routes/articles.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 返回firebase数据库中的所有article // return this.store.findAll('article'); return this.store.peekAll('article'); }});
由于没有发送请求,我也没有把数据存储到本地,所以这个调用什么数据都没有。
// app/routes/articles/article.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function(params) { // return this.store.findRecord('article', params.article_id); return this.store.peekRecord('article', params.article_id); }});
由于在父路由中调用findAll
获取到数据并已经存储到Store
中,所以可以用peekRecord()
方法获取到数据。 但是在模型简介这篇文章介绍过Store
的特性,当界面获取数据的时候首先会在Store
中查询数据是否存在,如果不存在在再发送请求获取,所以感觉peekRecord()
和findRecord()
方法区别不是很大!
项目中经常会遇到根据某个值查询出一组匹配的数据。此时返回的数据就不是只有一条了,那么Ember有是怎么去实现的呢?
// app/routes/articles.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { // 返回firebase数据库中的所有article // return this.store.findAll('article'); // return this.store.peekAll('article'); // 使用query方法查询category为Java的数据 return this.store.query('article', { filter: { category: 'java' } }).then(function(item) { // 对匹配的数据做处理 return item; }); }});
查询category
为Java
的数据。如果你只想精确查询到某一条数据可以使用queryRecord()
方法。如下:
this.store.queryRecord('article', { filter: { id: ' -JzyT-VLEWdF6zY3CefO' } }).then(function(item) { // 对匹配的数据做处理});
到此,常用的方法介绍完毕,希望通过介绍上述几个方法起到抛砖引玉的效果,有关于DS.Store类的还有很多很多的方法,使用方式都是类似的,更多方法请自己看API文档学习。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
前一篇介绍了查询方法,本篇介绍新建、更新、删除记录的方法。本篇的示例代码创建在上一篇的基础上。对于整合firebase、创建route
和template
请参看上一篇,增加一个controller:ember g controller articles
。
创建新的记录使用createRecord()
方法。比如下面的代码新建了一个aritcle
记录。修改模板,在模板上增加几个input
输入框用于输入article
信息。
<div class="container"> <div class="row"> <div class="col-md-4 col-xs-4"> <ul class="list-group"> {{#each model as |item|}} <li class="list-group-item"> {{#link-to 'articles.article' item.id}} {{item.title}} -- <small>{{item.category}}</small> {{/link-to}} </li> {{/each}} </ul> <div> title:{{input value=title}}<br> body: {{textarea value=body cols="80" rows="3"}}<br> category: {{input value=category}}<br> <button {{ action "saveItem"}}>保存</button> <font color='red'>{{tipInfo}}</font> </div> </div> <div class="col-md-8 col-xs-8"> {{outlet}} </div> </div></div>
页面的字段分别对应这模型article
的属性。点击“保存”后提交到controller
处理。下面是获取数据保存数据的controller
。
// app/controllers/articles.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { // 表单提交,保存数据到Store。Store会自动更新到firebase saveItem: function() { var title = this.get('title'); if ('undefined' === typeof(title) || '' === title.trim()) { this.set('tipInfo', "title不能为空"); return ; } var body = this.get('body'); if ('undefined' === typeof(body) || '' === body.trim()) { this.set('tipInfo', "body不能为空"); return ; } var category = this.get('category'); if ('undefined' === typeof(category) || '' === category.trim()) { this.set('tipInfo', "category不能为空"); return ; } // 创建数据记录 var article = this.store.createRecord('article', { title: title, body: body, category: category, timestamp: new Date().getTime() }); article.save(); //保存数据的到Store // 清空页面的input输入框 this.set('title', ""); this.set('body', ""); this.set('category', ""); } }});
主要看createRecord
方法,第一个参数是模型名称。第二个参数是个哈希,在哈希总设置模型属性值。最后调用article.save()
方法把数据保存到Store
,再由Store
保存到firebase。运行效果如下图:
输入信息,点击“保存”后数据立刻会显示在列表”no form -- java”之后。然后你可以点击标题查询详细信息,body的信息会在页面后侧显示。
通过这里实例我想你应该懂得去使用createRecord()
方法了!但是如果有两个模型是有关联关系保存的方法又是怎么样的呢?下面再新增一个模型。
ember g model users
然后在模型中增加关联。
// app/models/article.jsimport DS from 'ember-data';export default DS.Model.extend({ title: DS.attr('string'), body: DS.attr('string'), timestamp: DS.attr('number'), category: DS.attr('string'), author: DS.belongsTo('user') //关联user});// app/models/user.jsimport DS from 'ember-data';export default DS.Model.extend({ username: DS.attr('string'), timestamp: DS.attr('number'), articles: DS.hasMany('article') //关联article});
修改模板articles.hbs
在界面上增加录入作者信息字段。
……省略其他代码<div> title:{{input value=title}}<br> body: {{textarea value=body cols="80" rows="3"}}<br> category: {{input value=category}}<br> <br> author: {{input value=username}}<br> <button {{ action "saveItem"}}>保存</button> <font color='red'>{{tipInfo}}</font></div>……省略其他代码
下面看看怎么在controller
中设置这两个模型的关联关系。一共有两种方式设置,一种是直接在createRecord()
方法中设置,另一种是在方法外设置。
// app/controllers/articles.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { // 表单提交,保存数据到Store。Store会自动更新到firebase saveItem: function() { // 获取信息和校验代码省略…… // 创建user var user = this.store.createRecord('user', { username: username, timestamp: new Date().getTime() }); // 必须要执行这句代码,否则user数据不能保存到Store, // 否则article通过user的id查找不到user user.save(); // 创建article var article = this.store.createRecord('article', { title: title, body: body, category: category, timestamp: new Date().getTime(), author: user //设置关联 }); article.save(); //保存数据的到Store // 清空页面的input输入框 this.set('title', ""); this.set('body', ""); this.set('category', ""); this.set('username', ""); } }});
输入上如所示信息,点击“保存”可以在firebase的后台看到如下的数据关联关系。
注意点:与这两个数据的关联是通过数据的id
维护的。那么如果我要通过article
获取user
的信息要怎么获取呢?
直接以面向对象的方式获取既可。
{{#each model as |item|}} <li class="list-group-item"> {{#link-to 'articles.article' item.id}} {{item.title}} -- <small>{{item.category}}</small> -- <small>{{item.author.username}}</small> {{/link-to}} </li>{{/each}}
注意看助手{{ item.author.username }}
。很像EL表达式吧!!前面提到过有两个方式设置两个模型的关联关系。下面的代码是第二种方式:
// 其他代码省略……// 创建articlevar article = this.store.createRecord('article', { title: title, body: body, category: category, timestamp: new Date().getTime() // , // author: user //设置关联});// 第二种设置关联关系方法,在外部手动调用set方法设置article.set('author', user);// 其他代码省略……
运行,重新录入信息,得到的结果是一致的。甚至你可以直接在createRecord
方法里调用方法来设置两个模型的关系。比如下面的代码段:
var store = this.store; // 由于作用域问题,在createRecord方法内部不能使用this.storevar article = this.store.createRecord('article', { title: title, // …… // , // author: store.findRecord('user', 1) //设置关联});// 第二种设置关联关系方法,在外部手动调用set方法设置article.set('author', store.findRecord('user', 1));
这种方式可以直接动态根据user的id属性值获取到记录,再设置关联关系。新增介绍完了,接着介绍记录的更新。
更新相对于新增来说非常相似。请看下面的代码段:首先在模板上增加更新的设置代码,修改子模板articles/article.hbs
:
<h1>{{model.title}}</h1><div class = "body">{{model.body}}</div><div><br><hr>更新测试<br>title: {{input value=model.title}}<br>body:<br> {{textarea value=model.body cols="80" rows="3"}}<br><button {{action 'updateArticleById' model.id}}>更新文章信息</button></div>
增加一个controller
,用户处理子模板提交的修改信息。
ember g controller articles/article
// app/controllers/articles/article.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { // 根据文章id更新 updateArticleById: function(params) { var title = this.get('model.title'); var body = this.get('model.body'); this.store.findRecord('article', params).then(function(art) { art.set('title', title); art.set('body', body); // 保存更新的值到Store art.save(); }); } }});
在左侧选择需要更新的数据,然后在右侧输入框中修改需要更新的数据,在修改过程中可以看到被修改的信息会立即反应到界面上,这个是因为Ember自动更新Store中的数据(还记得很久前讲过的观察者(observer)吗?)。
如果你没有点击“更新文章信息”提交,你修改的信息不会更新到firebase。页面刷新后还是原来样子,如果你点击了“更新文章信息”数据将会把更新的信息提交到firebase。
由于save
、findRecord
方法返回值是一个promises
对象,所以你还可以针对出错情况进行处理。比如下面的代码:
var user = this.store.createRecord('user', { // ……});user.save().then(function(fulfill) { // 保存成功}).catch(function(error) { // 保存失败});this.store.findRecord('article', params).then(function(art) { // ……}).catch(function(error) { // 出错处理代码});
具体代码我就不演示了,请读者自己编写测试吧!!
既然有了新增那么通常就会有删除。记录的删除与修改非常类似,也是首先查询出要删除的数据,然后执行删除。
// app/controllers/articles.jsimport Ember from 'ember';export default Ember.Controller.extend({ actions: { // 表单提交,保存数据到Store。Store会自动更新到firebase saveItem: function() { // 省略 }, // 根据id属性值删除数据 delById : function(params) { // 任意获取一个作为判断表单输入值 if (params && confirm("你确定要删除这条数据吗??")) { // 执行删除 this.store.findRecord('article', params).then(function(art) { art.destroyRecord(); alert('删除成功!'); }, function(error) { alert('删除失败!'); }); } else { return; } } }});
修改显示数据的模板,增加删除按钮,并传递数据的id
值到controller
。
<div class="container"> <div class="row"> <div class="col-md-4 col-xs-4"> <ul class="list-group"> {{#each model as |item|}} <li class="list-group-item"> {{#link-to 'articles.article' item.id}} {{item.title}} -- <small>{{item.category}}</small> -- <small>{{item.author.username}}</small> {{/link-to}} <button {{action 'delById' item.id}}>删除</button> </li> {{/each}} </ul> // ……省略其他代码 </div></div>
结果如上图,点击第二条数据删除按钮。弹出提示窗口,点击“确定”之后成功删除数据,并弹出“删除成功!”,到firebase后台查看数据,确实已经删除成功。然而与此关联的user却没有删除,正常情况下也应该是不删除关联的user数据的。最终结果只剩下一条数据:
到此,有关新增、更新、删除的方法介绍完毕。已经给出了详细的演示实例,我相信,如果你也亲自在自己的项目中实践过,那么掌握这几个方法是很容易的!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
Ember的Store就像一个缓存池,用户提交的数据以及从服务器获取的数据会首先保存到Store。如果用户再次请求相同的数据会直接从Store中获取,而不是发送HTTP请求去服务器获取。
当数据发生变化,数据首先更新到Store中,Store会理解更新到其他页面。所以当你改变Store中的数据时,会立即反应出来,比如上一篇更新记录小结,当你修改article
的数据时会立即反应到页面上。
你可以调用push()
方法一次性把多条数据保存到Store中。比如下面的代码:
// app/routes/application.jsimport Ember from 'ember';export default Ember.Route.extend({ model: function() { this.store.push({ data: [ { id: 1, type: 'album', attributes: { // 设置model属性值 title: 'Fewer Moving Parts', artist: 'David Bazan' songCount: 10 }, relationships: {} // 设置两个model的关联关系 }, { id: 2, type: 'album', attributes: { // 设置model属性值 title: 'Calgary b/w I Can't Make You Love Me/Nick Of Time', artist: 'Bon Iver', songCount: 2 }, relationships: {} // 设置两个model的关联关系 } ] }); }});
注意:type
属性值必须是模型的属性名字。attributes
哈希里的属性值与模型里的属性对应。
本篇不是很重要,就简单提一提,如有兴趣请看Pushing Records Into The Store的详细介绍。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
在前面Ember.js 入门指南之三十八定义模型中介绍过模型之前的关系。主要包括一对一、一对多、多对多关系。但是还没介绍两个有关联关系模型的更新、删除等操作。
为了测试新建两个模型类。
ember g model postember g model comment
// app/models/post.jsimport DS from 'ember-data';export default DS.Model.extend({ comments: DS.hasMany('comment')});// app/model/comment.jsimport DS from 'ember-data';export default DS.Model.extend({ post: DS.belongsTo('post')});
设置关联,关系的维护放在多的一方comment
上。
let post = this.store.peekRecord('post', 1);let comment = this.store.createRecord('comment', { post: post});comment.save();
保存之后post
会自动关联到comment
上(保存post
的id
属性值到post
属性上)。
当然啦,你可以在从post
上设置关联关系。比如下面的代码:
let post = this.store.peekRecord('post', 1);let comment = this.store.createRecord('comment', { // 设置属性值});// 手动吧对象设置到post数组中。(post是多的一方,comments属性应该是保存关系的数组)post.get('comments').pushObject(comment);comment.save();
如果你学过Java里的hibernate框架我相信你很容易就能理解这段代码。你可以想象,post
是一的一方,如果它要维护关系是不是要把与其关联的comment
的id
保存到comments
属性(数组)上,因为一个post
可以关联多个comment
,所以comments
属性应该是一个数组。
更新关联关系与创建关联关系几乎是一样的。也是首先获取需要关联的模型在设置它们的关联关系。
let post = this.store.peekRecord('post', 100);let comment = this.store.peekRecord('comment', 1);comment.set('psot', post); // 重新设置comment与post的关系comment.save(); // 保存关联的关系
假设原来comment
关联的post
是id
为1
的数据,现在重新更新为comment
关联id
为100
的post
数据。
如果是从post
方更新,那么你可以像下面的代码这样:
let post = this.store.peekRecord('post', 100);let comment this.store.peekRecord('comment', 1);post.get('comments').pushObject(comment); // 设置关联post.save(); // 保存关联
既然有新增关系自然也会有删除关联关系。如果要移除两个模型的关联关系,只需要把关联的属性值设置为null
就可以了。
let comment = this.store.peekRecord('comment', 1);comment.set('post', null); //解除关联关系comment.save();
当然你也可以从一的一方移除关联关系。
let post = this.store.peekRecord('post', 1);let comment = this.store.peekRecord('comment', 1);post.get('comments').removeObject(comment); // 从关联数组中移除commentpost.save();
从一的一方维护关系其实就是在维护关联的数组元素。
只要Store改变了Handlebars模板就会自动更新页面显示的数据,并且在适当的时期Ember Data会自动更新到服务器上。
有关于模型之间关系的维护就介绍到这里,它们之间关系的维护只有两种方式,一种是用一的一方维护,另一种是用多的一方维护,相比来说,从一的一方维护更简单。但是如果你需要一次性更新多个纪录的关联时使用第二种方式更加合适(都是针对数组操作)。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
元数据是数据与一个特定的模式或类型,而不是一个纪录。
一个很常见的例子是分页。通常会像下面的代码设置分页:
let result = this.store.query(‘post’, { limit: 10, offset: 0});
设置了每页显示数据为10条,但是你不知道总条数,又怎么知道一共有多少页呢?这时候元数据就派上用场了。
{ "post": { "id": 1, "title": "Progressive Enhancement is Dead", "comments": ["1", "2"], "links": { "user": "/people/tomdale" }, // ... }, "meta": { "total": 100 }}
这些数据是从后台返回的JSON格式数据,如果你想获取元数据可以使用this.get('meta')
获取。甚至还可以从query()
方法中获取。
let
和 =>
都是javascript ES6的语法,如果你想了解有关javascript ES6请Google。
对于元数据在项目中的使用会在后面的例子中展现。在介绍完Ember基础知识后我回做一个比较完整的小项目,我会在项目中尽可能的使用所讲过的知识点,敬请期待……_小项目代码:todos_
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
在Ember应用中适配器决定了数据保存到后台的方式,比如URL格式和请求头部。Ember Data默认的适配器是内置的REST API回调。
实际使用中经常会扩展默认的适配器。Ember的立场是应该通过扩展适配器来添加不同的功能,而非添加标识。这样可以使得代码更加容易测试、更加容易理解,同时也降低了可能需要扩展的适配器的代码。
如果你的后端使用的是Ember约定的规则那么可用使用适配器adapters/application.js
。适配器application
优先级比默认的适配器高,但是比指定的模型适配器优先级低。模型适配器定义规则是:adapter-modelName.js
。比如下面的代码定义了一个模型适配器adapter-post
。
// app/adapters/adapter-post.jsimport Ember from 'ember';export default DS.JSONAPIAdapter.extend({ namespace: 'api/v1'});
此时适配器的优先级次序为:JSONAPIAdapter
> application
> 默认内置适配器;
Ember内置的是配有如下几种:
JSONAPIAdapter
适配器通常用于扩展非标准的后台接口。
JSONAPIAdapter
适配器非常智能,它能够自动确定URL链接是那个模型。比如,如果你需要通过id
获取post
:
this.store.find('post', 1).then(function(post) { // 处理post});
JSONAPIAdapter
会自动发送get
请求到/post/1
。
下表是Action、请求、URL三者的映射关系(由于本站markdown解析器不支持表格所以直接使用截图替代了)。
比如在action
中执行find()
方法,会发送get
请求,JSONAPIAdapter
会自动解析成形如/posts/1
的URL。
为了适配复数名字的模型属性名称,可以是使用Ember Inflector绑定别名。
let inflector = Ember.Inflector.inflector;inflector.irregular('formula', 'formulae');inflector.uncountable('advice');
这样绑定之后目的是告诉JSONAPIAdapter
。你可以使用/formulae/1
代替/formulas/1
。但是目前我还没搞清楚这个设置是什么意思?又有什么用?如果读者知道请指教。
使用属性namespace
可以设置URL的前缀。
app/adapters/application.jsimport Ember from 'ember';export default DS.JSONAPIAdapter.extend({ namespace: 'api/1'});
请求person
会自动转发到/api/1/people/1
。
默认情况下适配器会转到当前域名下。如果你想让URL转到新的域名可以使用属性host设置。
app/adapters/application.jsimport Ember from 'ember';export default DS.JSONAPIAdapter.extend({ host: 'http://api.example.com'});
注意:如果你的项目是使用自己的后台数据库这个设置特别重要!!!属性host
所指的就是你服务器接口的地址。
请求person
会自动转发到http://api.example.com/people/1
。
默认情况下Ember会尝试去根据复数的模型类名、中划线分隔的模型类名生成URL。如果需要改变这个默认的规则可以使用属性pathForType
设置。
// app/adapters/application.jsimport Ember from ‘ember’;export default DS.JSONAPIAdapter.extend({ pathForType: function(type) { return Ember.String.underscore(type); }});
修改默认生成路径规则为下滑线分隔。比如请求person
会转向/person/1
。请求user-profile
会转向/user_profile/1
(默认情况转向user-profile/1
)。
有些请求需要设置请求头信息,比如提供API
的key
。可以以键值对的方式设置头部信息,Ember Data会为每个请求都加上这个头信息设置。
app/adapters/application.jsimport Ember from 'ember';export default DS.JSONAPIAdapter.extend({ headers: {'API_KEY': 'secret key','ANOTHER_HEADER': 'some header value' }});
更强大地方是你可以在header
中使用计算属性,动态设置请求头信息。下面的代码是将一个session
对象设置到适配器中。
app/adapters/application.jsimport Ember from ‘ember’;export default DS.JSONAPIAdapter.extend({ session: Ember.inject.service(‘session’); headers: Ember.computed(‘session.authToken’, function() {‘API_KEY’: this.get(‘session.authToken’),‘ANOTHER_HEADER’: ‘some header value’ });});
对于session
应该非常不陌生,特别是在控制用户登录方面使用非常广泛,另外又一个非常好用的插件,专门用于控制用户登录的,这个插件是Ember Simple Auth,另外有一篇博文是介绍如何使用这个插件实现用户登录的,请看使用ember-simple-auth实现Ember.js应用的权限控制的介绍。
你还可以使用volatile()
方法设置计算属性为非缓存属性,这样每次发送请求都会重新计算header
里的值。
// app/adapters/application.jsimport Ember from ‘ember’;export default DS.JSONAPIAdapter.extend({ // …… }).volatile();});
更多有关于适配器的信息浏览下面的网址:
对于适配器主要掌握JSONAPIAdapter
足矣,如果你需要个性化定制URL或者请求的域名可以在适配中配置。不过大部分情况下都是使用默认设置。
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
在Ember应用中,序列化器会格式化与后台交互的数据,包括发送和接收的数据。默认情况下会使用JSON API序列化数据。如果你的后端使用不同的格式,Ember Data允许你自定义序列化器或者定义一个完全不同的序列化器。
Ember Data内置了三个序列化器。JSONAPISerializer是默认的序列化器,用与处理后端的JSON API。JSONSerializer是一个简单的序列化器,用与处理单个JSON对象或者是处理记录数组。RESTSerializer是一个复杂的序列化器,支持侧面加载,在Ember Data2.0之前是默认的序列化器。
当你向服务器请求数据时,JSONSerializer会把服务器返回的数据当做是符合下列规范的JSON数据。
注意:特别是项目使用的是自定义适配器的时候,后台返回的数据格式必须符合JSOP API规范,否则无法实现数据的CRUD操作,Ember就无法解析数据,关于自定义适配器这点的知识请看上一篇Ember.js 入门指南之四十四自定义适配器,在文章中有详细的介绍自定义适配器和自定义序列化器是息息相关的。
JSONSerializer期待后台返回的是一个符合JSON API规范和约定的JSON文档。比如下面的JSON数据,这些数据的格式是这样的:
比如请求/people/123
,响应的数据如下:
{ "data": { "type": "people", "id": "123", "attributes": { "first-name": "Jeff", "last-name": "Atwood" } }}
如果响应的数据有多条,那么data
将是以数组形式返回。
{ "data": [ { "type": "people", "id": "123", "attributes": { "first-name": "Jeff", "last-name": "Atwood" } },{ "type": "people", "id": "124", "attributes": { "first-name": "chen", "last-name": "ubuntuvim" } } ]}
数据有时候并不是请求的主体,如果数据有链接。链接的关系会放在included
下面。
{ "data": { "type": "articles", "id": "1", "attributes": { "title": "JSON API paints my bikeshed!" }, "links": { "self": "http://example.com/articles/1" }, "relationships": { "comments": { "data": [ { "type": "comments", "id": "5" }, { "type": "comments", "id": "12" } ] } } }], "included": [{ "type": "comments", "id": "5", "attributes": { "body": "First!" }, "links": { "self": "http://example.com/comments/5" } }, { "type": "comments", "id": "12", "attributes": { "body": "I like XML better" }, "links": { "self": "http://example.com/comments/12" } }]}
从JSON数据看出,id
为5
的comment
链接是"self": http://example.com/comments/5
。id
为12
的comment
链接是"self": http://example.com/comments/12
。并且这些链接是单独放置included
内。
Ember Data默认的序列化器是JSONAPISerializer,但是你也可以自定义序列化器覆盖默认的序列化器。
要自定义序列化器首先要定义一个名为application
序列化器作为入口。
直接使用命令生成:ember g serializer application
// app/serializers/application.jsimport DS from 'ember-data';export default DS.JSONSerializer.extend({});
甚至你还可以针对某个模型定义序列化器。比如下面的代码为post
定义了一个专门的序列化器,在前一篇自定义适配器中介绍过如何为一个模型自定义适配器,这个两个是相关的。
// app/serializers/post.jsimport DS from ‘ember-data’;export default DS.JSONSerializer.extend({});
如果你想改变发送到后端的JSON数据格式,你只需重写serialize
回调,在回调中设置数据格式。
比如前端发送的数据格式是如下结构,
{ "data": { "attributes": { "id": "1", "name": "My Product", "amount": 100, "currency": "SEK" }, "type": "product" }}
但是服务器接受的数据结构是下面这种结构:
{ "data": { "attributes": { "id": "1", "name": "My Product", "cost": { "amount": 100, "currency": "SEK" } }, "type": "product" }}
此时你可以重写serialize
回调。
// app/serializers/application.jsimport DS from 'ember-data';export default DS.JSONSerializer.extend({ serialize: function(snapshot, options) { var json = this._super(...arguments); // ?? json.data.attributes.cost = { amount: json.data.attributes.amount, currency: json.data.attributes.currency }; delete json.data.attributes.amount; delete json.data.attributes.currency; return json; }});
那么如果是反过来呢。如果后端返回的数据格式为:
{ "data": { "attributes": { "id": "1", "name": "My Product", "cost": { "amount": 100, "currency": "SEK" } }, "type": "product" }}
但是前端需要的格式是:
{ "data": { "attributes": { "id": "1", "name": "My Product", "amount": 100, "currency": "SEK" }, "type": "product" }}
此时你可以重写回调方法normalizeResponse
或normalize
,在方法里设置数据格式:
// app/serializers/application.jsimport DS from 'ember-data';export default DS.JSONSerializer.extend({ normalizeResponse: function(store, primaryModelClass, payload, id, requestType) { payload.data.attributes.amount = payload.data.attributes.cost.amount; payload.data.attributes.currency = payload.data.attributes.cost.currency; delete payload.data.attributes.cost; return this._super(...arguments); }});
每一条数据都有一个唯一值作为ID
,默认情况下Ember会为每个模型加上一个名为id
的属性。如果你想改为其他名称,你可以在序列化器中指定。
// app/serializers/application.jsimport DS from 'ember-data';export default DS.JSONSerializer.extend({ primatyKey: '__id'});
把数据主键名修改为__id
。
Ember Data约定的属性名是驼峰式的命名方式,但是序列化器却期望的是中划线分隔的命名方式,不过Ember会自动转换,不需要开发者手动指定。然而,如果你想修改这种默认的方式也是可以的,只需在序列化器中使用属性keyForAttributes
指定你喜欢的分隔方式即可。比如下面的代码把序列号的属性名称改为以下划线分隔:
// app/serializers/application.jsimport DS from 'ember-data';export default DS.JSONSerializer.extend({ keyForAttributes: function(attr) { return Ember.String.underscore(attr); }});
如果你想模型数据被序列化、反序列化时指定模型属性的别名,直接在序列化器中使用attrs
属性指定即可。
// app/models/person.jsexport default DS.Model.extend({ lastName: DS.attr(‘string’)});
指定序列化、反序列化属性别名:
// app/serializers/application.jsimport DS from 'ember-data';export default DS.JSONSerializer.extend({ attrs: { lastName: ‘lastNameOfPerson’ }});
指定模型属性别名为lastNameOfPerson
。
一个模型通过ID
引用另一个模型。比如有两个模型存在一对多关系:
// app/models/post.jsexport default DS.Model.extend({ comments: DS.hasMany(‘comment’, { async: true });});
序列化后JSON数据格式如下,其中关联关系通过一个存放ID
属性值的数组实现。
{ "data": { "type": "posts", "id": "1", "relationships": { "comments": { "data": [ { "type": "comments", "id": "5" }, { "type": "comments", "id": "12" } ] } } }}
可见,有两个comment
关联到一个post
上。如果是belongsTo
关系的,JSON结构与hadMany
关系相差不大。
{ "data": { "type": "comment", "id": "1", "relationships": { "original-post": { "data": { "type": "post", "id": "5" }, } } }}
id
为1
的comment
关联了ID
为5
的post
。
在某些情况下,Ember内置的属性类型(string
、number
、boolean
、date
)还是不够用的。比如,服务器返回的是非标准的数据格式时。
Ember Data可以注册新的JSON转换器去格式化数据,可用直接使用命令创建:ember g transform coordinate-point
// app/transforms/coordinate-point.jsimport DS from 'ember-data';export default DS.Transform.extend({ deserialize: function(v) { return [v.get('x'), v.get('y')]; }, serialize: function(v) { return Ember.create({ x: v[0], y: v[1]}); }});
定义一个复合属性类型,这个类型由两个属性构成,形成一个坐标。
// app/models/curor.jsimport DS from 'ember-data';export default DS.Model.extend({ position: DS.attr(‘coordinate-point’)});
自定义的属性类型使用方式与普通类型一致,直接作为attr
方法的参数。最后当我们接受到服务返回的数据形如下面的代码所示:
{ cursor: { position: [4, 9] }}
加载模型实例时仍然作为一个普通对象加载。仍然可以使用.
操作获取属性值。
var cursor = this.store.findRecord(‘cursor’, 1);cursor.get(‘position.x’); // => 4cursor.get(‘position.y’); // => 9
并不是所有的API都遵循JSONAPISerializer约定通过数据命名空间和拷贝关系记录。比如系统遗留问题,原先的API返回的只是简单的JSON格式并不是JSONAPISerializer约定的格式,此时你可以自定义序列化器去适配旧接口。并且可以同时兼容使用RESTAdapter去序列号这些简单的JSON数据。
// app/serializer/application.jsexport default DS.JSONSerializer.extend({ // ...});
尽管Ember Data鼓励你拷贝模型关联关系,但有时候在处理遗留API时,你会发现你需要处理的JSON中嵌入了其他模型的关联关系。不过EmbeddedRecordsMixin
可以帮你解决这个问题。
比如post
中包含了一个author
记录。
{ "id": "1", "title": "Rails is omakase", "tag": "rails", "authors": [ { "id": "2", "name": "Steve" } ]}
你可以定义里的模型关联关系如下:
// app/serializers/post.jsexport default DS.JSONSerialier.extend(DS.EmbeddedRecordsMixin, { attrs: {author: { serialize: ‘records’, deserialize: ‘records’} }});
如果你发生对象本身需要序列化与反序列化嵌入的关系,你可以使用属性embedded
设置。
// app/serializers/post.jsexport default DS.JSONSerialier.extend(DS.EmbeddedRecordsMixin, { attrs: {author: { embedded: ‘always’ } }});
序列化与反序列化设置有3个关键字:
records
用于标记全部的记录都是序列化与反序列化的ids
用于标记仅仅序列化与反序列化记录的idfalse
用于标记记录不需要序列化与反序列化例如,你可能会发现你想读一个嵌入式记录提取时一个JSON有效载荷只包括关系的身份在序列化记录。这可能是使用serialize: ids
。你也可以选择通过设置序列化的关系 serialize: false
。
export default DS.JSONSerializer.extend(DS.EmbeddedRecordsMixin, { attrs: { author: { serialize: false, deserialize: 'records' }, comments: { deserialize: 'records', serialize: 'ids' } }});
如果你没有重写attrs
去指定模型的关联关系,那么EmbeddedRecordsMixin
会有如下的默认行为:
belongsTo:{serialize: ‘id’, deserialize: ‘id’ }hasMany: { serialize: false, deserialize: ‘ids’ }
如果项目需要自定义序列化器,Ember推荐扩展JSONAIPSerializer或者JSONSerializer来实现你的需求。但是,如果你想完全创建一个全新的与JSONAIPSerializer、JSONSerializer都不一样的序列化器你可以扩展DS.Serializer
类,但是你必须要实现下面三个方法:
知道规范化JSON数据对Ember Data来说是非常重要的,如果模型属性名不符合Ember Data规范这些属性值将不会自动更新。如果返回的数据没有在模型中指定那么这些数据将会被忽略。比如下面的模型定义,this.store.push()
方法接受的格式为第二段代码所示。
// app/models/post.jsimport DS from 'ember-data';export default DS.Model.extend({ title: DS.attr(‘string’), tag: DS.attr(‘string’), comments: hasMany(‘comment’, { async: true }), relatedPosts: hasMany(‘post’)});
{ data: { id: "1", type: 'post', attributes: { title: "Rails is omakase", tag: "rails", }, relationships: { comments: { data: [{ id: "1", type: 'comment' }, { id: "2", type: 'comment' }], }, relatedPosts: { data: { related: "/api/v1/posts/1/related-posts/" } } }}
每个序列化记录必须按照这个格式要正确地转换成Ember Data记录。
本篇的内容难度很大,属于高级主题的内容!如果暂时理解不来不要紧,你可以先使用firebase构建项目,等你熟悉了整个Ember流程以及数据是如何交互之后再回过头看这篇和上一篇Ember.js 入门指南之四十四自定义适配器,这样就不至于难以理解了!!
到本篇为止,有关Ember的基础知识全部介绍完毕!!!从2015-08-26开始到现在刚好2个月,原计划是用3个月时间完成的,提前了一个月,归其原因是后面的内容难度大,理解偏差大!文章质量也不好,感觉时间比较仓促,说以节省了很多时间!(本篇是重新整理发表的,原始版博文发布的时候Ember还是2.0版本,现在已经是2.5了!!)
介绍来打算介绍APPLICATION CONCERNS和TESTING这两章!也有可能把旧版的Ember todomvc案例改成Ember2.0版本的,正好可以拿来练练手速!!!
很庆幸的是目标:把旧版的Ember todomvc案例改成Ember2.0版本的,也完成了!!!并且扩展了很多功能,有关代码情况todos v2,欢迎读者fork学习!如果觉得有用就给我一个star
吧!!谢谢!!!
博文完整代码放在Github(博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!),如果你觉得博文对你有点用,请在github项目上给我点个star
吧。您的肯定对我来说是最大的动力!!
英文原文:https://guides.emberjs.com/v2.7.0/testing/
测试是
Ember。js
框架开发环节中很重要的一环。
现在假设你正在利用Ember框架开发一个博客系统,这个系统包含user
和post
模型,有登录及创建博客的操作。最后假设你希望在你的程序里实现自动化测试。
你一共需要下面这3种类型的测试:
验收测试是用来确保程序流程正确,且各类交互特性符合用户预期的测试。
验收测试用于确认项目基本功能,保证项目核心功能没有退化,确保该项目的目标得以实现。测试应用的方式和用户与应用程序的交互方式是一致的(比如填写表单,点击按钮)。
在上述的场景中,可能会做如下的验收测试:
单元测试是针对程序中的最小可测试单元进行的测试,比如一个类或者一个方法。该测试可以编写与程序逻辑相对的语句来测试相关单元
下面是一些单元测试的具体例子:
集成测试是处于单元测试和验收测试之间的测试。集成测试目的是验证客户端与全系统交互,所有单元测试,以及微观层面具体代码的算法逻辑是否都能通过。
集成测试用来验证应用程序各个模块相互关系,比如若干个UI控件之间的行为。也可以用于确认数据和动作在系统不同的部件中被正确的传递和执行,同时在给定假设条件下,可以提供系统各部件配合运行的情况。
我们建议对每个组件都进行集成测试,因为组件各个组件以相同的方式运行在系统的上下文中,并且组件之间也有相互影响,包括从模板中渲染组件、接收组件生命周期回调函数。
集成测试示例如下:
QUnit是本手册的默认测试框架,但是Ember.js也支持其他第三方的测试框架。
在命令行输入ember test
来运行测试。也可以通过ember test -server
命令,在每次文件改动后,重新运行测试。
在本地开发项目的时候可以通过访问/tests/index.html
来运行你的测试,前提是你需要使用命令ember server
运行了你的项目。如果你是使用这种方式有一点需要注意:
ember server
运行的测试,是在开发环境下的测试,调用的是开发环境下的参数ember test --server
运行的测试,是在测试环境下的测试,调用的是测试环境下的参数,比如加载的依赖也是不同的。因此我们推荐你使用ebmer test --server
来运行测试。使用--filter
选项来指定运行部分测试。比如:快速运行当前工作的测试使用命令ember test --filter="dashboard"
、运行指定类型的测试使用命令ember test --filter="integration"
、可以使用!
来排除验收测试ember test --filter="!acceptance"
。
使用ember generate acceptance-test
创建一个验收测试,比如:
ember g acceptance-test login
执行完毕命令之后得到如下文件内容:
//tests/acceptance/login-test.jsimport { test } from 'qunit';import moduleForAcceptance from 'people/tests/helpers/module-for-acceptance';moduleForAcceptance('Acceptance | login');test('visting /login', function(assert) { visit('/login'); andThen(function() { assert.equal(currentURL(), '/login'); });});
moduleForAcceptance
用来启动、终止程序。最后几行test
中包含了一个示例。
几乎所有的测试都有一个路由请求,用于和页面交互(通过helper)并检查DOM是否按照期望值进行改变。
举个例子:
test('should add new post', function(assert) { visit('/posts/new'); fillIn('input.title', 'My new post'); click('button.submit'); andThen(() => assert.equal(find('ul.posts li:first').text(), 'My new post'));});
大体意思为:进入路由/posts/new
,在输入框input.title
填入My new post
,点击button.submit
,期望的结果是: 在对应列表下ul.posts li.first
的文本为My new post
.
在测试web应用中的一个主要的问题是,由于代码都是基于事件驱动的,因此他们有可能是异步
的,会使代码无序
运行。
比如有两个按钮,从不同的服务器载入数据,我们先后点击他们,但可能结果返回的顺序并不是我们点击的顺序。
当你在编写测试的时候,你需要特别注意一个问题,就是你无法确定在发出一个请求后,是否会立刻得到返回的响应。因此,你的断言需要以同步的状态来等待被测试体。例如上面所举的例子,应该等待两个服务器均返回数据后,这时测试代码才会执行其逻辑来检测数据的正确性。
这就是为什么在做断言的时候,Ember测试助手都是被包裹在一个确保同步状态的代码中。这样做避免了对所有这样的代码都去做这样的包裹,并且因为减少了模板代码,从而提高了代码的可读性.
Ember包含多个测试助来辅助进行验收测试。一共有2种类型:异步助手asynchronous
和同步助手synchronous
异步测试助手可以意识到程序中的异步行为,使你可以更方便的编写确切的测试。
同时,这些测试助手会按注册的顺序执行,并且是链式运行。每个测试助手的调用都是在前一个调用结束之后,才会执行下一个。因此,你可以不用当心测试助手的执行顺序。
click(selector)
click
事件,返回一个异步执行成功的promise
。fillIn(selector, value)
作为
时,记得
元素的value
是标签所指定的值,并不是
标签显示的内容。keyEvent(selector, type, keyCode)
keypress
,按键按下keydown
,按键弹起keyup
事件的keyCode
。triggerEvent(selector,type,keyCode)
blur
、ddlclick
等事件...visit(url)
同步测试助手,当触发时会立即执行并返回结果.
currentPath()
currentRouteName()
currentURL()
find(selector, context)
andThen
测试助手将会等待所有异步测试助手完成之后再执行.举个例子:
// tests/acceptance/new-post-appears-first-test.jstese('should add new post', function(assert) { visit('/posts/new'); fillIn('input.title', 'My new post'); click('button.submit'); andThen(() => assert.equal(find('ul.posts li:first').text(), 'My new post')); });
首先,我们访问/posts/new
地址,在有title
css类的input
输入框内内填入内容“My new post”,然后点击有CSS类submit
的按钮。
等待签名的异步测试助手执行完(具体说,andThen
会在路由/posts/new
访问完毕,input
表单填充完毕,按钮被点击之后)毕后会执行andThen
助手。注意andThen
助手只有一个参数,这个参数是一个函数,函数的代码是其实测试助手执行完毕之后才执行的代码。
在andThen
助手中,我们最后会调用assert.equal
断言来判定对应位置的值是否为My new post
。
使用命令ember generate test-helper
来创建自定义测试助手。
下面的代码是执行命令ember g test-helper shouldHaveElementWithCount
得到的测试例子:
//tests/helpers/should-have-element-with-count.jsexport default Ember.Test.registerAsyncHelper( 'shouldHaveElementWithCount', function(app){});
Ember.Test.registerAsyncHelper
和Ember.Test.registerHelper'是当
startApp被调用时,会将自定义测试助手注册。两者的区别在于,前者
Ember.Test.registerHelper`会在之前任何的异步测试助手运行完成之后运行,并且后续的异步测试助手在运行前都会等待他完成.
测试助手方法一般都会以当前程序的第一个参数被调用,其他参数,比如assert
,在测试助手被调用的时候提供。测试助手需要在调用startApp
前进行注册,但ember-cli
会帮你处理,你不需要担心这个问题。
下面是一个非异步的测试助手:
//tests/helpers/should-have-element-with-count.jsexport default Ember.Test.registerHelper('shouldHaveElementWithCount', function(app, assert, selector, n, context){ const el = findWithAssert(selector, context); const count = el.length; assert.equal(n, count, 'found ${count} times'); });//shouldHaveElementWithCount(assert, 'ul li', 3);
下面是一个异步的测试助手:
export default Ember.Test.registerAsynHelper('dblclick', function(app, assert, selector, context){ let $el = findWithAssert(selector, context); Ember.run(() => $el.dblclick()); });//dblclick(assert, '#persion-1')
异步测试助手也可以让你将多个测试助手合并为一个.举个例子:
//tests/helpers/add-contact.jsexport default Ember.Test.registerAsyncHelper('addContact', function(app,name) { fillIn('#name', name); click('button.create'); });//addContact('Bob');//addContact('Dan');
最后, 别忘了将你的测试助手添加进tests/.jshintrc
和tests/helpers/start-app.js
中、在tests/.jshintrc
中,你需要将其添加进predef
块中,不然就会得到jshint测试失败的消息.
{ "predef": [ "document", "window", "locaiton", ... "shouldHaveElementWithCount", "dblclick", "addContact" ], ...}*
你需要在tests/helpers/start-app.js
引入测试助手,这些助手将会被注册到应用中。
import Ember from 'ember';import Application from '../../app';import Router from '../../router';import config from '../../config/environmnet';import './should-have-element-with-count';import './dblclick';import './add-contact';
单元测试一般被用来测试一些小的代码块,并确保它正在做的是什么。与验收测试不同的是,单元测试被限定在小范围内并且不需要Emeber程序运行。
与Ember基本对象一样的,创建单元测试也只需要继承Ember.Object
即可。然后在代码块内编写具体的测试内容,比如控制器、组件。每个测试就是一个Ember.Object
实例对象,你可以设置对象的状态,运行断言。通过下面的例子,让我们一起看看测试如何使用。
创建一个简单的实例,实例内包含一个计算属性computedFoo
,此计算属性依赖普通属性foot
。
//app/models/somt-thing.jsexport default Ember.Object.extend({ foo: 'bar', computedFoo: Ember.compuuted('foo',function() { const foo = this.get('foo'); return `computed ${foo}`; })});
在测试中,我们建立一个实例,然后更新属性foo
的值(这个操作会触发计算属性computedFoo
,使其自动更新),然后给出一个符合预期的断言
:
//tests/unit/models/some-thing-test.jsimport {moduleFor, test} from 'ember-qunit';moduleFor('model:some-thing', 'Unit | some thing', { unit: true});test('should correctly concat foo', function(assert) { const someThing = this.subject(); somtThing.set('foo', 'baz'); //设置属性foo的值 assert.equal(someThing.get('computedFoo'), 'computed baz'); //断言,判断计算属性值是否相等于computed baz});
例子中使用了moduleFor
,它是由Ember-Qunit
提供的单元测试助手。这些测试助手为我们提供了很多便利,比如subject
功能,它可以寻找并实例化测试所用的对象。同时你还可以在subject
方法中自定义初始化的内容,这些初始化的内容可以是传递给主体功能的实例变量。比如在单元测试内初始化属性“foo”你可以这样做:this.subject({foo: 'bar'});
,那么单元测试在运行时属性foo
的值就是bar
。
下面让我们来看一下如何测试对象方法的逻辑。在本例中对象内部有一个设置属性(更新属性foo
值)值的方法testMethod
。
//app/models/some-thing.jsexport default Ember.Object.extend({ foo: 'bar', testMethod() { this.set('foo', 'baz'); }});
要对其进行测试,我们先创建如下实例,然后调用testMethod
方法,然后用断言判断方法的调用结果是否是正确的。
//tests/unit/models/some-thing-test.jstest('should update foo on testMethod', function(assert) { const someThing = this.subject(); someThing.testMethod(); assert.equal(someThing.get('foo'), 'baz');});
如果一个对象方法返回的是一个值,你可以很容易的给予断言进行判定是否正确。假设我们的对象拥有一个calc
方法,方法的返回值是基于对象内部的状态值。代码如下:
//app/models/some-thing.jsexport default Ember.Object.extend({ count: 0, calc() { this.incrementProperty('count'); let count = this.get('count'); return `count: ${count}`; }});
在测试中需要调用calc
方法,并且断言其返回值是否正确。
//tests/unit/models/some-thing-test.jstest('should return incremented count on calc', function(assert) { const someThing = this.subject(); assert.equal(someThing.calc(), 'count: 1'); assert.equal(someThing.calc(), 'count: 2');});
假设我们有一个对象,这个对象拥有一些属性,并且有一个方法在监测着这些属性。
//app/models/some-thing.jsexport default Ember.Object.extend({ foo: 'bar' other: 'no',, doSomething: Ember.observer('foo', function() { this.set('other', 'yes'); })});
为了测试doSomething
方法,我们创建一个SomeThing
对象,更新foo
属性值,然后进行断言是否达到预期结果。
//tests/unit/models/some-thing-test.jstest('should set other prop to yes when foo changes', function(assert) { const someThing = this.subject(); someThing.set('foo', 'baz'); assert.equal(someThing.get('other'), 'yes'); });
更多建议: