[译] 第十九天: Ember - 缺失的EmberJS指南

前言

到目前为止 ,这个系列我们探讨了Bower, AngularJS, GruntJS, PhoneGap, 和MeteorJS JavaScript技术,今天,我决定学习一个叫Ember的框架。本文,我们来学习用Ember创建一个单页网摘网页。这个指南会写两篇文章,第一篇讲客户端和长期保存数据到HTML 5本地存储中,第二篇讲用RESTful后端部署到OpenShift. 接下来的几天会完成第二篇。 

程序用例

本文,我们来开发一个网摘程序,允许用户发布和共享链接,你可访问在线应用。这个应用可以做以下事:

  1. 当用户打开应用'/' url,可以看到一个文章列表,以提交日期排序。 
  2. 用户点击任意文章如#/storites/d6p88, 他可以看到文章由谁提交的,什么时候提交的和文章摘要。 

  3. 最后,用户可以到页面#/story/new提交新内容,这会保存在用户浏览器本地缓存中。 

什么是Ember?

Ember是一个客户端JavaScript MV* 框架,用来开发艰巨的Web程序,它有jQueryHandlebars库依赖,如果你用过Backbone, 那你可以发现Ember如同固执的Backbone或者Backbone++. 如果你遵循它的命名规范,Ember可以为你做很多事,Ember.js严格遵循命名规范,假如我们应用中有一个url路径 /stories, 那我们会有以下:

  • 一个stories模板
  • 一个StoriesRoute
  • 一个StoriesController 

要更好的了解Ember命名规范,参考文档。 

Ember核心概念

这部分我们关注EmberJS四个主要核心概念,今天的demo会用到。

  1. Model: 模型呈现了我们要展示给用户的程序域对象,以上我们讨论的程序用例中,一篇文章代表一个模型,文章随同它的属性如标题,url等组成了模型,模型可以重置或更新,靠jQuery从服务器加载JSON数据或者程序使用Ember数据. Ember数据是客户端ORM实施,使对底层长期存储执行CRUD操作更简单。Ember数据提供了一个仓库接口,可配置一系列提供的适配器。两个核心适配器是RESTAdapter和FixtureAdapter. 本文我们用LocalStorage适配器,用于长期保存数据到HTML 5 本地存储。详情请参考文档
  2. RouterRoute: 路由器用于指定程序路由,一个url对应一个路由。例如,用户打开'/#/story/new', 那newstroy模板就会被加载,这个newstory模板代表了一个HTML表格,用户可以通过创建Ember.Route子类自定义路由。以上示例中,假设用户打开'/#/story/new', 想在newstory加载默认模型,NewStoryRoute会给一个默认模型给newstory. 详情请参考文档
  3. Controller: 控制器可做两件事:一是装饰路由返回的模板,二是舰艇用户执行动作。例如,当用户用相关数据提交文章后,NewStoryController会用Ember Data API将数据持久保存到底层存储中,详情请参考文档
  4. 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
View Code

 

用你喜欢的流行浏览器(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"

  }

}
View Code

 

再新建个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', []); 

};
View Code

 

用npm安装依赖。

$ npm install grunt --save-dev

$ npm install grunt-contrib-watch --save-dev
View Code

 

然后调用grunt watch命令,在你浏览器里打开index.html.

$ grunt watch

Running "watch" task

Waiting...OK
View Code

 

对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'];

  }

});
View Code

 

以上代码:

  1. 第一行 App = Ember.Application.create(); 创建了Ember程序实例,它将创建Ember程序新实例,使它在浏览器的JavaScript环境作为参数可用。
  2. App.Router.map用于定义程序路由,每个Ember程序有一个默认路由调用'/' url下可用的引用,当'/'路由被调用了,index模板就会被加载。基于程序url模板加载,很多默认配置,index模板定义在index.html里。
  3. 在Ember里,每个模板由一个模型支持,一个路由指定模板该由哪个模型支持,以上app.js中,IndexRoute为index模板作为模型返回一个字符串数组,index模板只是递归这个数组和显示列表。 

第四步:移除开始模板代码

请删除js/app.js文件的所有内容,粘贴以下内容。

App = Ember.Application.create(); 

App.Router.map(function() {

  // put your routes here

});
View Code

 

同样的替换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>
View Code

 

第五步:添加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>
View Code

 

以上html代码,<script type="text/x-handlebars"> 代表我们application模板,这个模板有一个{{outlet}}标签控制所有其他模板,会根据url改变。 

同时添加以下css到css/style.css, 它为主体添加40px的上边距,这为适当加在主体固定顶部导航栏设置的。

body{

    padding-top: 40px;

}
View Code

 

第六步:提交新文章

开始执行提交新文章功能,在Ember里,建议你根据URL(s)而定,当用户浏览'#/story/new' url, 那么一个表格就应该呈现给用户。 

在App.Router.Map里给'#/story/new' 添加一个新的路由。

App.Router.map(function() {

  this.resource('newstory' , {path : 'story/new'});

});
View Code

 

然后在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>
View Code

 

要看这个表格,只需打开 '#/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>
View Code

 

以上需要注意的是{{#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>
View Code

 

前面讨论过,Ember数据是客户端ORM实施,使对底层长期存储执行CRUD操作更简单。这里我们用LSAdapter(Local Storage Adapter). 添加以下内容到app.js.

App.ApplicationAdapter = DS.LSAdapter.extend({

  namespace: 'stories'

});
View Code

 

然后定义模型,一个文章应该有一个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') 

});

 
View Code

 

接下来写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');

    }

 }

});
View Code

 

以上代码获取所有表格值,然后用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;

    }

});

 
View Code

 

每个路由支持一个模板,这个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>
View Code

 

如果我们访问你的程序主页,可以看到文章列表。每个循环递归基于文章集合,这里,控制器相当于indexController.

现在我们可以在'/'路由看到文章。

有一个问题是文章不是按日期排序的,要按提交日期排序,需要创建IndexController用于负责模板的排序,我们说了想按提交日期排序,并且按降序来确保最新的在顶部。

App.IndexController = Ember.ArrayController.extend({

    sortProperties : ['submittedOn'],

    sortAscending : false

});
View Code

 

更新之后,可以看到是按提交日期排序了。 

第八步:查看单独文章

这个程序我们要做的最后一个功能是当用户点击一篇文章,他应该可以看到详细内容,要执行这个需要添加以下路由。

App.Router.map(function() {

    this.resource('index',{path : '/'},function(){

        this.resource('story', { path:'/stories/:story_id' });

    }); 

    this.resource('newstory' , {path : 'story/new'}); 

});
View Code

 

以上代码是一个嵌套路由示例。 

: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);

    }

});
View Code

 

最后,更新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>
View Code

 

更新好后,可以在你浏览器里看到这些更新。 

第九步:格式化提交日期的格式

Ember有助手概念,这些助手功能可以从任意Handlebars模板调用。 

我们用moment.js库格式化日期,添加以下内容到index.html.

<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.4.0/moment.min.js"></script>
View Code

 

然后,定义第一个助手用于格式化日期便于我们阅读。

Ember.Handlebars.helper('format-date', function(date){

    return moment(date).fromNow();

});
View Code

 

最后添加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>
View Code

 

你可以看到这些更新。 

这就是今天的内容,继续给反馈吧。

原文:https://www.openshift.com/blogs/day-19-ember-the-missing-emberjs-tutorial

posted on 2014-01-06 21:43  百花宫  阅读(1371)  评论(0编辑  收藏  举报