编者注:咱们发现了有趣的系列文章《30天学习30种新技术》,正在翻译,一天一篇更新,年终礼包。下面是第19天的内容。javascript
到目前为止,咱们这一系列文章涉及了Bower、AngularJS、GruntJS、PhoneGap和MeteorJS 这些JavaScript技术。今天我打算学习一个名为Ember的框架。本文将介绍如何用Ember建立一个单页面的社交化书签应用。本教程将包括两篇:第1篇介绍客户端代码和用HTML 5本地存储持久保存数据,第2篇中咱们将使用一个部署在OpenShift上的REST后端。过几天我会写第2篇。css
咱们将开发一个社交化书签应用,容许用户提交和分享连接。你能够在这里查看这个应用。这个应用能够作到:html
当用户访问/
时,他会看到以提交时间排序的报道列表。html5
当用户访问某个书签时,例如#/stories/d6p88
,用户会看到关于这个报道的信息,例如是谁提交的,什么时候提交的,以及文章的摘要。java
最后,当用户经过#/story/new
提交新报道时,内容会存储在用户浏览器的本地存储上。jquery
Ember是一个客户端的JavaScript MV* 框架,用来构建野心勃勃的web应用。它依赖于jQuery和Handlebars库。若是你曾经在Backbone下工做,那么你会发现Ember是一个武断的Backbone,或者Backbone++。Ember能够为你完成不少事情,若是你遵循它的命名约定的话。Ember.js在这方面很突出。所以,若是咱们在应用中加入了url路由和报道,那么咱们就有了这些:git
请参考命名约定文档来理解Ember的命名约定。github
本节将介绍咱们的示例应用中将涉及的四个EmberJS的核心概念:web
模型:模型表明咱们展现给用户的应用领域内的对象。在上述例子中,一个报道就表明一个模型。报道,加上它的属性,包括标题、url等,构成一个模型。模型能够经过jQuery加载服务器端的JSON数据的方式来获取和更新,也能够经过Ember Data来获取和更新。Ember Data是一个客户端的ORM实现,能够利用它方便地对底层的持久性存储进行CRUD操做。Ember Data提供一个仓库接口,能够借助提供的一些适配器配置。Ember Data提供的两个核心适配器是RESTAdapter和FixtureAdapter。在本文中,咱们将使用LocalStorage适配器,该适配器将数据持久化为 HTML 5 的LocalStorage。请参阅此文档了解详情。ajax
路由器和路由:路由器指定应用的全部路由。路由器将URL映射到路由。例如,当一个用户访问/#/story/new
的时候,将渲染newstory
模板。该模板展示了一个HTML表单。用户可经过建立Ember.Route
子类来定制路由。在上述例子中,用户访问/#/story/new
将渲染一个基于newstory
模板的默认模型。NewStoryRoute
会负责将默认的模型分配给newstory
模板。请参阅文档了解详情。
控制器:控制器能够作两件事——首先它装饰路由返回的模型,接着它监听用户执行的行动。例如,当用户提交报道的时候,NewStoryController负责经过Ember Data API将报道的数据持续化到存储层。请参阅文档了解详情。
模版:模板向用户展现应用的界面。每一个应用都有一个默认的应用模板。
EmberJS提供了一个Chrome插件,所以调试ember应用很容易。这个插件能够在 chrome web store 下载安装。能够查看Ember团队作的视频了解chrome插件的详情。
今天的示例程序的代码可从github取得。
ember提供了一套新手装备,所以开始使用框架很是简单。新手套装包括了须要用到的javascript文件(ember-*.js
、jquery-*.js
和handlerbars-*.js
)以及示例应用。下载新手套装,解压缩,最后重命名为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
在浏览器中打开index.html
,你会看到以下页面:
这一步是可选的,不过若是你作了这步,那么你的生活质量将大大提升。若是你决定跳过这步,那么每次你作了改动以后都须要刷新浏览器。在第7天的文章,我讨论了GruntJS的在线重载功能。我没有在EmberJS里找到任何自动重载的功能,所以我决定使用GruntJS的livereload来提升效率。你须要Node、NPM和Grunt-CLI。请参考我第5天和第7天的文章了解详情。
在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
在index.html
的头部加入:
<script src="http://localhost:9090/livereload.js"></script>
调用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']; } });
解释下以上的代码:
第一行建立了一个Ember应用的实例。
使用App.Route.map
定义应用的路由。每一个Ember应用都有一个默认路由Index
,绑定到/
。因此,当调用/
路由的时候,index
模板将被渲染。index
模板由index.html
定义。感受到了不少“约定大于配置”了吧?
在Ember中,每一个模板都有一个model做为支持。路由负责制定哪一个mobdel支持哪一个模板。在上述app.js
中,IndexRoute
返回一个字符串数组,做为index模板的model。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 bootstrap包,而后复制bootstrap.css
到css文件夹,同时复制字体文件夹。
接着在index.html
中加入bootstrap.css
,在页首使用一个固定位置的导航条。
<!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">
表明咱们的应用模板。应用模板使用{{outlet}}
标签为其余模板预留位置,其内容取决于url。
在css/style.css
中加入下面的代码。这会在正文上方添加一个40px的空白。这样才能正确地渲染固定位置的导航条。
body{ padding-top: 40px; }
咱们将开始实现提交新报道的功能。Ember建议你围绕着URL思考。当用户访问#/story/new
的时候,会展现一个表单。
在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}}
建立了一个指向路由的连接。请参阅文档了解详情。
表单已经有了,接下来要添加HTML 5本地存储的功能。为了添加本地存储支持,咱们须要首先下载Ember Data和Local Storage Adapter JavaScript文件。将这些文件放在js/libs
下。接着,在index.html
中添加这些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/libs/ember-data.js"></script> <script src="js/libs/localstorage_adapter.js"></script> <script src="js/app.js"></script>
如前所述,Ember Data是一个客户端的ORM实现,它使在底层存储进行CRUD操做很容易。这里咱们将使用LSAdapter。在app.js
中加入:
App.ApplicationAdapter = DS.LSAdapter.extend({ namespace: 'stories' });
接着是定义model。一篇报道须要有url、title(标题)、fullname(提交报道的用户的全名)、excerpt(摘要),以及SubmittedOn(日期)信息。在下面的模型中,咱们使用了字符串和日期类型。适配器默认支持的属性类型为字符串、数字、布尔值和日期。
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在内存中建立记录。为了在localstorage中存储记录,咱们须要调用Story对象的save方法。最后,咱们将用户重定向到index
路由。
接着咱们测试下这个应用,建立一个新的报道,接着打开Chrome开发者工具,在资源区域你能够查看这则报道。
接着咱们要作的是,当用户访问首页的时候,展现全部报道。
正如我以前提到的,路由负责询问model。咱们将加上IndexRoute,它会找出本地存储中保存的全部报道。
App.IndexRoute = Ember.Route.extend({ model : function(){ var stories = this.get('store').findAll('story'); return stories; } });
每一个路由支持一个模板。IndexRoute支持index模板,所以咱们须要修改index.html
:
<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负责排序。咱们指定依照submittedOn
属性倒序排列,以确保新的报道出如今上面。
App.IndexController = Ember.ArrayController.extend({ sortProperties : ['submittedOn'], sortAscending : false });
修改以后,咱们会看到按照submittedOn属性排序的报道。
最后要实现的功能是:用户点击某则报道的时候会看到详细信息。咱们加一个路由:
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。
而后咱们添加根据报道id获取报道的StoryRoute。
App.StoryRoute = Ember.Route.extend({ model : function(params){ var store = this.get('store'); return store.find('story',params.story_id); } });
最后,咱们更新下index.html
,给每一个报道添加连接:
<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-data
辅助函数。
<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>
报道页面的效果以下:
今天就到这里了。持续反馈。
原文 Day 19: Ember--The Missing EmberJS Tutorial
翻译 SegmentFault