最初のEmber.jsアプリを構築するためのガイド
公開: 2022-03-11最新のWebアプリケーションがクライアント側でますます実行されるようになると(「Webサイト」ではなく「Webアプリケーション」と呼ばれるようになったという事実自体がはっきりとわかります)、クライアント側のフレームワークへの関心が高まっています。 。 この分野には多くのプレーヤーがいますが、多くの機能と多くの可動部分を備えたアプリケーションでは、特にAngular.jsとEmber.jsの2つが際立っています。
包括的な[Angular.jsチュートリアル][https://www.toptal.com/angular-js/a-step-by-step-guide-to-your-first-angularjs-app]を既に公開しているので、この投稿では、Ember.jsに焦点を当てます。ここでは、音楽コレクションをカタログ化するための単純なEmberアプリケーションを構築します。 フレームワークの主要な構成要素を紹介し、その設計原則を垣間見ることができます。 読みながらソースコードを見たい場合は、Githubでロックンロールとして入手できます。
何を構築しますか?
Rock&Rollアプリの最終バージョンは次のようになります。
左側にはアーティストのリストがあり、右側には選択したアーティストの曲のリストがあります(私は音楽が好きですが、逸脱していることもわかります)。 テキストボックスに入力して隣接するボタンを押すだけで、新しいアーティストや曲を追加できます。 各曲の横にある星は、iTunesのようにそれを評価するのに役立ちます。
アプリの基本的な機能を次の手順に分解できます。
- [追加]をクリックすると、[新しいアーティスト]フィールドで指定された名前で新しいアーティストがリストに追加されます(特定のアーティストの曲にも同じことが言えます)。
- [新しいアーティスト]フィールドを空にすると、[追加]ボタンが無効になります(特定のアーティストの曲についても同じことが言えます)。
- アーティストの名前をクリックすると、右側にそのアーティストの曲が表示されます。
- 星をクリックすると、特定の曲が評価されます。
これを機能させるにはまだ長い道のりがあるので、始めましょう。
ルート:Ember.jsアプリの鍵
Emberの際立った特徴の1つは、URLに重点を置いていることです。 他の多くのフレームワークでは、別々の画面に別々のURLを設定することは、不足しているか、後から付け加えられています。 Emberでは、ルーター(URLとURL間の遷移を管理するコンポーネント)は、ビルディングブロック間の作業を調整する中心的な要素です。 したがって、Emberアプリケーションの内部動作を理解するための鍵でもあります。
アプリケーションのルートは次のとおりです。
App.Router.map(function() { this.resource('artists', function() { this.route('songs', { path: ':slug' }); }); });
リソースルート、 artists
、およびその中にネストされたsongs
のルートを定義します。 その定義により、次のルートが得られます。
優れたEmberInspectorプラグイン(ChromeとFirefoxの両方に存在します)を使用して、生成されたルートを読みやすい方法で表示しました。 エンバールートの基本的なルールは次のとおりです。上記の表を使用して、特定のケースについて確認できます。
暗黙の
application
ルートがあります。これは、すべてのリクエスト(トランジション)に対してアクティブになります。
暗黙の
index
ルートがあります。これは、ユーザーがアプリケーションのルートに移動したときに入力されます。
各リソースルートは同じ名前のルートを作成し、その下に暗黙的にインデックスルートを作成します。
このインデックスルートは、ユーザーがルートに移動するとアクティブになります。 この場合、
artists.index
は、ユーザーが/artists
に移動したときにトリガーされます。単純な(リソース以外の)ネストされたルートには、プレフィックスとして親ルート名があります。
this.route('songs', ...)
として定義したルートには、名前としてartists.songs
が含まれます。 ユーザーが/artists/pearl-jam
または/artists/radiohead
に移動するとトリガーされます。パスが指定されていない場合は、ルート名と同じであると見なされます。
パスに
:
が含まれている場合、動的セグメントと見なされます。それに割り当てられた名前(この場合は
slug
)は、URLの適切なセグメントの値と一致します。 上記のslug
セグメントには、pearl-jam
、radiohead
、またはURLから抽出されたその他の値が含まれます。
アーティストのリストを表示する
最初のステップとして、左側にアーティストのリストを表示する画面を作成します。 この画面は、ユーザーが/artists/
に移動したときに表示されるはずです。
その画面がどのようにレンダリングされるかを理解するために、もう1つの包括的な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
属性をモデルフック自体の戻り値、つまりアーティストのリストに設定するのに十分なほど賢いです。 (繰り返しますが、これは「設定より規約」パラダイムの結果です。)1つのレイヤーをステップダウンして、リストを表示するためのテンプレートを作成できます。
<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
ルートに移動するリンクをレンダリングします。 リンクにはアーティスト名が含まれています。 ハンドルバーの#each
ヘルパーは、その中のスコープを現在のアイテムに変更するため、 {{name}}
は常に現在反復中のアーティストの名前を参照します。
ネストされたビューのネストされたルート
上記のスニペットのもう1つの興味深い点: {{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>
要約すると、1つのルート( artists
)は、そのコンテンツを左側のサイドバーにレンダリングします。そのモデルはアーティストのリストです。 別のルートであるartists.index
は、 artists
テンプレートによって提供されるスロットに独自のコンテンツをレンダリングします。 モデルとして機能するデータをフェッチすることもできますが、この場合、表示したいのは静的テキストだけなので、その必要はありません。
アーティストを作成する
パート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
を使用します。 その中で、テキスト入力の値を、このテンプレート、 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
を返し、ボタンがdisabled
になるように実装されています。 最後の.property
呼び出しは、これを「計算されたプロパティ」にします。これは、Emberケーキのもう1つのすばらしいスライスです。
計算されたプロパティは、他のプロパティに依存するプロパティであり、それ自体が「通常」または計算されます。 Emberは、依存するプロパティの1つが変更されるまで、これらの値をキャッシュします。 次に、計算されたプロパティの値を再計算し、再度キャッシュします。
上記のプロセスを視覚的に表したものを次に示します。 要約すると、ユーザーがアーティストの名前を入力すると、 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
オブジェクトでラップする必要があり、ルート、コントローラー、またはビューで定義できます。 アクションの結果がコントローラーに限定されるのではなく、「グローバル」であるため、ここでルート上で定義することにしました。
ここでは特別なことは何も起こっていません。 保存操作が正常に完了したことをバックエンドから通知された後、次の3つのことを順番に実行します。
- テンプレートのモデル(すべてのアーティスト)に新しいアーティストを追加して、再レンダリングされ、新しいアーティストがリストの最後のアイテムとして表示されるようにします。
-
newName
バインディングを介して入力フィールドをクリアし、DOMを直接操作する必要をなくします。 - 新しいルート(
artists.songs
)に移行し、そのルートのモデルとして新しく作成されたアーティストを渡します。transitionTo
は、ルート間を内部的に移動する方法です。 (link-to
ヘルパーは、ユーザーアクションを介してそれを行うのに役立ちます。)
アーティストの曲を表示する
アーティストの名前をクリックすることで、アーティストの曲を表示できます。 また、新ルートのモデルとなるアーティストもお譲りします。 このようにモデルオブジェクトが渡された場合、モデルを解決する必要がないため、ルートのmodel
フックは呼び出されません。
ここでのアクティブなルートはartists.songs
であるため、コントローラーとテンプレートはそれぞれArtistsSongsController
と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ビルディングブロックであるコンポーネントとは対照的です。コンポーネントは、渡されたものだけにアクセスできる、分離された再利用可能なコントロールです。 (この例では、星評価コンポーネントを使用することもできます。)
ユーザーが星の1つをクリックしたときに、ビューに星の数がどのように表示され、曲の評価が設定されるかを見てみましょう。
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
rating
およびnumStars
は、 ArtistsController
のdisabled
プロパティで前に説明した計算されたプロパティです。 上記では、いわゆる計算プロパティマクロを使用しましたが、そのうちの約12個がEmberで定義されています。 それらは、典型的な計算されたプロパティをより簡潔にし、エラーが発生しにくくします(書き込み)。 レーティングをコンテキスト(したがって曲)のrating
に設定し、 fullStars
プロパティとnumStars
プロパティの両方を定義して、スターレーティングウィジェットのコンテキストで読みやすくしました。
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>
このスニペットには、いくつかの注意点が含まれています。
- まず、
each
ヘルパーは、プロパティ名の前にview
を付けることにより、(コントローラー上のプロパティではなく)viewプロパティを使用することを指定します。 - 次に、spanタグの
class
属性には、動的クラスと静的クラスが混在しています。 接頭辞:
が付いているものはすべて静的クラスになりますが、full:glyphicon-star:glyphicon-star-empty
表記はJavaScriptの三項演算子のようなものです。完全なプロパティがtrueの場合は、最初のクラスを割り当てる必要があります。 そうでない場合は、2番目。 - 最後に、タグがクリックされると、
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ライブラリの使用方法を確認してください。 楽しく構築してください!