- Day 1: Bower - 管理你的客户端依赖关系
- Day 2: AngularJS - 对 AngularJS 的初步认识
- Day 3: Flask - 使用 Python 和 OpenShift 进行即时 Web 开发
- Day 4:PredictionIO - 如何创建一个博客推荐器
- Day 5: GruntJS - 重复乏味的工作总会有人做(反正我不做)
- Day 6:在 Java 虚拟机上使用 Grails 进行快速 Web 开发
- Day 7: GruntJS 在线重载 提升生产率至新境界
- Day 8: Harp.JS - 现代静态 web 服务器
- Day 9: TextBlob - 对文本进行情感分析
- Day 10: PhoneGap - 开发手机应用如此简单
- Day 11: AeroGear 推送服务器:使应用的通知推送变得简单
- Day 12: OpenCV - Java 开发者的人脸检测
- Day 13: Dropwizard - 非常棒的 Java REST 服务器栈
- Day14:使用斯坦福 NER 软件包实现你自己的命名实体识别器(Named Entity Recognition,NER)
- Day 15:Meteor - 从零开始创建一个 Web 应用
- Day 16: Goose Extractor - 好用的文章提取工具
- Day 17: 使用 JBoss Forge 和 OpenShift 构建部署 JAVA EE 6 应用
- Day 18: BoilerPipe - Java 开发者的文章提取工具
- Day 19: EmberJS 入门指南
- Day 20: 斯坦福 CoreNLP - 用 Java 给 Twitter 进行情感分析
- Day 21:Docker 入门教程
- Day 22: 使用 Spring、MongoDB 和 AngularJS 开发单页面应用
- Day 23:使用 TimelineJS 构建精美的时间轴
- Day 24: 使用 Yeoman 自动构建 Ember 项目
- Day 25: 联合 Tornado、MongoDB 和 AngularJS 进行应用开发
- Day 26: TogetherJS - 让我们一起来编程!
- Day 27: Restify - 在 Node.js 中构建正确的 REST Web 服务
- Day 28: OpenShift 的 Eclipse 集成
- Day 29:编写你的第一个 Google Chrome 扩展程序
- Day 30: Play Framework - Java 开发者的梦想框架
Day 19: EmberJS 入门指南
到目前为止,我们这一系列文章涉及了 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>
报道页面的效果如下:
今天就到这里了。持续反馈。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论