構建您的第一個 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 個錯誤