[译] 第十九天: Ember - 缺失的EmberJS指南
前言
到目前为止 ,这个系列我们探讨了Bower, AngularJS, GruntJS, PhoneGap, 和MeteorJS JavaScript技术,今天,我决定学习一个叫Ember的框架。本文,我们来学习用Ember创建一个单页网摘网页。这个指南会写两篇文章,第一篇讲客户端和长期保存数据到HTML 5本地存储中,第二篇讲用RESTful后端部署到OpenShift. 接下来的几天会完成第二篇。
程序用例
本文,我们来开发一个网摘程序,允许用户发布和共享链接,你可访问在线应用。这个应用可以做以下事:
- 当用户打开应用'/' url,可以看到一个文章列表,以提交日期排序。
- 用户点击任意文章如#/storites/d6p88, 他可以看到文章由谁提交的,什么时候提交的和文章摘要。
- 最后,用户可以到页面#/story/new提交新内容,这会保存在用户浏览器本地缓存中。
什么是Ember?
Ember是一个客户端JavaScript MV* 框架,用来开发艰巨的Web程序,它有jQuery和Handlebars库依赖,如果你用过Backbone, 那你可以发现Ember如同固执的Backbone或者Backbone++. 如果你遵循它的命名规范,Ember可以为你做很多事,Ember.js严格遵循命名规范,假如我们应用中有一个url路径 /stories, 那我们会有以下:
- 一个stories模板
- 一个StoriesRoute
- 一个StoriesController
要更好的了解Ember命名规范,参考文档。
Ember核心概念
这部分我们关注EmberJS四个主要核心概念,今天的demo会用到。
- Model: 模型呈现了我们要展示给用户的程序域对象,以上我们讨论的程序用例中,一篇文章代表一个模型,文章随同它的属性如标题,url等组成了模型,模型可以重置或更新,靠jQuery从服务器加载JSON数据或者程序使用Ember数据. Ember数据是客户端ORM实施,使对底层长期存储执行CRUD操作更简单。Ember数据提供了一个仓库接口,可配置一系列提供的适配器。两个核心适配器是RESTAdapter和FixtureAdapter. 本文我们用LocalStorage适配器,用于长期保存数据到HTML 5 本地存储。详情请参考文档。
- Router和Route: 路由器用于指定程序路由,一个url对应一个路由。例如,用户打开'/#/story/new', 那newstroy模板就会被加载,这个newstory模板代表了一个HTML表格,用户可以通过创建Ember.Route子类自定义路由。以上示例中,假设用户打开'/#/story/new', 想在newstory加载默认模型,NewStoryRoute会给一个默认模型给newstory. 详情请参考文档。
- Controller: 控制器可做两件事:一是装饰路由返回的模板,二是舰艇用户执行动作。例如,当用户用相关数据提交文章后,NewStoryController会用Ember Data API将数据持久保存到底层存储中,详情请参考文档。
- Template: 模板代表程序的用户接口,每个程序都有一个默认模板,当程序启动时被加载。标头,页脚,导航和其他常用内容应该放置在这个模板里。Ember.js用Handlebars 模板库来增强程序用户接口。
Ember Chrome扩展
EmberJS也提供了一个chrome扩展,使程序易于调试,这个扩展在chrome web store有,更多了解请参考Ember团队的简短视频。
Github仓库
今天的demo放在github: day19-emberjs-demo.
第一步:下载starter kit
Ember提供了一个starter kit让我们快速入门。Starter kit包含需要的javascript文件(ember-*.js, jquery-*.js, handlerbars-*.js)和示例程序。下载并解压,最后按以下方式重命名为getbookmarks, getbookmarks是程序名字。
$ wget https://github.com/emberjs/starter-kit/archive/v1.1.2.zip $ unzip v1.1.2.zip $ mv starter-kit-1.1.2/ getbookmarks
用你喜欢的流行浏览器(Chrome或者Firefox)打开index.html, 就可以看到示例程序。
第二步:激活GruntJS Watch(可选)
这步是可选的,不过如果你执行了,会带给你惊喜。要是决定跳过,那每次用浏览器打开index.html或者我们做了任何改动后请重新加载你的浏览器。第七天的时候,我们讨论了用GruntJS实时加载自动更新,EmberJS里我没找到自动加载的功能,所以我决定创造性的用GruntJS实时加载。你需要安装Node, NPM, Grunt-CLI, 请参考我第五和七天的博客,里面有详述。
在getbookmarks文件夹新建package.json文件,粘贴以下内容。
{ "name": "getbookmarks", "version": "0.0.1", "description": "GetBookMarks application", "devDependencies": { "grunt": "~0.4.1", "grunt-contrib-watch": "~0.5.3" } }
再新建个Gruntfile.js的文件然后粘贴以下内容。
module.exports = function(grunt) { grunt.initConfig({ watch :{ scripts :{ files : ['js/app.js','css/*.css','index.html'], options : { livereload : 9090, } } } }); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.registerTask('default', []); };
用npm安装依赖。
$ npm install grunt --save-dev
$ npm install grunt-contrib-watch --save-dev
然后调用grunt watch命令,在你浏览器里打开index.html.
$ grunt watch Running "watch" task Waiting...OK
对index.html做些改动,不刷新浏览器你就可以看到改动生效了。
第三步:理解开始模板程序
在开始模板里,有两个程序相关文件(除了css)-index.html和app.js, 要理解模板程序做了什么,我们需要理解app.js文件。
App = Ember.Application.create(); App.Router.map(function() { // put your routes here }); App.IndexRoute = Ember.Route.extend({ model: function() { return ['red', 'yellow', 'blue']; } });
以上代码:
- 第一行 App = Ember.Application.create(); 创建了Ember程序实例,它将创建Ember程序新实例,使它在浏览器的JavaScript环境作为参数可用。
- App.Router.map用于定义程序路由,每个Ember程序有一个默认路由调用'/' url下可用的引用,当'/'路由被调用了,index模板就会被加载。基于程序url模板加载,很多默认配置,index模板定义在index.html里。
- 在Ember里,每个模板由一个模型支持,一个路由指定模板该由哪个模型支持,以上app.js中,IndexRoute为index模板作为模型返回一个字符串数组,index模板只是递归这个数组和显示列表。
第四步:移除开始模板代码
请删除js/app.js文件的所有内容,粘贴以下内容。
App = Ember.Application.create(); App.Router.map(function() { // put your routes here });
同样的替换index.html.
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>GetBookMarks -- Share your favorite links online</title> <link rel="stylesheet" href="css/normalize.css"> <link rel="stylesheet" href="css/style.css"> <script src="http://localhost:9090/livereload.js"></script> </head> <body> <script type="text/x-handlebars"> {{outlet}} </script> <script type="text/x-handlebars" data-template-name="index"> </script> <script src="js/libs/jquery-1.9.1.js"></script> <script src="js/libs/handlebars-1.0.0.js"></script> <script src="js/libs/ember-1.1.2.js"></script> <script src="js/app.js"></script> </body> </html>
第五步:添加Twitter Bootstrap
我们用twitter boostrap来作为程序样式,从官网下载最新Twitter Bootstrap包,复制bootstrap.css到css文件夹,fonts文件夹到getbookmarks文件夹。
接下来,添加bootstrap.css样式表到index.html, 在页面顶部用固定的导航栏。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>GetBookMarks -- Share your favorite links online</title> <link rel="stylesheet" href="css/normalize.css"> <link rel="stylesheet" type="text/css" href="css/bootstrap.css"> <link rel="stylesheet" href="css/style.css"> <script src="http://localhost:9090/livereload.js"></script> </head> <body> <script type="text/x-handlebars"> <nav class="navbar navbar-default navbar-fixed-top" role="navigation"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="#">GetBookMarks</a> </div> </div> </nav> <div id="main" class="container"> {{outlet}} </div> </script> <script type="text/x-handlebars" data-template-name="index"> </script> <script src="js/libs/jquery-1.9.1.js"></script> <script src="js/libs/handlebars-1.0.0.js"></script> <script src="js/libs/ember-1.1.2.js"></script> <script src="js/app.js"></script> </body> </html>
以上html代码,<script type="text/x-handlebars"> 代表我们application模板,这个模板有一个{{outlet}}标签控制所有其他模板,会根据url改变。
同时添加以下css到css/style.css, 它为主体添加40px的上边距,这为适当加在主体固定顶部导航栏设置的。
body{ padding-top: 40px; }
第六步:提交新文章
开始执行提交新文章功能,在Ember里,建议你根据URL(s)而定,当用户浏览'#/story/new' url, 那么一个表格就应该呈现给用户。
在App.Router.Map里给'#/story/new' 添加一个新的路由。
App.Router.map(function() { this.resource('newstory' , {path : 'story/new'}); });
然后在index.html里添加一个'newstory'模板来加载表格。
<script type="text/x-handlebars" id="newstory"> <form class="form-horizontal" role="form"> <div class="form-group"> <label for="title" class="col-sm-2 control-label">Title</label> <div class="col-sm-10"> <input type="title" class="form-control" id="title" name="title" placeholder="Title of the link" required> </div> </div> <div class="form-group"> <label for="excerpt" class="col-sm-2 control-label">Excerpt</label> <div class="col-sm-10"> <textarea class="form-control" id="excerpt" name="excerpt" placeholder="Short description of the link" required></textarea> </div> </div> <div class="form-group"> <label for="url" class="col-sm-2 control-label">Url</label> <div class="col-sm-10"> <input type="url" class="form-control" id="url" name="url" placeholder="Url of the link" required> </div> </div> <div class="form-group"> <label for="tags" class="col-sm-2 control-label">Tags</label> <div class="col-sm-10"> <textarea id="tags" class="form-control" name="tags" placeholder="Comma seperated list of tags" rows="3" required></textarea> </div> </div> <div class="form-group"> <label for="fullname" class="col-sm-2 control-label">Full Name</label> <div class="col-sm-10"> <input type="text" class="form-control" id="fullname" name="fullname" placeholder="Enter your Full Name like Shekhar Gulati" required> </div> </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <button type="submit" class="btn btn-success" {{action 'save'}}>Submit Story</button> </div> </div> </form> </script>
要看这个表格,只需打开 '#/story/new'.
然后在导航栏添加一个链接,让我们轻松导航到文章提交表格,用以下内容替代nav元素。
<nav class="navbar navbar-default navbar-fixed-top navbar-inverse" role="navigation"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="#">GetBookMarks</a> </div> <ul class="nav navbar-nav pull-right"> <li>{{#link-to 'newstory'}}<span class="glyphicon glyphicon-plus"></span> Submit Story{{/link-to}}</li> </ul> </div> </nav>
以上需要注意的是{{#link-to}}的用法帮助,{{#link-to}}帮助用于创建路由的链接,详情请参考文档。
现在我们可以浏览表格,来添加存储到HTML 5本地存储的功能,要添加本地存储支持,需要先下载Ember Data和本地存储适配器,放到js/libs文件夹,然后添加脚本标签到index.html.
<script src="js/libs/jquery-1.9.1.js"></script> <script src="js/libs/handlebars-1.0.0.js"></script> <script src="js/libs/ember-1.1.2.js"></script> <script src="js/libs/ember-data.js"></script> <script src="js/libs/localstorage_adapter.js"></script> <script src="js/app.js"></script>
前面讨论过,Ember数据是客户端ORM实施,使对底层长期存储执行CRUD操作更简单。这里我们用LSAdapter(Local Storage Adapter). 添加以下内容到app.js.
App.ApplicationAdapter = DS.LSAdapter.extend({ namespace: 'stories' });
然后定义模型,一个文章应该有一个url, 标题,提交文章的用户的全名,文章摘要,提交时间。我们也可以指定属性的类型,如下模型,我们用字符型和日期格式。默认适配器支持属性类型有字符型,数字,布尔型和日期格式。
App.Story = DS.Model.extend({
url : DS.attr('string'),
tags : DS.attr('string'),
fullname : DS.attr('string'),
title : DS.attr('string'),
excerpt : DS.attr('string'),
submittedOn : DS.attr('date')
});
接下来写NewStoryController用于持久保存数据。
App.NewstoryController = Ember.ObjectController.extend({
actions :{
save : function(){
var url = $('#url').val();
var tags = $('#tags').val();
var fullname = $('#fullname').val();
var title = $('#title').val();
var excerpt = $('#excerpt').val();
var submittedOn = new Date();
var store = this.get('store');
var story = store.createRecord('story',{
url : url,
tags : tags,
fullname : fullname,
title : title,
excerpt : excerpt,
submittedOn : submittedOn
});
story.save();
this.transitionToRoute('index');
}
}
});
以上代码获取所有表格值,然后用store API创建一个in-memory记录,要保存记录到本地,我们需要在Story对象上调用save方法,最后,指向index路由。
现在你可以测试程序,创建一个新文章,然后到Chrome 开发者工具,在源文件部分查看文章。
第七步:显示所有文章
接下来的逻辑步骤是当用户查看主页时显示所有文章。
如我之前提到的,一个路由对应一个查询模型,我们来添加IndexRoute用于在本地存储找到所有保存的文章。
App.IndexRoute = Ember.Route.extend({
model : function(){
var stories = this.get('store').findAll('story');
return stories;
}
});
每个路由支持一个模板,这个IndexRoute支持index 模板,在index.html添加index模板。
<script type="text/x-handlebars" id="index"> <div class="row"> <div class="col-md-4"> <table class='table'> <thead> <tr><th>Recent Stories</th></tr> </thead> {{#each controller}} <tr><td> {{title}} </td></tr> {{/each}} </table> </div> <div class="col-md-8"> {{outlet}} </div> </div> </script>
如果我们访问你的程序主页,可以看到文章列表。每个循环递归基于文章集合,这里,控制器相当于indexController.
现在我们可以在'/'路由看到文章。
有一个问题是文章不是按日期排序的,要按提交日期排序,需要创建IndexController用于负责模板的排序,我们说了想按提交日期排序,并且按降序来确保最新的在顶部。
App.IndexController = Ember.ArrayController.extend({
sortProperties : ['submittedOn'],
sortAscending : false
});
更新之后,可以看到是按提交日期排序了。
第八步:查看单独文章
这个程序我们要做的最后一个功能是当用户点击一篇文章,他应该可以看到详细内容,要执行这个需要添加以下路由。
App.Router.map(function() {
this.resource('index',{path : '/'},function(){
this.resource('story', { path:'/stories/:story_id' });
});
this.resource('newstory' , {path : 'story/new'});
});
以上代码是一个嵌套路由示例。
:story_id部分调用动态字段,因为相应的文章id会加到URL.
接下来我们添加StoryRoute用于找到文章id对应的文章。
App.StoryRoute = Ember.Route.extend({
model : function(params){
var store = this.get('store');
return store.find('story',params.story_id);
}
});
最后,更新index.html从index模板链接到每个文章,用{{#link-to}}在每个循环里完成。
<script type="text/x-handlebars" id="index"> <div class="row"> <div class="col-md-4"> <table class='table'> <thead> <tr><th>Recent Stories</th></tr> </thead> {{#each controller}} <tr><td> {{#link-to 'story' this}} {{title}} {{/link-to}} </td></tr> {{/each}} </table> </div> <div class="col-md-8"> {{outlet}} </div> </div> </script> <script type="text/x-handlebars" id="story"> <h1>{{title}}</h1> <h2> by {{fullname}} <small class="muted">{{submittedOn}}</small></h2> {{#each tagnames}} <span class="label label-primary">{{this}}</span> {{/each}} <hr> <p class="lead"> {{excerpt}} </p> </script>
更新好后,可以在你浏览器里看到这些更新。
第九步:格式化提交日期的格式
Ember有助手概念,这些助手功能可以从任意Handlebars模板调用。
我们用moment.js库格式化日期,添加以下内容到index.html.
<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.4.0/moment.min.js"></script>
然后,定义第一个助手用于格式化日期便于我们阅读。
Ember.Handlebars.helper('format-date', function(date){
return moment(date).fromNow();
});
最后添加format-date助手。
<script type="text/x-handlebars" id="story"> <h1>{{title}}</h1> <h2> by {{fullname}} <small class="muted">{{format-date submittedOn}}</small></h2> {{#each tagnames}} <span class="label label-primary">{{this}}</span> {{/each}} <hr> <p class="lead"> {{excerpt}} </p> </script>
你可以看到这些更新。
这就是今天的内容,继续给反馈吧。
原文:https://www.openshift.com/blogs/day-19-ember-the-missing-emberjs-tutorial