构建您的第一个 Ember.js 应用程序的指南

已发表: 2022-03-11

随着现代 Web 应用程序在客户端做的越来越多(我们现在将它们称为“Web 应用程序”而不是“网站”这一事实本身就很能说明问题),对客户端框架的兴趣越来越大. 这个领域有很多参与者,但对于具有大量功能和许多移动部件的应用程序,其中两个特别突出:Angular.js 和 Ember.js。

我们已经发布了一个全面的 [Angular.js 教程][https://www.toptal.com/angular-js/a-step-by-step-guide-to-your-first-angularjs-app],所以我们'在这篇文章中,我们将重点关注 Ember.js,我们将在其中构建一个简单的 Ember 应用程序来对您的音乐收藏进行分类。 您将了解该框架的主要构建块,并了解其设计原则。 如果您想在阅读时查看源代码,可以在 Github 上以摇滚乐的形式获得。

我们要建造什么?

这是我们的摇滚应用程序在其最终版本中的样子:

带有 ember.js 的应用程序的最终版本

在左侧,您会看到我们有一个艺术家列表,在右侧,是所选艺术家的歌曲列表(您还可以看到我对音乐很有品味,但我离题了)。 只需在文本框中键入并按下相邻的按钮,即可添加新的艺术家和歌曲。 每首歌曲旁边的星星用于对其进行评分,例如 iTunes。

我们可以将应用程序的基本功能分解为以下步骤:

  1. 单击“添加”将新艺术家添加到列表中,其名称由“新艺术家”字段指定(给定艺术家的歌曲也是如此)。
  2. 清空“新艺术家”字段会禁用“添加”按钮(对于给定艺术家的歌曲也是如此)。
  3. 单击艺术家的姓名会在右侧列出他们的歌曲。
  4. 单击星星对给定歌曲进行评分。

我们还有很长的路要走,所以让我们开始吧。

路由:Ember.js 应用程序的关键

Ember 的显着特征之一是它对 URL 的高度重视。 在许多其他框架中,为单独的屏幕提供单独的 URL 要么是缺乏的,要么是事后才添加的。 在 Ember 中,路由器(管理 url 和它们之间的转换的组件)是协调构建块之间工作的核心部分。 因此,它也是理解 Ember 应用程序内部工作原理的关键。

以下是我们申请的路线:

 App.Router.map(function() { this.resource('artists', function() { this.route('songs', { path: ':slug' }); }); });

我们定义了一个资源路由、 artists和嵌套在其中的songs路由。 该定义将为我们提供以下路线:

路线

我使用了很棒的 Ember Inspector 插件(Chrome 和 Firefox 都有它)以一种易于阅读的方式向您展示生成的路由。 以下是 Ember 路由的基本规则,您可以借助上表验证我们的特定情况:

  1. 有一个隐含的application路线。

    这会为所有请求(转换)激活。

  2. 有一个隐式index路由。

    当用户导航到应用程序的根目录时,将输入此内容。

  3. 每个资源路由都会创建一个同名的路由,并在其下隐式创建一个索引路由。

    当用户导航到该路线时,此索引路线将被激活。 在我们的例子中, artists.index在用户导航到/artists时被触发。

  4. 一个简单的(非资源)嵌套路由将其父路由名称作为其前缀。

    我们定义为this.route('songs', ...)的路线将以artists.songs作为其名称。 当用户导航到/artists/pearl-jam/artists/radiohead时,它会被触发。

  5. 如果没有给出路径,则假定它等于路由名称。

  6. 如果路径包含: ,则将其视为动态段

    分配给它的名称(在我们的例子中是slug )将匹配 url 适当段中的值。 上面的slug段将具有值 Pearl pearl-jamradiohead或从 URL 中提取的任何其他值。

显示艺术家列表

作为第一步,我们将构建在左侧显示艺术家列表的屏幕。 当用户导航到/artists/时,应向用户显示此屏幕:

艺术家

要了解该屏幕是如何呈现的,是时候介绍另一个重要的 Ember 设计原则:约定优于配置。 在上一节中,我们看到/artists激活了artists路线。 按照惯例,该路由对象的名称是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属性是artists ,这意味着它将为外部路线artists渲染。 它为右侧面板的内容指定了一个出口,内部路由artists.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>

总之,一条路线( artists )在左侧边栏中呈现其内容,其模型是艺术家列表。 另一条路线, artists.index将自己的内容渲染到artists模板提供的插槽中。 它可以获取一些数据作为模型,但在这种情况下,我们想要显示的只是静态文本,所以我们不需要。

相关: 8 个重要的 Ember.js 面试问题

创建艺术家

第 1 部分:数据绑定

接下来,我们希望能够创建艺术家,而不是仅仅看一个无聊的列表。

当我展示呈现艺术家列表的artists模板时,我有点作弊。 我剪掉了顶部以专注于重要的事情。 现在,我将其添加回来:

 <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 帮助input来呈现简单的文本输入。 在其中,我们将文本输入的值绑定到支持此模板的控制器ArtistsControllernewName属性。 因此,当输入的 value 属性发生变化时(换句话说,当用户在其中键入文本时)控制器上的newName属性将保持同步。

我们还让我们知道单击按钮时应该触发createArtist操作。 最后,我们将按钮的disabled属性绑定到控制器的 disabled 属性。 那么控制器长什么样子呢?

 App.ArtistsController = Ember.ArrayController.extend({ newName: '', disabled: function() { return Ember.isEmpty(this.get('newName')); }.property('newName') });

newName在开始时设置为空,这意味着文本输入将为空白。 (还记得我所说的绑定吗?尝试更改newName并看到它反映为输入字段中的文本。)

disabled是这样实现的,当输入框中没有文本时,它将返回true ,因此按钮将被禁用。 最后的.property调用使其成为“计算属性”,是 Ember 蛋糕的另一块美味。

计算属性是依赖于其他属性的属性,这些属性本身可以是“正常的”或计算的。 Ember 会缓存这些值,直到其中一个依赖属性发生更改。 然后它重新计算计算属性的值并再次缓存它。

这是上述过程的可视化表示。 总结一下:当用户输入艺术家的名字时, newName属性会更新,然后是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'); } }); } } });

动作处理程序需要包装在actions对象中,并且可以在路由、控制器或视图上定义。 我在这里选择在路由上定义它,因为动作的结果并不局限于控制器,而是“全局”的。

这里没有什么花哨的。 后端通知我们保存操作成功完成后,我们依次做三件事:

  1. 将新艺术家添加到模板的模型(所有艺术家),以便重新渲染并且新艺术家显示为列表的最后一项。
  2. 通过newName绑定清除输入字段,使我们不必直接操作 DOM。
  3. 过渡到新路线 ( artists.songs ),传入新创建的艺术家作为该路线的模型。 transitionTo是在内部路由之间移动的方式。 ( link-to帮助器用于通过用户操作来做到这一点。)

为艺术家显示歌曲

我们可以通过单击艺术家的名字来显示艺术家的歌曲。 我们还传递了即将成为新路线模特的艺术家。 如果这样传入模型对象,则不会调用路由的model钩子,因为不需要解析模型。

这里的活动路线是artists.songs ,因此控制器和模板将分别是ArtistsSongsController和 Artists artists/songs 。 我们已经看到了模板是如何渲染到artists模板提供的插座中的,因此我们可以只关注手头的模板:

 <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 构建块、组件形成对比,组件是隔离的、可重用的控件,只能访问已传递给它们的内容。 (在这个例子中,我们也可以使用星级组件。)

让我们看看当用户点击其中一颗星星时视图如何显示星星的数量并设置歌曲的评分:

 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; }, (...) });

ratingfullStarsnumStars是计算属性,我们之前讨论过ArtistsControllerdisabled属性。 上面,我使用了一个所谓的计算属性宏,其中大约有十几个是在 Ember 中定义的。 它们使典型的计算属性更简洁,更不容易出错(编写)。 我将rating设置为上下文(以及歌曲)的评分,同时我定义了fullStarsnumStars属性,以便它们在星级小部件的上下文中更好地阅读。

stars方法是主要的吸引力。 它返回一个星星的数据数组,其中每个项目都包含一个rating属性(从 1 到 5)和一个标志 ( full ) 以指示星星是否已满。 这使得在模板中遍历它们变得非常简单:

 <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>

这个片段包含几个注意点:

  1. 首先, each助手通过在属性名称前加上view来指定它使用视图属性(而不是控制器上的属性)。
  2. 其次,span标签的class属性分配了混合的动态和静态类。 任何以:为前缀的东西都成为静态类,而full:glyphicon-star:glyphicon-star-empty表示法就像 JavaScript 中的三元运算符:如果 full 属性为真,则应分配第一个类; 如果没有,第二个。
  3. 最后,当点击标签时,应该触发setRating动作——但 Ember 会在视图上查找它,而不是在路由或控制器上查找,就像创建新艺术家的情况一样。

因此,该操作在视图上定义:

 App.StarRating = Ember.View.extend({ (...) actions: { setRating: function() { var newRating = $(event.target).data('rating'); this.set('rating', newRating); } } });

我们从模板中分配的rating数据属性中获取评分,然后将其设置为歌曲的rating 。 请注意,新评级不会保留在后端。 根据我们创建艺术家的方式来实现此功能并不难,并留给有动力的读者作为练习。

把它包起来

我们品尝了上述余烬蛋糕的几种成分:

  • 我们已经看到路由如何成为 Ember 应用程序的关键,以及它们如何充当命名约定的基础。
  • 我们已经看到双向数据绑定和计算属性如何使我们的模型数据成为唯一的事实来源,并允许我们避免直接的 DOM 操作。
  • 我们已经了解了如何以多种方式触发和处理操作,并构建自定义视图来创建属于我们的 HTML 的控件。

漂亮,不是吗?

进一步阅读(和观看)

Ember 的内容远不止我在这篇文章中所能描述的。 如果您想观看有关我如何构建上述应用程序的更高级版本的截屏视频系列和/或了解有关 Ember 的更多信息,您可以注册我的邮件列表以每周获取文章或提示。

我希望我已经激起了你更多地了解 Ember.js 的兴趣,并且你已经远远超出了我在这篇文章中使用的示例应用程序。 随着您继续了解 Ember.js,请务必浏览我们关于 Ember Data 的文章以了解如何使用 ember-data 库。 玩得开心!

相关: Ember.js 和开发人员最常犯的 8 个错误