Ember.js開発者が犯す8つの最も一般的な間違い
公開: 2022-03-11Ember.jsは、複雑なクライアント側アプリケーションを構築するための包括的なフレームワークです。 その信条の1つは「設定より規約」であり、ほとんどのWebアプリケーションに共通する開発の大部分が存在するという信念であり、したがって、これらの日常的な課題のほとんどを解決するための単一の最良の方法です。 ただし、適切な抽象化を見つけ、すべてのケースをカバーするには、時間とコミュニティ全体からの入力が必要です。 推論が進むにつれて、解決策を見つける必要があるときに私たちの手を投げて誰もが自分で身を守るのではなく、時間をかけてコア問題の解決策を正しく取得し、それをフレームワークに焼き付ける方が良いです。
Ember.jsは、開発をさらに容易にするために絶えず進化しています。 ただし、他の高度なフレームワークと同様に、Ember開発者が陥る可能性のある落とし穴がまだあります。 次の投稿で、これらを回避するためのマップを提供したいと思います。 さっそく飛び込もう!
よくある間違いその1:すべてのコンテキストオブジェクトが渡されたときにモデルフックが起動することを期待する
アプリケーションに次のルートがあると仮定します。
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' } });
ここまでは順調ですね。 ただし、ブラウザのナビゲーションバーからアプリケーションをロードする以外に、ルートを入力する方法は他にもあります。 それらの1つは、テンプレート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}}
エンバーの緩和計画
ルーティング可能なコンポーネントは、おそらくバージョン2.1または2.2でまもなくEmberに登場する予定です。 それらが着陸すると、動的セグメントを持つルートにどのように移行しても、モデルフックは常に呼び出されます。 ここで対応するRFCを読んでください。
よくある間違いその2:ルート駆動型コントローラーがシングルトンであることを忘れる
Ember.jsのルートは、対応するテンプレートのコンテキストとして機能するコントローラーにプロパティを設定します。 これらのコントローラーはシングルトンであるため、コントローラーがアクティブでなくなった場合でも、コントローラーで定義された状態は存続します。
これは見落としがちなことで、私もこれに出くわしました。 私の場合、バンドと曲を含む音楽カタログアプリがありました。 songs
コントローラーのsongCreationStarted
フラグは、ユーザーが特定のバンドの曲の作成を開始したことを示しています。 問題は、ユーザーが別のバンドに切り替えた場合、 songCreationStarted
の値が持続し、途中で完成した曲が他のバンドのものであるように見え、混乱していたことでした。
解決策は、残したくないコントローラーのプロパティを手動でリセットすることです。 これを行うための1つの可能な場所は、対応するルートのsetupController
フックです。これは、 afterModel
フック(その名前が示すように、 model
フックの後にあります)の後のすべての遷移で呼び出されます。
// app/routes/band.js export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); controller.set('songCreationStarted', false); } });
エンバーの緩和計画
繰り返しになりますが、ルーティング可能なコンポーネントの夜明けはこの問題を解決し、コントローラーを完全に終わらせます。 ルーティング可能なコンポーネントの利点の1つは、ライフサイクルがより一貫しており、ルートから移行するときに常に分解されることです。 彼らが到着すると、上記の問題はなくなります。
よくある間違いその3: setupController
でデフォルトの実装を呼び出さない
Emberのルートには、アプリケーション固有の動作を定義するためのライフサイクルフックがいくつかあります。 対応するテンプレートのデータをフェッチするために使用されるmodel
と、テンプレートのコンテキストであるコントローラーをセットアップするためのsetupController
についてはすでに説明しました。
この後者のsetupController
には、コントローラーのmodel
プロパティとしてmodel
フックからモデルを割り当てるという適切なデフォルトがあります。
// ember-routing/lib/system/route.js setupController(controller, context, transition) { if (controller && (context !== undefined)) { set(controller, 'model', context); } }
( context
は、上記のmodel
と呼んでいるember-routing
パッケージで使用される名前です)
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 } });
エンバーの緩和計画
前に述べたように、コントローラーと、それらを使用するsetupController
フックは間もなくなくなるため、この落とし穴はもはや脅威にはなりません。 ただし、ここで学ぶべきより大きな教訓があります。それは、祖先での実装に注意することです。 Emberのすべてのオブジェクトの母であるEmber.Object
で定義されているinit
関数は、注意が必要なもう1つの例です。
よくある間違いその4:親以外のルートでthis.modelFor
を使用する
Emberルーターは、URLを処理するときに、各ルートセグメントのモデルを解決します。 アプリケーションに次のルートがあると仮定します。
Router.map({ this.route('bands', function() { this.route('band', { path: ':id' }, function() { this.route('songs'); }); }); });
/bands/24/songs
のURLを指定すると、 bands
、 bands.band
、 bands.band.songs
の順にmodel
フックが呼び出されます。 ルートAPIには、特に便利なメソッドmodelFor
があります。このメソッドは、子ルートで使用して、親ルートの1つからモデルをフェッチできます。これは、そのモデルがその時点で確実に解決されているためです。
たとえば、次のコードは、 bands.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'); }); });
bands
ルートが親ではなくなり、そのモデルが解決されていないため、URLで指定されたバンドをフェッチする方法が機能しなくなります。
// 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
を使用できない場合は、他の手段を使用して必要なデータを取得することです。
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); } });
よくある間違いその5:コンポーネントアクションが実行されるコンテキストの間違い
ネストされたコンポーネントは、常にEmberで推論するのが最も難しい部分の1つです。 Ember 1.10でのブロックパラメーターの導入により、この複雑さの多くは軽減されましたが、多くの状況では、子コンポーネントから起動されたアクションがトリガーされるコンポーネントを一目で確認するのは依然として困難です。
band-list-items
を含むband-list
コンポーネントがあり、各バンドをリスト内のお気に入りとしてマークできると仮定します。
// 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')); } } });
ユーザーが[Favethis]ボタンをクリックすると、 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
ルート(またはその親ルートの1つ)で処理する必要があります。
エンバーの緩和計画
ネストのレベルが多い場合(たとえば、 band-list-item
内にfav-button
コンポーネントがある場合)、これはより複雑になることが想像できます。 メッセージを出すには、内側からいくつかのレイヤーに穴を開けて、各レベルで意味のある名前を定義する必要があります( setAsFavorite
、 favoriteAction
、 faveAction
など)。
これは、マスターブランチですでに利用可能であり、おそらく1.13に含まれる「改善されたアクションRFC」によって簡単になります。
上記の例は、次のように簡略化されます。
// 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の計算されたプロパティは他のプロパティに依存しており、この依存関係は開発者が明示的に定義する必要があります。 役割の1つがadmin
である場合にのみ、trueである必要があるisAdmin
プロパティがあるとします。 これは人がそれを書くかもしれない方法です:
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles')
上記の定義では、 isAdmin
の値は、 roles
配列オブジェクト自体が変更された場合にのみ無効になりますが、アイテムが既存の配列に追加または削除された場合は無効になりません。 追加と削除も再計算をトリガーする必要があることを定義する特別な構文があります。
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles.[]')
よくある間違いその7:オブザーバーに優しい方法を使用しない
Common Mistake No. 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.[]') });
このようなUser
にadmin
の役割を追加すると、驚きます。
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}}
さらに、コンポーネントが星を表示し、各ポイントに1つの完全な星が表示され、その後、最大評価まで空の星が表示されると仮定します。 星がクリックされると、コントローラーで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(); } } });
それで仕事は終わりますが、いくつか問題があります。 まず、渡されたアイテムに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としてご希望の場合は、私のブログにアクセスして、投稿の下部にあるリンクをクリックしてください。
私について
私は2年前にEmber.jsでフロントエンドの世界に来ました、そして私はここにとどまります。 私はEmberに非常に熱心になり、ゲストの投稿と自分のブログの両方で熱心にブログを書き始め、会議で発表し始めました。 私は、Emberを学びたい人のために、Ember.jsでロックンロールという本を書きました。 ここからサンプルの章をダウンロードできます。