编者注:我们发现了有趣的系列文章《30天学习30种新技术》,正在翻译,一天一篇更新,年终礼包。下面是第19天的内容。


到目前为止,我们这一系列文章涉及了BowerAngularJSGruntJSPhoneGapMeteorJS 这些JavaScript技术。今天我打算学习一个名为Ember的框架。本文将介绍如何用Ember创建一个单页面的社交化书签应用。本教程将包括两篇:第1篇介绍客户端代码和用HTML 5本地存储持久保存数据,第2篇中我们将使用一个部署在OpenShift上的REST后端。过几天我会写第2篇。

应用

我们将开发一个社交化书签应用,允许用户提交和分享链接。你可以在这里查看这个应用。这个应用可以做到:

  • 当用户访问/时,他会看到以提交时间排序的报道列表。

  • 当用户访问某个书签时,例如#/stories/d6p88,用户会看到关于这个报道的信息,例如是谁提交的,何时提交的,以及文章的摘要。

  • 最后,当用户通过#/story/new提交新报道时,内容会存储在用户浏览器的本地存储上。

什么是Ember?

Ember是一个客户端的JavaScript MV* 框架,用来构建野心勃勃的web应用。它依赖于jQueryHandlebars库。如果你曾经在Backbone下工作,那么你会发现Ember是一个武断的Backbone,或者Backbone++。Ember可以为你完成很多事情,如果你遵循它的命名约定的话。Ember.js在这方面很突出。因此,如果我们在应用中加入了url路由和报道,那么我们就有了这些:

  • 报道的模板
  • StoriesRoute
  • StoriesController

请参考命名约定文档来理解Ember的命名约定。

Ember核心概念

本节将介绍我们的示例应用中将涉及的四个EmberJS的核心概念:

  1. 模型:模型代表我们展示给用户的应用领域内的对象。在上述例子中,一个报道就代表一个模型。报道,加上它的属性,包括标题、url等,构成一个模型。模型可以通过jQuery加载服务器端的JSON数据的方式来获取和更新,也可以通过Ember Data来获取和更新。Ember Data是一个客户端的ORM实现,可以利用它方便地对底层的持久性存储进行CRUD操作。Ember Data提供一个仓库接口,可以借助提供的一些适配器配置。Ember Data提供的两个核心适配器是RESTAdapter和FixtureAdapter。在本文中,我们将使用LocalStorage适配器,该适配器将数据持久化为 HTML 5 的LocalStorage。请参阅此文档了解详情。

  2. 路由器和路由:路由器指定应用的所有路由。路由器将URL映射到路由。例如,当一个用户访问/#/story/new的时候,将渲染newstory模板。该模板展现了一个HTML表单。用户可通过创建Ember.Route子类来定制路由。在上述例子中,用户访问/#/story/new将渲染一个基于newstory模板的默认模型。NewStoryRoute会负责将默认的模型分配给newstory模板。请参阅文档了解详情。

  3. 控制器:控制器可以做两件事——首先它装饰路由返回的模型,接着它监听用户执行的行动。例如,当用户提交报道的时候,NewStoryController负责通过Ember Data API将报道的数据持续化到存储层。请参阅文档了解详情。

  4. 模版:模板向用户展示应用的界面。每个应用都有一个默认的应用模板。

Ember的Chrome插件

EmberJS提供了一个Chrome插件,因此调试ember应用很容易。这个插件可以在 chrome web store 下载安装。可以查看Ember团队做的视频了解chrome插件的详情。

Github仓库

今天的示例程序的代码可从github取得。

第一步 下载新手套装

ember提供了一套新手装备,因此开始使用框架非常简单。新手套装包括了需要用到的javascript文件(ember-*.jsjquery-*.jshandlerbars-*.js)以及示例应用。下载新手套装,解压缩,最后重命名为getbookmarks

  1. wget https://github.com/emberjs/starter-kit/archive/v1.1.2.zip
  2. unzip v1.1.2.zip
  3. mv starter-kit-1.1.2/ getbookmarks

在浏览器中打开index.html,你会看到如下页面:

第二步 启用GruntJS监视

这一步是可选的,不过如果你做了这步,那么你的生活质量将大大提高。如果你决定跳过这步,那么每次你做了改动之后都需要刷新浏览器。在第7天的文章,我讨论了GruntJS的在线重载功能。我没有在EmberJS里找到任何自动重载的功能,因此我决定使用GruntJS的livereload来提高效率。你需要Node、NPM和Grunt-CLI。请参考我第5天第7天的文章了解详情。

getbookmarks文件夹内创建package.json,内容如下:

  1. {
  2. "name": "getbookmarks",
  3. "version": "0.0.1",
  4. "description": "GetBookMarks application",
  5. "devDependencies": {
  6. "grunt": "~0.4.1",
  7. "grunt-contrib-watch": "~0.5.3"
  8. }
  9. }

创建Gruntfile.js,内容如下:

  1. module.exports = function(grunt) {
  2. grunt.initConfig({
  3. watch :{
  4. scripts :{
  5. files : ['js/app.js','css/*.css','index.html'],
  6. options : {
  7. livereload : 9090,
  8. }
  9. }
  10. }
  11. });
  12. grunt.loadNpmTasks('grunt-contrib-watch');
  13. grunt.registerTask('default', []);
  14. };

使用npm安装依赖:

  1. npm install grunt --save-dev
  2. npm install grunt-contrib-watch --save-dev

index.html的头部加入:

  1. <script src="http://localhost:9090/livereload.js"></script>

调用grunt watch命令,同时在你的默认浏览器中打开index.html

  1. ; grunt watch
  2. Running "watch" task
  3. Waiting...OK

修改index.html,无需刷新就能看到改变:

第三步 理解新手模板应用

在新手模板中,除了css之外,有两个和应用相关的文件——index.htmlapp.js。为了理解模板应用的作用,我们需要理解app.js

  1. App = Ember.Application.create();
  2. App.Router.map(function() {
  3. // put your routes here
  4. });
  5. App.IndexRoute = Ember.Route.extend({
  6. model: function() {
  7. return ['red', 'yellow', 'blue'];
  8. }
  9. });

解释下以上的代码:

  1. 第一行创建了一个Ember应用的实例。

  2. 使用App.Route.map定义应用的路由。每个Ember应用都有一个默认路由Index,绑定到/。所以,当调用/路由的时候,index模板将被渲染。index模板由index.html定义。感觉到了很多“约定大于配置”了吧?

  3. 在Ember中,每个模板都有一个model作为支持。路由负责制定哪个mobdel支持哪个模板。在上述app.js中,IndexRoute返回一个字符串数组,作为index模板的model。index模板迭代这个数组然后渲染一个列表。

第四步 移除新手模板代码

移除js/app.js中的代码,然后用以下内容替换:

  1. App = Ember.Application.create();
  2. App.Router.map(function() {
  3. // put your routes here
  4. });

相应地,将index.html的内容替换为:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>GetBookMarks -- Share your favorite links online</title>
  6. <link rel="stylesheet" href="css/normalize.css">
  7. <link rel="stylesheet" href="css/style.css">
  8. <script src="http://localhost:9090/livereload.js"></script>
  9. </head>
  10. <body>
  11. <script type="text/x-handlebars">
  12. {{outlet}}
  13. </script>
  14. <script type="text/x-handlebars" data-template-name="index">
  15. </script>
  16. <script src="js/libs/jquery-1.9.1.js"></script>
  17. <script src="js/libs/handlebars-1.0.0.js"></script>
  18. <script src="js/libs/ember-1.1.2.js"></script>
  19. <script src="js/app.js"></script>
  20. </body>
  21. </html>

第五步 添加Twitter Bootstrap

我们将使用twitter bootstrap来给应用添加样式。从官网下载twitter bootstrap包,然后复制bootstrap.css到css文件夹,同时复制字体文件夹。

接着在index.html中加入bootstrap.css,在页首使用一个固定位置的导航条。

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>GetBookMarks -- Share your favorite links online</title>
  6. <link rel="stylesheet" href="css/normalize.css">
  7. <link rel="stylesheet" type="text/css" href="css/bootstrap.css">
  8. <link rel="stylesheet" href="css/style.css">
  9. <script src="http://localhost:9090/livereload.js"></script>
  10. </head>
  11. <body>
  12. <script type="text/x-handlebars">
  13. <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
  14. <div class="container">
  15. <div class="navbar-header">
  16. <a class="navbar-brand" href="#">GetBookMarks</a>
  17. </div>
  18. </div>
  19. </nav>
  20. <div id="main" class="container">
  21. {{outlet}}
  22. </div>
  23. </script>
  24. <script type="text/x-handlebars" data-template-name="index">
  25. </script>
  26. <script src="js/libs/jquery-1.9.1.js"></script>
  27. <script src="js/libs/handlebars-1.0.0.js"></script>
  28. <script src="js/libs/ember-1.1.2.js"></script>
  29. <script src="js/app.js"></script>
  30. </body>
  31. </html>

上述html中,<script type="text/x-handlebars">代表我们的应用模板。应用模板使用{{outlet}}标签为其他模板预留位置,其内容取决于url。

css/style.css中加入下面的代码。这会在正文上方添加一个40px的空白。这样才能正确地渲染固定位置的导航条。

  1. body{
  2. padding-top: 40px;
  3. }

第五步 提交新报道

我们将开始实现提交新报道的功能。Ember建议你围绕着URL思考。当用户访问#/story/new的时候,会展示一个表单。

App.Router.Map中增加一个绑定#/story/new的新路由:

  1. App.Router.map(function() {
  2. this.resource('newstory' , {path : 'story/new'});
  3. });

接着我们在index.html中添加一个渲染表单的newstory模板:

  1. <script type="text/x-handlebars" id="newstory">
  2. <form class="form-horizontal" role="form">
  3. <div class="form-group">
  4. <label for="title" class="col-sm-2 control-label">Title</label>
  5. <div class="col-sm-10">
  6. <input type="title" class="form-control" id="title" name="title" placeholder="Title of the link" required>
  7. </div>
  8. </div>
  9. <div class="form-group">
  10. <label for="excerpt" class="col-sm-2 control-label">Excerpt</label>
  11. <div class="col-sm-10">
  12. <textarea class="form-control" id="excerpt" name="excerpt" placeholder="Short description of the link" required></textarea>
  13. </div>
  14. </div>
  15. <div class="form-group">
  16. <label for="url" class="col-sm-2 control-label">Url</label>
  17. <div class="col-sm-10">
  18. <input type="url" class="form-control" id="url" name="url" placeholder="Url of the link" required>
  19. </div>
  20. </div>
  21. <div class="form-group">
  22. <label for="tags" class="col-sm-2 control-label">Tags</label>
  23. <div class="col-sm-10">
  24. <textarea id="tags" class="form-control" name="tags" placeholder="Comma seperated list of tags" rows="3" required></textarea>
  25. </div>
  26. </div>
  27. <div class="form-group">
  28. <label for="fullname" class="col-sm-2 control-label">Full Name</label>
  29. <div class="col-sm-10">
  30. <input type="text" class="form-control" id="fullname" name="fullname" placeholder="Enter your Full Name like Shekhar Gulati" required>
  31. </div>
  32. </div>
  33. <div class="form-group">
  34. <div class="col-sm-offset-2 col-sm-10">
  35. <button type="submit" class="btn btn-success" {{action 'save'}}>Submit Story</button>
  36. </div>
  37. </div>
  38. </form>
  39. </script>

访问#/story/new即可查看表单:

接着我们在导航条中添加一个链接,这样访问报道提交表单就很容易。替换一下nav元素:

  1. <nav class="navbar navbar-default navbar-fixed-top navbar-inverse" role="navigation">
  2. <div class="container">
  3. <div class="navbar-header">
  4. <a class="navbar-brand" href="#">GetBookMarks</a>
  5. </div>
  6. <ul class="nav navbar-nav pull-right">
  7. <li>{{#link-to 'newstory'}}<span class="glyphicon glyphicon-plus"></span> Submit Story{{/link-to}}</li>
  8. </ul>
  9. </div>
  10. </nav>

注意上面我们用{{#link-to}}创建了一个指向路由的链接。请参阅文档了解详情。

表单已经有了,接下来要添加HTML 5本地存储的功能。为了添加本地存储支持,我们需要首先下载Ember DataLocal Storage Adapter JavaScript文件。将这些文件放在js/libs下。接着,在index.html中添加这些script标签。

  1. <script src="js/libs/jquery-1.9.1.js"></script>
  2. <script src="js/libs/handlebars-1.0.0.js"></script>
  3. <script src="js/libs/ember-1.1.2.js"></script>
  4. <script src="js/libs/ember-data.js"></script>
  5. <script src="js/libs/localstorage_adapter.js"></script>
  6. <script src="js/app.js"></script>

如前所述,Ember Data是一个客户端的ORM实现,它使在底层存储进行CRUD操作很容易。这里我们将使用LSAdapter。在app.js中加入:

  1. App.ApplicationAdapter = DS.LSAdapter.extend({
  2. namespace: 'stories'
  3. });

接着是定义model。一篇报道需要有url、title(标题)、fullname(提交报道的用户的全名)、excerpt(摘要),以及SubmittedOn(日期)信息。在下面的模型中,我们使用了字符串和日期类型。适配器默认支持的属性类型为字符串、数字、布尔值和日期。

  1. App.Story = DS.Model.extend({
  2. url : DS.attr('string'),
  3. tags : DS.attr('string'),
  4. fullname : DS.attr('string'),
  5. title : DS.attr('string'),
  6. excerpt : DS.attr('string'),
  7. submittedOn : DS.attr('date')
  8. });

接着我们编写NewstoryController来持久化内容:

  1. App.NewstoryController = Ember.ObjectController.extend({
  2. actions :{
  3. save : function(){
  4. var url = $('#url').val();
  5. var tags = $('#tags').val();
  6. var fullname = $('#fullname').val();
  7. var title = $('#title').val();
  8. var excerpt = $('#excerpt').val();
  9. var submittedOn = new Date();
  10. var store = this.get('store');
  11. var story = store.createRecord('story',{
  12. url : url,
  13. tags : tags,
  14. fullname : fullname,
  15. title : title,
  16. excerpt : excerpt,
  17. submittedOn : submittedOn
  18. });
  19. story.save();
  20. this.transitionToRoute('index');
  21. }
  22. }
  23. });

以上代码展示了如何从获取表单中的值,然后使用store API在内存中创建记录。为了在localstorage中存储记录,我们需要调用Story对象的save方法。最后,我们将用户重定向到index路由。

接着我们测试下这个应用,创建一个新的报道,接着打开Chrome开发者工具,在资源区域你可以查看这则报道。

第六步 显示所有报道

接着我们要做的是,当用户访问首页的时候,展示所有报道。

正如我之前提到的,路由负责询问model。我们将加上IndexRoute,它会找出本地存储中保存的所有报道。

  1. App.IndexRoute = Ember.Route.extend({
  2. model : function(){
  3. var stories = this.get('store').findAll('story');
  4. return stories;
  5. }
  6. });

每个路由支持一个模板。IndexRoute支持index模板,因此我们需要修改index.html

  1. <script type="text/x-handlebars" id="index">
  2. <div class="row">
  3. <div class="col-md-4">
  4. <table class='table'>
  5. <thead>
  6. <tr><th>Recent Stories</th></tr>
  7. </thead>
  8. {{#each controller}}
  9. <tr><td>
  10. {{title}}
  11. </td></tr>
  12. {{/each}}
  13. </table>
  14. </div>
  15. <div class="col-md-8">
  16. {{outlet}}
  17. </div>
  18. </div>
  19. </script>

现在访问/,我们会看到一个报道的列表:

还有一个问题,报道没有按照时间顺序排列。我们将创建一个IndexController负责排序。我们指定依照submittedOn属性倒序排列,以确保新的报道出现在上面。

  1. App.IndexController = Ember.ArrayController.extend({
  2. sortProperties : ['submittedOn'],
  3. sortAscending : false
  4. });

修改之后,我们会看到按照submittedOn属性排序的报道。

第七步 查看单独的报道

最后要实现的功能是:用户点击某则报道的时候会看到详细信息。我们加一个路由:

  1. App.Router.map(function() {
  2. this.resource('index',{path : '/'},function(){
  3. this.resource('story', { path:'/stories/:story_id' });
  4. });
  5. this.resource('newstory' , {path : 'story/new'});
  6. });

以上的代码展示了如何嵌套路由。

:story_id部分叫做动态字段,因为相应的报道 id会被注入URL。

然后我们添加根据报道id获取报道的StoryRoute。

  1. App.StoryRoute = Ember.Route.extend({
  2. model : function(params){
  3. var store = this.get('store');
  4. return store.find('story',params.story_id);
  5. }
  6. });

最后,我们更新下index.html,给每个报道添加链接:

  1. <script type="text/x-handlebars" id="index">
  2. <div class="row">
  3. <div class="col-md-4">
  4. <table class='table'>
  5. <thead>
  6. <tr><th>Recent Stories</th></tr>
  7. </thead>
  8. {{#each controller}}
  9. <tr><td>
  10. {{#link-to 'story' this}}
  11. {{title}}
  12. {{/link-to}}
  13. </td></tr>
  14. {{/each}}
  15. </table>
  16. </div>
  17. <div class="col-md-8">
  18. {{outlet}}
  19. </div>
  20. </div>
  21. </script>
  22. <script type="text/x-handlebars" id="story">
  23. <h1>{{title}}</h1>
  24. <h2> by {{fullname}} <small class="muted">{{submittedOn}}</small></h2>
  25. {{#each tagnames}}
  26. <span class="label label-primary">{{this}}</span>
  27. {{/each}}
  28. <hr>
  29. <p class="lead">
  30. {{excerpt}}
  31. </p>
  32. </script>

修改完毕地后,可以在浏览器中直接看到结果。

第八步 为submittedOn日期添加格式

Ember下有辅助函数的概念。所有Handlebars模板都可以调用辅助函数。

我们将使用moment.js库为日期添加格式。将以下代码加入index.html。

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

接着我们将定义我们的第一个辅助函数,该函数将日期转为人类可读的形式:

  1. Ember.Handlebars.helper('format-date', function(date){
  2. return moment(date).fromNow();
  3. });

最后我们在报道模板中加入format-data辅助函数。

  1. <script type="text/x-handlebars" id="story">
  2. <h1>{{title}}</h1>
  3. <h2> by {{fullname}} <small class="muted">{{format-date submittedOn}}</small></h2>
  4. {{#each tagnames}}
  5. <span class="label label-primary">{{this}}</span>
  6. {{/each}}
  7. <hr>
  8. <p class="lead">
  9. {{excerpt}}
  10. </p>
  11. </script>

报道页面的效果如下:

今天就到这里了。持续反馈。


原文 Day 19: Ember--The Missing EmberJS Tutorial

翻译 SegmentFault

本文转载于猿2048:Day 19: EmberJS 入门指南

Day 19: EmberJS 入门指南的更多相关文章

  1. AngularJS快速入门指南19:示例代码

    本文给出的大部分示例都可以直接运行,通过点击运行按钮来查看结果,同时支持在线编辑代码. <div ng-app=""> <p>Name: <input ...

  2. AngularJS快速入门指南20:快速参考

    thead>tr>th, table.reference>tbody>tr>th, table.reference>tfoot>tr>th, table ...

  3. AngularJS快速入门指南18:Application

    是时候创建一个真正的AngularJS单页面应用程序了(SPA). 一个AngularJS应用程序示例 你已经了解了足够多的内容来创建第一个AngularJS应用程序: My Note Save Cl ...

  4. AngularJS快速入门指南03:表达式

    AngularJS通过表达式将数据绑定到HTML. AngularJS表达式 AngularJS表达式写在双大括号中:{{ 表达式语句 }}. AngularJS表达式绑定数据到HTML的方式与ng- ...

  5. AngularJS快速入门指南01:导言

    AngularJS使用新的attributes扩展了HTML AngularJS对单页面应用的支持非常好(SPAs) AngularJS非常容易学习 现在就开始学习AngularJS吧! 关于本指南 ...

  6. Web API入门指南(安全)转

    安全检测的工具站点:https://www.owasp.org/index.php/Category:Vulnerability_Scanning_Tools Web API入门指南有些朋友回复问了些 ...

  7. 《Gulp 入门指南》 : 使用 gulp 压缩 JS

    <Gulp 入门指南> : 使用 gulp 压缩 JS 请务必理解如下章节后阅读此章节: 安装 Node 和 gulp 访问论坛获取帮助 压缩 js 代码可降低 js 文件大小,提高页面打 ...

  8. Webstorm10.0.3破解程序及汉化包下载、Webstorm配置入门指南

    核心提示: WebStorm 是jetbrains公司旗下一款JavaScript 开发工具.被广大中国JS开发者誉为“Web前端开发神器”.“最强大的HTML5编辑器”.“最智能的JavaSscri ...

  9. Web API 入门指南

    Web API 入门指南 - 闲话安全2013-09-21 18:56 by 微软互联网开发支持, 231 阅读, 3 评论, 收藏, 编辑 Web API入门指南有些朋友回复问了些安全方面的问题,安 ...

随机推荐

  1. WIN10:IE浏览器的默认主页以及通过链接搜索的默认引擎

    主页设置: 地址栏搜索引擎:

  2. MM32F0020 UART1中断接收

    目录: 1.MM32F0020简介 2.初始化MM32F0020 UART1和NVIC中断 3.编写MM32F0020 UART1中断接收函数 4.编写MM32F0020 UART1发送字节和ASCI ...

  3. js扒代码技巧(一)

    1.确定找到自己想要的代码 2.方法内部的代码需要执行后才能调用 导出方法: //案列1 //案例1 // 函数里面的方法被赋值成变量 // 解: //将函数在方法外导出到全局变量 var hex_m ...

  4. postman-接口测试常用test模块

    一.配置环境变量区分不同运行环境(开发.测试.生产等). 对接口进行测试时,不同环境往往对应不同的域名或IP,在Postman里一个接口域名相同但因为地址不同重复写多次很明显是愚蠢的做法,下面我们可以 ...

  5. Linux 磁盘inode字节数占满的问题

    查看ext系列文件系统的信息 #dumpe2fs /dev/sdc1 Inode count: 65536 inode号数量 Block count: 262144 块数量 Reserved bloc ...

  6. NTFS权限概述

    NTFS权限概述 NTFS是我常见的一种磁盘格式,在Windows系统中使用广泛,它打破了FAT的局限性.在我使用ntfs格式分区的时候经常会涉及到ntfs权限设置问题,来帮助我们对文件的处理.那么什 ...

  7. 结合AngularJS实现拖拽

    最近项目中要实现,左侧树向右侧树中元素的拖拽功能,开始在网上看了好多ng-drag等等操作,都没有实现预想的效果,偶然发现一篇博客,然后根据博客改编,实现了自己想要的效果.下面简单的分析一下实现过程. ...

  8. boxcox1p归一化+pipeline+StackingCVRegressor

    找到最好的那个参数lmbda. from mlxtend.regressor import StackingCVRegressor from sklearn.datasets import load_ ...

  9. 1354:括弧匹配检验ybt

    [题目描述]假设表达式中允许包含两种括号:圆括号和方括号,其嵌套的顺序随意,如([ ]())或[([ ][ ])]等为正确的匹配,[( ])或([ ]( )或 ( ( ) ) )均为错误的匹配. 现在 ...

  10. async-validator 源码学习笔记(三):rule

    系列文章: 1.async-validator 源码学习(一):文档翻译 2.async-validator 源码学习笔记(二):目录结构 rule 主要实现的是校验规则,文件结构为下图: 一.rul ...