本文概述
随着现代Web应用程序在客户端上做的越来越多(事实本身, 我们现在将它们称为” Web应用程序”, 而不是”网站”, 这确实可以说明问题), 对客户端框架的兴趣日益浓厚。这个领域有很多参与者, 但是对于具有许多功能和许多活动部件的应用程序, 其中两个特别突出:Angular.js和Ember.js。
我们要建造什么?
这是我们的”摇滚”应用最终版的外观:
在左侧, 你会看到我们有一个歌手列表, 在右侧, 有一个所选歌手的歌曲列表(你也可以看到我在音乐方面很有品位, 但我离题了)。只需在文本框中键入并按下相邻按钮, 即可添加新的艺术家和歌曲。在iTunes上, 每首歌曲旁边的星号都对其评分。
我们可以将应用的基本功能分解为以下步骤:
- 点击”添加”会将新歌手添加到列表中, 并在”新歌手”字段中指定名称(给定歌手的歌曲也是如此)。
- 清空”新歌手”字段会禁用”添加”按钮(给定歌手的歌曲也是如此)。
- 单击艺术家的姓名, 将在右侧列出他们的歌曲。
- 单击星星为给定的歌曲评分。
要使它正常工作, 我们还有很长的路要走, 让我们开始吧。
路线:Ember.js应用程序的钥匙
Ember的显着特征之一是对URL的高度重视。在许多其他框架中, 缺少或为单独的屏幕使用单独的URL是作为事后考虑的。在Ember中, 路由器(管理URL和URL之间的过渡的组件)是协调构件之间工作的核心部分。因此, 这也是了解Ember应用程序内部工作的关键。
这是我们应用程序的路线:
App.Router.map(function() {
this.resource('artists', function() {
this.route('songs', { path: ':slug' });
});
});
我们定义了一条资源路线, 艺术家以及一条嵌套在其中的歌曲路线。该定义将为我们提供以下路线:
我使用了出色的Ember Inspector插件(Chrome和Firefox均存在), 以易于阅读的方式向你显示生成的路由。以下是Ember路由的基本规则, 你可以借助上表针对我们的特殊情况进行验证:
-
有一个隐式的应用程序路由。
这将为所有请求(转换)激活。
-
有一个隐式索引路由。
当用户导航到应用程序的根目录时, 将输入此内容。
-
每个资源路由都会创建一个具有相同名称的路由, 并在其下方隐式创建一个索引路由。
当用户导航到该路线时, 此索引路线将被激活。在我们的例子中, 当用户导航到/ artists时, artist.index被触发。
-
简单(非资源)嵌套路由将以其父路由名称作为前缀。
我们定义为this.route(‘songs’, …)的路线将以artist.songs为名称。当用户导航到/ artists / pearl-jam或/ artists / radiohead时, 它会被触发。
-
如果未给出路径, 则假定该路径等于路由名称。
-
如果路径包含:, 则将其视为动态段。
分配给它的名称(在我们的示例中为slug)将与url相应段中的值匹配。上面的段段的值将为Pearl-jam, radiohead或从URL中提取的任何其他值。
显示艺术家列表
首先, 我们将构建一个屏幕, 在左侧显示艺术家列表。当用户导航到/ artists /时, 应该向用户显示此屏幕:
为了了解该屏幕的呈现方式, 是时候介绍另一项Ember设计原则:约定优先于配置。在上一节中, 我们看到/ artists激活了artist路线。按照约定, 该路由对象的名称为ArtistsRoute。此路线对象负责获取数据以呈现应用。发生在路线的模型挂钩中:
App.ArtistsRoute = Ember.Route.extend({
model: function() {
var artistObjects = [];
Ember.$.getJSON('http://localhost:9393/artists', function(artists) {
artists.forEach(function(data) {
artistObjects.pushObject(App.Artist.createRecord(data));
});
});
return artistObjects;
}
});
在此代码段中, 数据是通过XHR调用从后端获取的, 并且在转换为模型对象之后, 将其推送到了一个可以随后显示的数组。但是, 路线的责任并不扩展到提供由控制器处理的显示逻辑。让我们来看看。
嗯, 事实上, 我们现在不需要定义控制器! Ember足够聪明, 可以在需要时生成控制器, 并将控制器的M.odel属性设置为模型钩子本身的返回值, 即艺术家列表。 (同样, 这是”基于配置的约定”范式的结果。)我们可以下一层, 并创建一个模板来显示列表:
<script type="text/x-handlebars" data-template-name="artists">
<div class="col-md-4">
<div class="list-group">
{{#each model}}
{{#link-to "artists.songs" this class="list-group-item artist-link"}}
{{name}}
<span class="pointer glyphicon glyphicon-chevron-right"></span>
{{/link-to}}
{{/each}}
</div>
</div>
<div class="col-md-8">
<div class="list-group">
{{outlet}}
</div>
</div>
</script>
如果看起来很熟悉, 那是因为Ember.js使用了Handlebars模板, 该模板具有非常简单的语法和帮助程序, 但不允许使用非平凡的逻辑(例如, 条件中的ORing或ANDing术语)。
在上面的模板中, 我们遍历模型(之前通过路由设置到包含所有艺术家的数组的路径), 并为其中的每个项目渲染一个链接, 将我们带到该艺术家的artists.songs路线。链接包含艺术家名称。 #Handlebars中的#each帮助器将其中的范围更改为当前项目, 因此{{name}}将始终引用当前正在迭代的艺术家的名称。
嵌套路线以获取嵌套视图
上面的代码片段中另一个有趣的地方是{{outlet}}, 它指定了模板中可以呈现内容的位置。嵌套路线时, 首先呈现外部资源路线的模板, 然后是内部路线, 内部路线将其模板内容呈现到外部路线定义的{{outlet}}中。这正是这里发生的情况。
按照惯例, 所有路由都会将其内容呈现到相同名称的模板中。上方, 上述模板的data-template-name属性是artist, 这意味着它将为外部路线Artist渲染。它为右面板的内容指定一个出口, 内部路线artist.index将其内容呈现到其中:
<script type="text/x-handlebars" data-template-name="artists/index">
<div class="list-group-item empty-list">
<div class="empty-message">
Select an artist.
</div>
</div>
</script>
总而言之, 一条路线(艺术家)在左侧栏中渲染其内容, 其模型为艺术家列表。另一种方法是artist.index将其自身的内容呈现到artist模板提供的插槽中。它可以获取一些数据作为其模型, 但是在这种情况下, 我们要显示的只是静态文本, 因此我们不需要这样做。
相关:8个基本的Ember.js面试问题
创作艺术家
第1部分:数据绑定
接下来, 我们希望能够创建艺术家, 而不仅仅是看一个无聊的列表。
当我显示用于渲染艺术家列表的艺术家模板时, 我有点作弊。我剪掉了顶部, 重点放在重要的部分上。现在, 我将其添加回去:
<script type="text/x-handlebars" data-template-name="artists">
<div class="col-md-4">
<div class="list-group">
<div class="list-group-item">
{{input type="text" class="new-artist" placeholder="New Artist" value=newName}}
<button class="btn btn-primary btn-sm new-artist-button" {{action "createArtist"}}
{{bind-attr disabled=disabled}}>Add</button>
</div>
< this is where the list of artists is rendered >
...
</script>
我们使用Ember辅助输入(带有文本类型)来呈现简单的文本输入。在其中, 我们将文本输入的值绑定到备份此模板ArtistsController的控制器的newName属性。结果, 当输入的value属性更改时(换句话说, 当用户在其中输入文本时), 控制器上的newName属性将保持同步。
我们还知道单击按钮时应触发createArtist动作。最后, 我们将按钮的disabled属性绑定到控制器的disabled属性。那么控制器是什么样的呢?
App.ArtistsController = Ember.ArrayController.extend({
newName: '', disabled: function() {
return Ember.isEmpty(this.get('newName'));
}.property('newName')
});
newName在开始时设置为空, 这意味着文本输入将为空白。 (还记得我讲过的有关绑定的内容吗?尝试更改newName并看到它反映为输入字段中的文本。)
实现了disable选项, 以便当输入框中没有文本时, 它将返回true, 因此该按钮将被禁用。最后的.property调用使它成为”计算所得的属性”, 这是Ember蛋糕的另一个美味片段。
计算属性是依赖于其他属性的属性, 这些属性本身可以是”正常”或计算得出的。 Ember缓存这些值, 直到其中一个依赖属性更改。然后, 它重新计算计算出的属性的值, 并再次对其进行缓存。
这是上述过程的直观表示。总结一下:当用户输入艺术家的姓名时, newName属性将更新, 然后是Disabled属性, 最后, 艺术家的姓名将添加到列表中。
绕道:真理的单一来源
考虑一下。借助绑定和计算的属性, 我们可以将数据建立(模型)为真实的唯一来源。上方, 新艺术家名称的更改触发了controller属性的更改, 而后者又触发了Disabled属性的更改。当用户开始键入新艺术家的名称时, 该按钮将变为启用状态, 就好像是在魔术一样。
系统越大, 我们从”单一真相”原则中获得的杠杆作用就越大。它使我们的代码干净, 健壮, 并使我们的属性定义更具声明性。
其他一些框架也强调将模型数据作为事实的唯一来源, 但是要么没有达到Ember的水平, 要么没有做到如此彻底的工作。例如, Angular具有双向绑定, 但没有计算属性。它可以通过简单的函数”模拟”计算出的属性。这里的问题是, 它无法知道何时刷新”计算属性”, 因此只能进行脏检查, 从而导致性能下降, 尤其是在较大的应用程序中尤其如此。
如果你想了解更多有关该主题的信息, 建议你阅读eviltrout的博客文章以获取更短的版本, 或者阅读Quora问题以进行更长的讨论, 双方的核心开发人员都应参与其中。
第2部分:动作处理程序
让我们回过头来看一下createArtist动作在触发后如何创建(按下按钮之后):
App.ArtistsRoute = Ember.Route.extend({
...
actions: {
createArtist: function() {
var name = this.get('controller').get('newName');
Ember.$.ajax('http://localhost:9393/artists', {
type: 'POST', dataType: 'json', data: { name: name }, context: this, success: function(data) {
var artist = App.Artist.createRecord(data);
this.modelFor('artists').pushObject(artist);
this.get('controller').set('newName', '');
this.transitionTo('artists.songs', artist);
}, error: function() {
alert('Failed to save artist');
}
});
}
}
});
动作处理程序需要包装在动作对象中, 并且可以在路线, 控制器或视图上定义。我选择在此处的路由上定义它, 因为操作的结果并不局限于控制器, 而是”全局”的。
这里没有任何幻想。在后端通知我们保存操作成功完成之后, 我们按顺序执行三件事:
- 将新艺术家添加到模板模型(所有艺术家)中, 以便重新渲染它, 新艺术家显示在列表的最后一项。
- 通过newName绑定清除输入字段, 从而使我们不必直接操作DOM。
- 过渡到新路线(艺术家, 歌曲), 并传入新创建的艺术家作为该路线的模型。 transitionTo是内部在路线之间移动的方式。 (链接到帮助程序可通过用户操作来实现。)
显示歌手的歌曲
我们可以通过单击歌手的名字来显示该歌手的歌曲。我们还传递了将成为新路线模型的艺术家。如果这样传入了模型对象, 则由于无需解析模型, 因此不会调用该路线的模型挂钩。
这里的活动路线是artist.songs, 因此控制器和模板将分别为ArtistsSongsController和artist / songs。我们已经看到了如何将模板渲染到artist模板提供的输出中, 因此我们可以只关注手头的模板:
<script type="text/x-handlebars" data-template-name="artists/songs">
(...)
{{#each songs}}
<div class="list-group-item">
{{title}}
{{view App.StarRating maxRating=5}}
</div>
{{/each}}
</script>
请注意, 我剥离了代码以创建新歌曲, 因为它与创建新歌手的歌曲完全相同。
根据服务器返回的数据, 在所有艺术家对象中设置了songs属性。完成此操作的确切机制对当前的讨论兴趣不大。现在, 我们足以知道每首歌曲都有标题和等级。
标题直接显示在模板中, 而评分通过StarRating视图由星星表示。让我们现在来看。
星级评分小部件
歌曲的评分介于1到5之间, 并通过App.StarRating视图显示给用户。视图可以访问其上下文(在本例中为歌曲)及其控制器。这意味着他们可以读取和修改其属性。这与另一个Ember构建块相反, 这些Ember构件是隔离的, 可重用的控件, 它们只能访问传递给它们的内容。 (在此示例中, 我们也可以使用星级评分组件。)
让我们看看当用户点击其中一颗星时, 视图如何显示星数并设置歌曲的等级:
App.StarRating = Ember.View.extend({
classNames: ['rating-panel'], templateName: 'star-rating', rating: Ember.computed.alias('context.rating'), fullStars: Ember.computed.alias('rating'), numStars: Ember.computed.alias('maxRating'), stars: function() {
var ratings = [];
var fullStars = this.starRange(1, this.get('fullStars'), 'full');
var emptyStars = this.starRange(this.get('fullStars') + 1, this.get('numStars'), 'empty');
Array.prototype.push.apply(ratings, fullStars);
Array.prototype.push.apply(ratings, emptyStars);
return ratings;
}.property('fullStars', 'numStars'), starRange: function(start, end, type) {
var starsData = [];
for (i = start; i <= end; i++) {
starsData.push({ rating: i, full: type === 'full' });
};
return starsData;
}, (...)
});
等级, fullStars和numStars是我们先前与ArtistsController的disabled属性讨论的计算属性。上面, 我使用了所谓的计算属性宏, 在Ember中定义了大约十二个宏。它们使典型的计算属性更简洁, 更不易出错(写)。我将等级设置为上下文(进而是歌曲)的等级, 同时定义了fullStars和numStars属性, 以便它们在”星级”小部件的上下文中更好地阅读。
星星法是主要吸引力。它返回一个恒星数据数组, 其中每个项目都包含一个等级属性(从1到5)和一个标志(满), 以指示该星星是否满。这使得在模板中遍历它们非常简单:
<script type="text/x-handlebars" data-template-name="star-rating">
{{#each view.stars}}
<span {{bind-attr data-rating=rating}}
{{bind-attr class=":star-rating :glyphicon full:glyphicon-star:glyphicon-star-empty"}}
{{action "setRating" target=view}}>
</span>
{{/each}}
</script>
此代码段包含几点注意事项:
- 首先, 每个帮助程序通过在属性名称前面加上view来指定其使用view属性(而不是控制器上的属性)。
- 其次, span标记的class属性分配了混合的动态和静态类。任何以:开头的内容都将成为静态类, 而full:glyphicon-star:glyphicon-star-empty表示法类似于JavaScript中的三元运算符:如果full属性为true, 则应分配第一个类;如果没有, 第二。
- 最后, 单击标签后, 应该触发setRating动作-但是Ember会在视图上查找它, 而不是在路由或控制器上进行查找, 就像创建新艺术家一样。
因此, 在视图上定义了动作:
App.StarRating = Ember.View.extend({
(...)
actions: {
setRating: function() {
var newRating = $(event.target).data('rating');
this.set('rating', newRating);
}
}
});
我们从模板中分配的评级数据属性中获得评级, 然后将其设置为歌曲的评级。请注意, 新评分不会在后端持续存在。根据我们如何创建艺术家并将其作为有动机的读者的练习来实施此功能并不困难。
包装全部
我们品尝了上述灰烬蛋糕的几种成分:
- 我们已经看到路由如何成为Ember应用程序的症结所在, 以及它们如何用作命名约定的基础。
- 我们已经看到双向数据绑定和计算属性如何使我们的模型数据成为事实的单一来源, 并允许我们避免直接的DOM操作。
- 我们已经看到了如何以多种方式触发和处理动作, 以及如何构建自定义视图来创建不属于HTML的控件。
美丽, 不是吗?
进一步阅读(和观看)
对于Ember而言, 除了我仅能胜任本职工作之外, 还拥有更多。如果你想观看有关我如何构建上述应用程序的更高级版本的截屏视频, 和/或了解有关Ember的更多信息, 则可以注册我的邮件列表, 每周获取文章或提示。
希望我能激发你的兴趣, 以了解有关Ember.js的更多信息, 并且你已经超越了本文中使用的示例应用程序。在继续学习Ember.js时, 请务必浏览一下我们关于Ember Data的文章, 以了解如何使用ember数据库。玩得开心!
相关:Ember.js和开发人员最常犯的8个错误
评论前必须登录!
注册