来源: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`吧。您的肯定对我来说是最大的动力!!