Day 19: EmberJS 入门指南
编者注:我们发现了有趣的系列文章《30天学习30种新技术》,正在翻译,一天一篇更新,年终礼包。下面是第19天的内容。
到目前为止,我们这一系列文章涉及了Bower、AngularJS、GruntJS、PhoneGap和MeteorJS 这些JavaScript技术。今天我打算学习一个名为Ember的框架。本文将介绍如何用Ember创建一个单页面的社交化书签应用。本教程将包括两篇:第1篇介绍客户端代码和用HTML 5本地存储持久保存数据,第2篇中我们将使用一个部署在OpenShift上的REST后端。过几天我会写第2篇。
应用
我们将开发一个社交化书签应用,允许用户提交和分享链接。你可以在这里查看这个应用。这个应用可以做到:
当用户访问
/时,他会看到以提交时间排序的报道列表。当用户访问某个书签时,例如
#/stories/d6p88,用户会看到关于这个报道的信息,例如是谁提交的,何时提交的,以及文章的摘要。最后,当用户通过
#/story/new提交新报道时,内容会存储在用户浏览器的本地存储上。
什么是Ember?
Ember是一个客户端的JavaScript MV* 框架,用来构建野心勃勃的web应用。它依赖于jQuery和Handlebars库。如果你曾经在Backbone下工作,那么你会发现Ember是一个武断的Backbone,或者Backbone++。Ember可以为你完成很多事情,如果你遵循它的命名约定的话。Ember.js在这方面很突出。因此,如果我们在应用中加入了url路由和报道,那么我们就有了这些:
- 报道的模板
 - StoriesRoute
 - StoriesController
 
请参考命名约定文档来理解Ember的命名约定。
Ember核心概念
本节将介绍我们的示例应用中将涉及的四个EmberJS的核心概念:
模型:模型代表我们展示给用户的应用领域内的对象。在上述例子中,一个报道就代表一个模型。报道,加上它的属性,包括标题、url等,构成一个模型。模型可以通过jQuery加载服务器端的JSON数据的方式来获取和更新,也可以通过Ember Data来获取和更新。Ember Data是一个客户端的ORM实现,可以利用它方便地对底层的持久性存储进行CRUD操作。Ember Data提供一个仓库接口,可以借助提供的一些适配器配置。Ember Data提供的两个核心适配器是RESTAdapter和FixtureAdapter。在本文中,我们将使用LocalStorage适配器,该适配器将数据持久化为 HTML 5 的LocalStorage。请参阅此文档了解详情。
路由器和路由:路由器指定应用的所有路由。路由器将URL映射到路由。例如,当一个用户访问
/#/story/new的时候,将渲染newstory模板。该模板展现了一个HTML表单。用户可通过创建Ember.Route子类来定制路由。在上述例子中,用户访问/#/story/new将渲染一个基于newstory模板的默认模型。NewStoryRoute会负责将默认的模型分配给newstory模板。请参阅文档了解详情。控制器:控制器可以做两件事——首先它装饰路由返回的模型,接着它监听用户执行的行动。例如,当用户提交报道的时候,NewStoryController负责通过Ember Data API将报道的数据持续化到存储层。请参阅文档了解详情。
模版:模板向用户展示应用的界面。每个应用都有一个默认的应用模板。
Ember的Chrome插件
EmberJS提供了一个Chrome插件,因此调试ember应用很容易。这个插件可以在 chrome web store 下载安装。可以查看Ember团队做的视频了解chrome插件的详情。
Github仓库
今天的示例程序的代码可从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,你会看到如下页面:
第二步 启用GruntJS监视
这一步是可选的,不过如果你做了这步,那么你的生活质量将大大提高。如果你决定跳过这步,那么每次你做了改动之后都需要刷新浏览器。在第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来给应用添加样式。从官网下载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>
修改完毕地后,可以在浏览器中直接看到结果。
第八步 为submittedOn日期添加格式
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
本文转载于猿2048:Day 19: EmberJS 入门指南
Day 19: EmberJS 入门指南的更多相关文章
- AngularJS快速入门指南19:示例代码
		
本文给出的大部分示例都可以直接运行,通过点击运行按钮来查看结果,同时支持在线编辑代码. <div ng-app=""> <p>Name: <input ...
 - AngularJS快速入门指南20:快速参考
		
thead>tr>th, table.reference>tbody>tr>th, table.reference>tfoot>tr>th, table ...
 - AngularJS快速入门指南18:Application
		
是时候创建一个真正的AngularJS单页面应用程序了(SPA). 一个AngularJS应用程序示例 你已经了解了足够多的内容来创建第一个AngularJS应用程序: My Note Save Cl ...
 - AngularJS快速入门指南03:表达式
		
AngularJS通过表达式将数据绑定到HTML. AngularJS表达式 AngularJS表达式写在双大括号中:{{ 表达式语句 }}. AngularJS表达式绑定数据到HTML的方式与ng- ...
 - AngularJS快速入门指南01:导言
		
AngularJS使用新的attributes扩展了HTML AngularJS对单页面应用的支持非常好(SPAs) AngularJS非常容易学习 现在就开始学习AngularJS吧! 关于本指南 ...
 - Web API入门指南(安全)转
		
安全检测的工具站点:https://www.owasp.org/index.php/Category:Vulnerability_Scanning_Tools Web API入门指南有些朋友回复问了些 ...
 - 《Gulp 入门指南》 : 使用 gulp 压缩 JS
		
<Gulp 入门指南> : 使用 gulp 压缩 JS 请务必理解如下章节后阅读此章节: 安装 Node 和 gulp 访问论坛获取帮助 压缩 js 代码可降低 js 文件大小,提高页面打 ...
 - Webstorm10.0.3破解程序及汉化包下载、Webstorm配置入门指南
		
核心提示: WebStorm 是jetbrains公司旗下一款JavaScript 开发工具.被广大中国JS开发者誉为“Web前端开发神器”.“最强大的HTML5编辑器”.“最智能的JavaSscri ...
 - Web API 入门指南
		
Web API 入门指南 - 闲话安全2013-09-21 18:56 by 微软互联网开发支持, 231 阅读, 3 评论, 收藏, 编辑 Web API入门指南有些朋友回复问了些安全方面的问题,安 ...
 
随机推荐
- 『无为则无心』Python日志 — 67、logging日志模块处理流程
			
目录 1.概括理解 2.详细说明 3.应用示例 1.概括理解 了解了四大组件的基本定义之后,我们通过图示的方式来理解下信息的传递过程: 也就是获取的日志信息,进入到Logger日志器中,传递给处理器确 ...
 - Java基本规范
			
1.Java是区分大小写的语言,关键字的大小不能写错,例如把class写成Class或者CLASS,都会导致出错. 2.在一个类的内部不能定义其他的类,即类和类之间是平行而非嵌套的关系. 3.一个程序 ...
 - linux多进/线程编程(7)——多线程1(线程的创建,回收,分离,设置线程属性等)
			
参考资料: 1.博客1:https://blog.csdn.net/zhou1021jian/article/details/71531699 2.博客2:https://blog.csdn.net/ ...
 - Python第三讲
			
今日内容概要 数据类型及内置方法 字符编码 文件处理 函数简介 今日内容详细 数据类型及内置方法 列表 name_list =['jason','oscar','tony','kevin'] 1.索引 ...
 - 防止SQL 注入;如何进行防SQL 注入。
			
防止SQL 注入:1.开启配置文件中的magic_quotes_gpc 和magic_quotes_runtime 设置2.执行sql 语句时使用addslashes 进行sql 语句转换3.Sql ...
 - 【简记】Linux界面连接WPA2-Enterprise Wifi
			
背景 公司从去年(2021)年开始使用 WPS2-Enterprise 安全方式,我的笔记本来公司就得用网线才能连(因为当时没意识到,只是以为连不上可能和驱动有关系,这是大错特错的理解..)赶上今天去 ...
 - JDBC学习一---JDBC入门
			
原文链接 今天开始会写一系列 Java 后端学习的笔记,一方面是为了以后翻阅查看,更主要的原因是通过写作输出的方式让自己的印象更深,避免遗忘. 首先是简单记录下自己学习使用 JDBC 的历程,由于目前 ...
 - 虚拟内存之后pagefile.sys不断的再增大快占满整个C盘,应该如何将处理?
			
"pagefile.sys"是页面交换文件,这个文件不能删除,不过我们可以改变其大小和存放位置. 1.右击"这台电脑/属性". 2.然后在对话框的"高 ...
 - LGP7704题解
			
来一个特别暴力的做法. 首先,如果删掉 \(x\) 和 \(y\) 的效果一定和删掉 \(xy\) 的效果相同,且代价一定不大于后者. 于是我们只删除质数,题目就变成了寻找 \(i!(1 \leq i ...
 - 活用Windows Server 2008系统的几种安全功能
			
与传统操作系统相比,Win2008系统的安全防范功能更加强大,安全防护能力自然也是高人一等,我们只要在平时善于使用该系统新增的各项安全防范功能,完全可以实现更高级别的安全保护目的.现在,本文就为大家贡 ...