Ember.js 開發人員最常犯的 8 個錯誤
已發表: 2022-03-11Ember.js 是一個用於構建複雜客戶端應用程序的綜合框架。 它的一個原則是“約定優於配置”,並堅信大多數 Web 應用程序都有很大一部分開發是通用的,因此是解決大多數這些日常挑戰的單一最佳方法。 然而,找到正確的抽象並涵蓋所有案例需要時間和整個社區的投入。 照理說,最好花時間找到正確的核心問題的解決方案,然後將其融入框架中,而不是在需要找到解決方案時舉起手來,讓每個人都自生自滅。
Ember.js 不斷發展以使開發更加容易。 但是,與任何高級框架一樣,Ember 開發人員仍可能陷入陷阱。 通過下面的帖子,我希望提供一個地圖來規避這些。 讓我們直接跳進去!
常見錯誤一:期望模型鉤子在所有上下文對像都被傳入時觸發
假設我們的應用程序中有以下路由:
Router.map(function() { this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
band
路由有一個動態段id
。 當應用程序使用/bands/24
之類的 URL 加載時, 24
被傳遞到相應路由的model
鉤子band
。 模型掛鉤的作用是反序列化段以創建一個對象(或對像數組),然後可以在模板中使用該對象:
// app/routes/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); // params.id is '24' } });
到目前為止,一切都很好。 但是,除了從瀏覽器的導航欄加載應用程序之外,還有其他方法可以輸入路由。 其中之一是使用來自模板的link-to
。 下面的代碼片段遍歷了一個波段列表,並創建了一個指向它們各自band
路徑的鏈接:
{{#each bands as |band|}} {{link-to band.name "band" band}} {{/each}}
link-to 的最後一個參數band
是一個填充路由動態段的對象,因此它的id
成為路由的 id 段。 許多人陷入的陷阱是在這種情況下不調用模型鉤子,因為模型是已知的並且已經被傳入。這是有道理的,它可能會將請求保存到服務器,但不可否認的是,它不是直覺的。 一個巧妙的方法是傳入,不是對象本身,而是它的 id:
{{#each bands as |band|}} {{link-to band.name "band" band.id}} {{/each}}
Ember 的緩解計劃
可路由組件將很快出現在 Ember 中,可能在 2.1 或 2.2 版本中。 當它們著陸時,模型鉤子將始終被調用,無論如何過渡到具有動態路段的路線。 在此處閱讀相應的 RFC。
常見錯誤 2:忘記路由驅動的控制器是單例的
Ember.js 中的路由在控制器上設置屬性,作為相應模板的上下文。 這些控制器是單例的,因此即使控制器不再處於活動狀態,它們上定義的任何狀態也會持續存在。
這是很容易忽視的事情,我也偶然發現了這一點。 就我而言,我有一個包含樂隊和歌曲的音樂目錄應用程序。 songs
控制器上的songCreationStarted
標誌表明用戶已開始為特定樂隊創作歌曲。 問題是,如果用戶隨後切換到另一個樂隊, songCreationStarted
的值仍然存在,並且似乎半完成的歌曲是給另一個樂隊的,這令人困惑。
解決方案是手動重置我們不想逗留的控制器屬性。 一個可能的地方是相應路由的setupController
鉤子,它在afterModel
鉤子之後的所有轉換中被調用(顧名思義,它位於model
鉤子之後):
// app/routes/band.js export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); controller.set('songCreationStarted', false); } });
Ember 的緩解計劃
同樣,可路由組件的出現將解決這個問題,徹底終結控制器。 可路由組件的優點之一是它們具有更一致的生命週期,並且在從其路由轉換時總是被拆除。 當他們到達時,上述問題將消失。
常見錯誤 3:沒有在setupController
中調用默認實現
Ember 中的路由有一些生命週期鉤子來定義特定於應用程序的行為。 我們已經看到了用於為相應模板和setupController
獲取數據的model
,用於設置控制器,模板的上下文。
後者setupController
有一個合理的默認值,即從model
鉤子中分配模型作為控制器的model
屬性:
// ember-routing/lib/system/route.js setupController(controller, context, transition) { if (controller && (context !== undefined)) { set(controller, 'model', context); } }
( context
是ember-routing
包使用的名稱,我稱之為上面的model
)
setupController
掛鉤可以出於多種目的被覆蓋,例如重置控制器的狀態(如上面的常見錯誤 2)。 但是,如果忘記調用我在 Ember.Route 中復制的父實現,則可能會陷入長時間的頭疼會話,因為控制器不會設置其model
屬性。 所以總是調用this._super(controller, model)
:
export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); // put the custom setup here } });
Ember 的緩解計劃
如前所述,控制器以及與它們一起的setupController
鉤子很快就會消失,因此這個陷阱將不再是威脅。 然而,這裡有一個更大的教訓要學習,那就是要注意祖先的實現。 在Ember.Object
中定義的init
函數是 Ember 中所有對象的母體,是您必須注意的另一個示例。
常見錯誤 4:將this.modelFor
與非父路由一起使用
Ember 路由器在處理 URL 時解析每個路由段的模型。 假設我們的應用程序中有以下路由:
Router.map({ this.route('bands', function() { this.route('band', { path: ':id' }, function() { this.route('songs'); }); }); });
給定 URL /bands/24/songs
,按此順序調用bands
、 bands.band
和bands.band.songs
的model
鉤子。 路由 API 有一個特別方便的方法modelFor
,可以在子路由中使用它從父路由之一獲取模型,因為那個模型肯定已經解決了。
例如,以下代碼是在bands.band
路由中獲取band 對象的有效方法:
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { var bands = this.modelFor('bands'); return bands.filterBy('id', params.id); } });
然而,一個常見的錯誤是在 modelFor 中使用的路由名稱不是路由的父級。 如果上述示例中的路線略有改變:
Router.map({ this.route('bands'); this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
我們獲取 URL 中指定的 band 的方法會中斷,因為bands
路由不再是父路由,因此它的模型尚未解析。
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { var bands = this.modelFor('bands'); // `bands` is undefined return bands.filterBy('id', params.id); // => error! } });
解決方案是只對父路由使用modelFor
,當modelFor
不能使用時,使用其他方式獲取必要的數據,比如從store中獲取。
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); } });
常見錯誤 5:錯誤觸發組件操作的上下文
嵌套組件一直是 Ember 最難推理的部分之一。 隨著 Ember 1.10 中塊參數的引入,這種複雜性已經減輕了很多,但在許多情況下,仍然很難一眼看出從子組件觸發的動作將在哪個組件上觸發。
假設我們有一個band-list
組件,其中包含band-list-items
,我們可以將每個樂隊標記為列表中的收藏夾。
// app/templates/components/band-list.hbs {{#each bands as |band|}} {{band-list-item band=band faveAction="setAsFavorite"}} {{/each}}
當用戶單擊按鈕時應該調用的動作名稱被傳遞到band-list-item
組件,並成為其faveAction
屬性的值。
現在讓我們看看band-list-item
的模板和組件定義:
// app/templates/components/band-list-item.hbs <div class="band-name">{{band.name}}</div> <button class="fav-button" {{action "faveBand"}}>Fave this</button>
// app/components/band-list-item.js export default Ember.Component.extend({ band: null, faveAction: '', actions: { faveBand: { this.sendAction('faveAction', this.get('band')); } } });
當用戶點擊“Fave this”按鈕時, faveBand
動作被觸發,它觸發了在其父組件band-list
上傳入的組件的faveAction
(在上面的例子中為setAsFavorite
)。

這讓很多人感到困惑,因為他們希望在控制器上觸發動作的方式與來自路由驅動模板的動作相同(然後在活動路由上冒泡)。 更糟糕的是沒有記錄錯誤消息。 父組件只是吞下錯誤。
一般規則是在當前上下文中觸發操作。 在非組件模板的情況下,該上下文是當前控制器,而在組件模板的情況下,它是父組件(如果有的話),或者如果組件沒有嵌套,則也是當前控制器。
因此在上述情況下, band-list
組件必須重新觸發從band-list-item
接收到的操作,以便將其冒泡到控制器或路由。
// app/components/band-list.js export default Ember.Component.extend({ bands: [], favoriteAction: 'setFavoriteBand', actions: { setAsFavorite: function(band) { this.sendAction('favoriteAction', band); } } });
如果band-list
是在bands
模板中定義的,那麼setFavoriteBand
操作必須在bands
控制器或bands
路由(或其父路由之一)中處理。
Ember 的緩解計劃
您可以想像,如果有更多級別的嵌套,這會變得更加複雜(例如,通過在band-list-item
中有一個fav-button
組件)。 你必須從內部鑽一個洞穿過幾個層才能得到你的信息,在每個級別定義有意義的名稱( setAsFavorite
、 favoriteAction
、 faveAction
等)
“Improved Actions RFC”使這變得更簡單,它已經在 master 分支上可用,並且可能會包含在 1.13 中。
上面的例子將被簡化為:
// app/templates/components/band-list.hbs {{#each bands as |band|}} {{band-list-item band=band setFavBand=(action "setFavoriteBand")}} {{/each}}
// app/templates/components/band-list-item.hbs <div class="band-name">{{band.name}}</div> <button class="fav-button" {{action "setFavBand" band}}>Fave this</button>
常見錯誤 6:使用數組屬性作為依賴鍵
Ember 的計算屬性依賴於其他屬性,而這種依賴關係需要開發者明確定義。 假設我們有一個isAdmin
屬性,當且僅當角色之一是admin
時,該屬性才應該為 true。 可以這樣寫:
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles')
使用上面的定義, isAdmin
的值只有在roles
數組對象本身發生變化時才會失效,但如果項目被添加或刪除到現有數組中則不會。 有一種特殊的語法來定義添加和刪除也應該觸發重新計算:
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles.[]')
常見錯誤 7:不使用觀察者友好的方法
讓我們從常見錯誤 6 中擴展(現已修復)示例,並在我們的應用程序中創建一個 User 類。
var User = Ember.Object.extend({ initRoles: function() { var roles = this.get('roles'); if (!roles) { this.set('roles', []); } }.on('init'), isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles.[]') });
當我們將admin
角色添加到這樣的User
時,我們會感到驚訝:
var user = User.create(); user.get('isAdmin'); // => false user.get('roles').push('admin'); user.get('isAdmin'); // => false ?
問題是如果使用現有的 Javascript 方法,觀察者將不會觸發(因此計算的屬性不會得到更新)。 如果在瀏覽器中對Object.observe
的全局採用有所改進,這可能會改變,但在那之前,我們必須使用 Ember 提供的一組方法。 在當前情況下, pushObject
是觀察者友好的push
等價物:
user.get('roles').pushObject('admin'); user.get('isAdmin'); // => true, finally!
常見錯誤 8:在組件中更改傳入的屬性
想像一下,我們有一個star-rating
組件,它顯示項目的評級並允許設置項目的評級。 評分可以是歌曲、書籍或足球運動員的運球技巧。
您可以在模板中這樣使用它:
{{#each songs as |song|}} {{star-rating item=song rating=song.rating}} {{/each}}
讓我們進一步假設該組件顯示星星,每個點一個滿星,然後是空星,直到最大評級。 單擊星號時,會在控制器上觸發set
操作,並且應將其解釋為用戶想要更新評級。 我們可以編寫以下代碼來實現這一點:
// app/components/star-rating.js export default Ember.Component.extend({ item: null, rating: 0, (...) actions: { set: function(newRating) { var item = this.get('item'); item.set('rating', newRating); return item.save(); } } });
這樣可以完成工作,但存在一些問題。 首先,它假設傳入的 item 有一個rating
屬性,所以我們不能使用這個組件來管理 Leo Messi 的運球技巧(這個屬性可能被稱為score
)。
其次,它會改變組件中項目的評級。 這會導致很難看出為什麼某個屬性會發生變化的情況。 想像一下,我們在同一個模板中有另一個組件,其中也使用了該評分,例如,用於計算足球運動員的平均得分。
減輕這種情況復雜性的口號是“數據下降,行動上升”(DDAU)。 數據應該被向下傳遞(從路由到控制器到組件),而組件應該使用動作來通知它們的上下文有關這些數據的變化。 那麼這裡應該如何應用DDAU呢?
讓我們添加一個應該發送以更新評級的操作名稱:
{{#each songs as |song|}} {{star-rating item=song rating=song.rating setAction="updateRating"}} {{/each}}
然後使用該名稱發送操作:
// app/components/star-rating.js export default Ember.Component.extend({ item: null, rating: 0, (...) actions: { set: function(newRating) { var item = this.get('item'); this.sendAction('setAction', { item: this.get('item'), rating: newRating }); } } });
最後,該動作由控制器或路由在上游處理,這是更新項目評級的地方:
// app/routes/player.js export default Ember.Route.extend({ actions: { updateRating: function(params) { var skill = params.item, rating = params.rating; skill.set('score', rating); return skill.save(); } } });
發生這種情況時,此更改會通過傳遞給star-rating
組件的綁定向下傳播,因此顯示的滿星數會發生更改。
這樣,組件中不會發生突變,並且由於唯一特定於應用程序的部分是路由中操作的處理,因此組件的可重用性不會受到影響。
我們也可以將相同的組件用於足球技巧:
{{#each player.skills as |skill|}} {{star-rating item=skill rating=skill.score setAction="updateSkill"}} {{/each}}
最後的話
重要的是要注意,我看到人們犯(或自己犯)的一些(大多數?)錯誤,包括我在這裡寫的那些,將在 2.x 系列的早期消失或大大減輕Ember.js 的。
我上面的建議解決了剩下的問題,所以一旦你在 Ember 2.x 中開發,你就沒有理由再犯任何錯誤了! 如果您希望這篇文章為 pdf 格式,請訪問我的博客並單擊文章底部的鏈接。
關於我
兩年前我和 Ember.js 一起來到了前端世界,我會留下來。 我對 Ember 充滿熱情,以至於我開始在客座帖子和我自己的博客上寫博客,以及在會議上發表演講。 我什至為任何想學習 Ember 的人寫了一本書, Rock and Roll with Ember.js 。 您可以在此處下載示例章節。