来源:yoember.com Zoltan 
声明 :本文的转载与翻译是经过作者认可的,再次感谢原作,如有侵权请给我留言,我会删除博文!!  希望本系列教程能帮助更多学习Ember.js的初学者。
接着前面五篇:
环境搭建以及使用Ember.js创建第一个静态页面 引入计算属性、action、动态内容 模型,保存数据到数据库 发布项目,加入CRUD功能 从服务器获取数据,引入组件  
前言 本篇主要是介绍模型直接的关联关系,比如:一对一、一对多关系。会创建两个模型author和book,设置它们的关系,并增加测试数据。
创建模型并设置关联 关联关系设置API:
belongsTo hasMany  
模型关系 :一个library对应多个book,一个author对应多个book。关系图如下:
使用Ember CLI 命令创建模型。
1 2 ember g model book title:string releaseYear:date library:belongsTo author:belongsTo ember g model author name:string books:hasMany 
手动在library中增加hasMany关联关系。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import  Model from  'ember-data/model' ;import  attr from  'ember-data/attr' ;import  { hasMany } from  'ember-data/relationships' ;import  Ember from  'ember' ;export  default  Model.extend({  name: attr('string' ),   address: attr('string' ),   phone: attr('string' ),   books: hasMany('books' ),   isValid: Ember.computed.notEmpty('name' ), }); 
创建一个后台管理页面“Seeder” 1 ember g route admin/seeder 
检查router.js看看路由是否成功创建。相关代码如下:
1 2 3 4 5 6 7 this .route('admin' , function (    this .route('invitations' );     this .route('contacts' );     this .route('seeder' ); }); 
修改导航模板navbar.hbs增加新建路由的入口链接。
1 2 3 4 5 <ul  class ="dropdown-menu" >   {{#nav-link-to 'admin.invitations'}}Invitations{{/nav-link-to}}   {{#nav-link-to 'admin.contacts'}}Contacts{{/nav-link-to}}   {{#nav-link-to 'admin.seeder'}}Seeder{{/nav-link-to}} </ul > 
使用Ember.RSVP.hash()在一个路由中返回多个模型的数据 Ember 支持在一个路由的model回调中返回多个模型的数据。有关方法发API请看Ember.RSVP.hash() 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import  Ember from  'ember' ;export  default  Ember.Route.extend({  model() {     return  Ember.RSVP.hash({       libraries: this .store.findAll('library' ),       books: this .store.findAll('book' ),       authors: this .store.findAll('author' )     })   },   setupController(controller, model) {     controller.set('libraries' , model.libraries);     controller.set('books' , model.books);     controller.set('authors' , model.authors);   } }); 
上述model()回调中返回了三个模型的数据:library、book和author。需要注意的是:上述代码中方法Ember.RSVP.hash()会发送3个请求,并且只有三个请求都成功才会执行成功。setupController()回调中,把三个模型分别设置到controller中。
路由内置方法调用次序 每个路由内都内置了很多方法,比如前面介绍的model、setupController、renderTemplate,这些都是内置在路由类中的方法,那么这些方法调用次序又是如何的呢?请看下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import  Ember from  'ember' ;export  default  Ember.Route.extend({  init() {     debugger ;   },   beforeModel(transition) {     debugger ;   },   model(params, transition) {     debugger ;   },   afterModel(model, transition) {     debugger ;   },   activate() {     debugger ;   },   setupController(controller, model) {     debugger ;   },   renderTemplate(controller, model) {     debugger ;   } }); 
打开浏览器的debug模式并在执行到这个路由中http://localhost:4200/test 。可以看到方法的执行次序与上述代码方法的次序是一致的。有关API请看下面网址的介绍:
init() beforeModel(transition) model(params, transition) activate() setupController(controller, model) renderTemplate(controller, model)  
数量显示功能 创建一个组件用于显示各个模型数据的总数。
1 ember g component number-box 
组件创建完毕之后在组件类中增加css类,使用属性classNames设置。
1 2 3 4 5 6 7 8 import  Ember from  'ember' ;export  default  Ember.Component.extend({  classNames: ['panel' , 'panel-warning' ] }); 
然后在组件模板中增加代码:
1 2 3 4 5 <div  class ="panel-heading" >   <h3  class ="text-center" > {{title}}</h3 >    <h1  class ="text-center" > {{if number number '...'}}</h1 >  </div > 
在修改app/templates/admin/seeder.hbs
1 2 3 4 5 6 7 8 <h1 > Seeder, our Data Center</h1 > <div  class ="row" >   <div  class ="col-md-4" > {{number-box title="Libraries" number=libraries.length}}</div >    <div  class ="col-md-4" > {{number-box title="Authors" number=authors.length}}</div >    <div  class ="col-md-4" > {{number-box title="Books" number=books.length}}</div >  </div > 
等待项目重启完成,进入到后台的seeder下可以看到三个小圆点,请记得,一定要在setupController中设置数据,model回调会自动从服务器获取数据,obj.length意思是调用length()方法获取数据长度,然后直接显示到模板上,效果如下截图,由于后面两个模型还没有数据所以显示省略号。
构建表单生成测试数据 前面已经介绍过属性的传递,下面的代码将为读者介绍一些更加高级的东西!!一大波代码即将来临!!!
1 2 ember g component seeder-block ember g component fader-label 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // app/components/seeder-block.js import Ember from 'ember'; export default Ember.Component.extend({   actions: {     generateAction() {       this.sendAction('generateAction');     },     deleteAction() {       this.sendAction('deleteAction');     }   } }); 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <div  class ="row" >   <div  class ="col-md-12" >      <h3 > {{sectionTitle}}</h3 >      <div  class ="row" >        <div  class ="form-horizontal" >          <label  class ="col-sm-2 control-label" > Number of new records:</label >          <div  class ="col-sm-2" >            {{input value=counter class='form-control'}}         </div >          <div  class ="col-sm-4" >            <button  class ="btn btn-primary"  {{action  'generateAction '}}> Generate {{sectionTitle}}</button >            {{#fader-label isShowing=createReady}}Created!{{/fader-label}}         </div >          <div  class ="col-sm-4" >            <button  class ="btn btn-danger"  {{action  'deleteAction '}}> Delete All {{sectionTitle}}</button >            {{#fader-label isShowing=deleteReady}}Deleted!{{/fader-label}}         </div >        </div >      </div >    </div >  </div > 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // app/components/fader-label.js import Ember from 'ember'; export default Ember.Component.extend({   tagName: 'span',   classNames: ['label label-success label-fade'],   classNameBindings: ['isShowing:label-show'],   isShowing: false,   isShowingChanged: Ember.observer('isShowing', function() {     Ember.run.later(() => {       this.set('isShowing', false);     }, 3000);   }) }); 
代码 classNames: ['label label-success label-fade']的作用是绑定三个CSS类到标签span上,得到html如<span class="label label-success label-fade">xxx</span>。classNameBindings: ['isShowing:label-show']的作用是根据属性isShowing的值判断是否添加CSS类label-show到标签span上。更多有关信息请看Ember.js 入门指南之十二handlebars属性绑定 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // app/styles/app.scss @import  'bootstrap' ;body  {  padding-top : 20px ; } html  {  overflow-y : scroll; } .library-item  {  min-height : 150px ; } .label-fade  {  opacity : 0 ;   @include transition(all 0.5s);   &.label-show  {     opacity : 1 ;   } } 
最主要、最关键的部分来了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <h1 > Seeder, our Data Center</h1 > <div  class ="row" >   <div  class ="col-md-4" > {{number-box title="Libraries" number=libraries.length}}</div >    <div  class ="col-md-4" > {{number-box title="Authors" number=authors.length}}</div >    <div  class ="col-md-4" > {{number-box title="Books" number=books.length}}</div >  </div > {{seeder-block     sectionTitle='Libraries'     counter=librariesCounter     generateAction='generateLibraries'     deleteAction='deleteLibraries'     createReady=libDone     deleteReady=libDelDone }} {{seeder-block   sectionTitle='Authors with Books'   counter=authorCounter   generateAction='generateBooksAndAuthors'   deleteAction='deleteBooksAndAuthors'   createReady=authDone   deleteReady=authDelDone }} 
属性generateAction和deleteAction用于关联控制器中的action方法,属性createReady和deleteReady是标记属性。
等待项目重启完毕,页面结果如下:
底部的两个输入框用于获取生成的数据条数。
安装faker.js构建测试数据 使用faker.js 构建测试数据。
1 ember install ember-faker 
安装完毕之后扩展各个模型,并在模型中调用randomize()方法产生数据。下面是各个模型的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import  Model from  'ember-data/model' ;import  attr from  'ember-data/attr' ;import  { hasMany } from  'ember-data/relationships' ;import  Ember from  'ember' ;import  Faker from  'faker' ;export  default  Model.extend({  name: attr('string' ),   address: attr('string' ),   phone: attr('string' ),   books: hasMany('book' , {inverse : 'library' , async : true }),   isValid: Ember.computed.notEmpty('name' ),   randomize() {     this .set('name' , Faker.company.companyName() + ' Library' );     this .set('address' , this ._fullAddress());     this .set('phone' , Faker.phone.phoneNumber());          return  this ;   },   _fullAddress() {     return  `${Faker.address.streetAddress()} , ${Faker.address.city()} ` ;   } }); 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import  Model from  'ember-data/model' ;import  attr from  'ember-data/attr' ;import  { belongsTo } from  'ember-data/relationships' ;import  Faker from  'faker' ;export  default  Model.extend({  title:        attr('string' ),   releaseYear:  attr('date' ),   author:       belongsTo('author' , {inverse : 'books' , async : true }),   library:      belongsTo('library' , {inverse : 'books' , async : true }),   randomize(author, library) {     this .set('title' , this ._bookTitle());     this .set('author' , author);     this .set('releaseYear' , this ._randomYear());     this .set('library' , library);     return  this ;   },   _bookTitle() {     return  `${Faker.commerce.productName()}  Cookbook` ;   },   _randomYear() {     return  new  Date (this ._getRandomArbitrary(1900 , 2015 ));   },   _getRandomArbitrary(min, max) {     return  Math .random() * (max - min) + min;   } }); 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import  Model from  'ember-data/model' ;import  attr from  'ember-data/attr' ;import  { hasMany } from  'ember-data/relationships' ;import  Faker from  'faker' ;export  default  Model.extend({  name: attr('string' ),   books: hasMany('book' , {inverse : 'author' , async : true }),   randomize() {     this .set('name' , Faker.name.findName());     return  this ;   } }); 
上述代码中。 async设置为true的作用是:在获取book的同时会把关联的author也加载出来,默认是不加载(延迟加载)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 import  Ember from  'ember' ;import  Faker from  'faker' ;export  default  Ember.Controller.extend({  libraries: [],   books: [],   authors: [],   actions: {     generateLibraries() {       const  counter = parseInt (this .get('librariesCounter' ));       for  (let  i = 0 ; i < counter; i++) {         this .store.createRecord('library' ).randomize().save().then(() =>  {           if  (i === counter-1 ) {             this .set('librariesCounter' , 0 );             this .set('libDone' , true );           }         });       }     },     deleteLibraries() {       this ._destroyAll(this .get('libraries' ));       this .set('libDelDone' , true );     },     generateBooksAndAuthors() {       const  counter = parseInt (this .get('authorCounter' ));       for  (let  i = 0 ; i < counter; i++) {         let  newAuthor = this .store.createRecord('author' );         newAuthor.randomize()           .save().then(() =>  {              if  (i === counter-1 ) {                this .set('authorCounter' , 0 );                this .set('authDone' , true );              }           }         );         this ._generateSomeBooks(newAuthor);       }     },     deleteBooksAndAuthors() {       this ._destroyAll(this .get('books' ));       this ._destroyAll(this .get('authors' ));       this .set('authDelDone' , true );     }   },      _generateSomeBooks(author) {     const  bookCounter = Faker.random.number(10 );     for  (let  j = 0 ; j < bookCounter; j++) {       const  library = this ._selectRandomLibrary();       this .store.createRecord('book' )         .randomize(author, library)         .save();       author.save();       library.save();     }   },   _selectRandomLibrary() {     const  libraries = this .get('libraries' );     const  librariesCounter = libraries.get('length' );          const  libraryIds = libraries.map((lib ) =>  {return  lib.get('id' );});     const  randomNumber = Faker.random.number(librariesCounter-1 );     const  randomLibrary = libraries.findBy('id' , libraryIds[randomNumber]);     return  randomLibrary;   },   _destroyAll(records) {     records.forEach((item ) =>  {       item.destroyRecord();     });   } }); 
重启项目,进入到http://localhost:4200/admin/seeder 。在输入框内输入要生成的测试数据条数,然后点击右边的蓝色按钮,如果生成成功可以在按钮右边看到绿色的“created”提示文字。如下图:
然后到firebase上查看。可以看到数据已经存在了,并且是随机的数据。
并且是实现了数据表之间的关联关系,比如一个author对应多个book,如下图。
或者是直接在http://localhost:4200/libraries 下查看。
在接下来的一篇文章中将介绍如何遍历关联关系中的对象,使用起来也是非常简单的,直接使用面向对象的方式遍历即可。
家庭作业 本篇的家庭作业仍然是好好理解组件!参照下面的文章认真学习、理解组件。
Ember.js 入门指南之二十八组件定义 Ember.js 入门指南之二十九属性传递 Ember.js 入门指南之三十包裹内容 Ember.js 入门指南之三十一自定义包裹组件的HTML标签 Ember.js 入门指南之三十二处理事件 Ember.js 入门指南之三十三action触发变化  
为了照顾懒人我把完整的代码放在[GitHub](https://github.com/ubuntuvim/library-app)上,如有需要请可以拿来参照参照。博文经过多次修改,博文上的代码与github代码可能有出入,不过影响不大!如果你觉得博文对你有点用,请在github项目上给我点个`star`吧。您的肯定对我来说是最大的动力!!