編者注:咱們發現了有趣的系列文章《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