13.コントローラの連携
始めに
本章ではサンプルアプリを例にコントローラの親子、兄弟関係やそれらの連携をどのように実装していくかを学びます。
サンプルアプリの説明
サンプルアプリはこちらです。
レシピ(サンプル集)»コントローラの連携サンプル
サンプルではhifiveでのデバッグをサポートするhifive開発者ツールを読み込んでいます。
Youtubeの動画プレイヤーアプリです。以下のような機能を持っています。
- キーワード検索
- キーワードでYoutubeの動画を検索します。検索結果が左側に表示されます。
- 動画の再生
- 検索結果やお気に入りリストから選択した動画を再生します。
- 再生中の動画情報の表示
- タイトル、投稿者コメントを表示します。
- 再生中の動画のコメント表示
- 動画の操作
- ボタンから、再生、停止など基本的なプレイヤーの操作が行えます。
- お気に入りリスト
- お気に入りの登録、削除が行えます。
サンプルアプリを構成するコントローラ
本アプリは1ページに複数の機能を持ちます。動画検索、動画再生、動画の操作、お気に入り、コメント表示の5つの主要な機能にに対してそれぞれ1つのコントローラを作成しています。
また、各コントローラを連携させるために、コントローラ同士の連携をつかさどる親コントローラを1つ用意します。
各コントローラの親子関係は以下のようになっています。
FavoriteController(お気に入りコントローラ)は、共通部品としてカルーセルリスト機能(スライド、ドラッグ操作でリストを1周させるビュー機能)と、右クリックメニュー表示機能を持ち、それぞれ子コントローラとして分割してあります。
ページ全体を把握するPageControllerが子コントローラを把握しており、兄弟コントローラ同士の連携は全て親コントローラがつかさどります。
もし、親コントローラを作成せずに、各コントローラが互いのメソッドを呼んで連携するようにしてしまうと、
- 画面の仕様変更があった時に依存関係が複雑になり、保守が大変。
- それが共通画面部品の場合に、使いまわしが困難になる。
といったデメリットがあります。
子コントローラの定義
あるコントローラに'~~Controller'で終わるプロパティに別のコントローラ定義オブジェクトを持たせると、それらのコントローラは親子関係となり、親コントローラをコントローラ化した際に、子コントローラもコントローラ化されます。親子両方のコントローラのライフサイクルイベントの実行とイベントハンドラの登録が行われます。
// 子コントローラの定義
var screenController = {
__name: 'youtube.controller.ScreenController',
// ~省略~
};
// screenControllerをグローバルに公開
h5.core.expose(screenController);
})();
(function(){
// 親コントローラ
var pageControlelr = {
__name: 'youtube.controller.PageController',
// 子コントローラとしてScreenControllerを持たせる
_screenController: youtube.controller.ScreenController,
// ~省略~
};
h5.core.expose(pageController);
})();
$(function(){
// コントローラ化
h5.core.controller('body', youtube.controller.PageController);
});
コントローラ化されるコントローラ定義オブジェクトが、子コントローラを持っているとき、子コントローラもコントローラ化されます。上記のソースコードの例では、PageControllerはScreenControllerを子コントローラとして持ち、ScreenControllerもコントローラ化されたので、ScreenController内のライフサイクルイベント(__readyなど)や、ScreenControllerで定義したイベントハンドラが動作するようになります。
親子コントローラのコントローラ化時の挙動については、リファレンス 親子コントローラのライフサイクル(未稿)をご覧ください。
この例では、body要素に、PageControllerとScreenControllerがバインドされ、PageControllerがScreenControllerのインスタンスを持っている(参照できる)状態になります。
子コントローラのメタ定義
子コントローラは、デフォルトでは親コントローラのルートエレメントと同じ個所にバインドされ、親コントローラも子コントローラもルートエレメントが同じになります。
ただし、__metaプロパティを記述することで、子コントローラのバインド先を親コントローラと異なる箇所にすることができます。
詳細は、コントローラのメタ属性の設定可能項目をご覧ください。
コントローラの連携
コントローラが兄弟コントローラと連携したいとき、子コントローラは親コントローラに対してイベントを通知します。親コントローラはイベントを受け取り、子コントローラのメソッドを呼び出します。
イベントをあげて通知するようなインターフェースにすることで、子コントローラは親のメソッドや兄弟のメソッドに依存せず、他の画面でも利用することができるようになります。
実際の実装例を示します。検索結果から動画をクリックした時に、動画を再生するときの連携例です。
SearchControllerで検索結果の動画サムネイルがクリックされたときのイベントを拾い、親コントローラに通知するためのイベントをあげます。
'li click': function(cotnext, $el) {
var videoId = $el.data('videoid'); // videoIDの取り出し
//'loadById'イベントをあげる。 動画のIDを引数で渡す。
this.trigger('loadById' ,{
id: id
});
},
this.trigger()を使うと、そのコントローラのルートエレメントを起点としたイベントを実行できます。サンプルアプリで独自に使用する"loadById"という名前のイベントを指定しています。
trigger()の第2引数に、loadByIdイベントハンドラに渡す引数を渡しています。
trigger()の詳細については、APIドキュメントController.triggerをご覧ください。
PageControllerは上がってきたイベントに応じて、子コントローラのメソッドを呼び出します。
'{rootElement} loadById': function(context) {
// 動画IDをcontext.evArgから取得する。
var id = context.evArg.id;
// 動画を再生。動画再生を行うScreenControllerのメソッドを呼び出す
this._screenController.loadById(id);
},
ScreenControllerは、loadByIdイベントが実行されると、自身の子コントローラであるScreenControllerのメソッドを呼び出して、ScreenControllerに動画のロードを開始させます。
triggerの第2引数で渡されたオブジェクトは、context.evArgに格納されます。動画IDをcontext.evArgから取り出して、ScreenController.loadById()に渡しています。
loadById: function(context){
// 動画のロード、再生を開始
},
ScreenControllerはloadByIdメソッドが呼ばれると動画のロード、再生を開始します。
このように、子コントローラであるSearchControllerは自分の兄弟や親のメソッドを呼ばずに、イベントをあげることでScreenControlelrのメソッドを実行できます。
大まかな流れとして、
- 他のコントローラと連携したいとき、triggerを使ってイベントをあげる。必要ならば引数を渡す。
- 親コントローラは、イベントが上がってきた時の処理をイベントハンドラに記述。イベントに対応する動作をするように、必要なら子コントローラのメソッドを呼び出す。
- 子コントローラは、外部から呼ばれることを想定したメソッドを用意しておく。
という手順で実装し、連携を行えるようにします。
サンプルアプリでの各コントローラ連携を示すと、以下のようになります。
連携のためのコントローラの機能
子コントローラの参照
親コントローラからは子コントローラを定義したプロパティで、子コントローラを参照できます。
var pageController = {
__name: 'youtube.controller.PageController',
/**
* 子コントローラ
*/
_screenController: youtube.controller.ScreenController,
hoge: function(context){
// 親コントローラから子コントローラを参照して、子コントローラのメソッド呼び出し
this._screenController.loadById();
// 子コントローラのルートエレメントを取得
var screenElm = this._screenController.rootElement;
// 子コントローラのルートエレメント以下の範囲でセレクタを使って要素を取得
var $screenTitle = this._screenController.$find('.title');
}
}
親コントローラの参照
子コントローラからは、parentControllerプロパティから親コントローラを取得できます。
var screenController = {
__name; 'youtube.controller.ScreenController',
hoge: function(id) {
// 親コントローラを取得
var parentCtrl = this.parentController;
// 親のルートエレメントを取得
var parentCtrl.rootElement;
// 親のルートエレメント以下の範囲でセレクタを使って要素を取得
var $hoge = parentCtrl.$find('.hoge');
}
}
親コントローラは複数の子コントローラインスタンスを持つことができますが、子コントローラから見た親コントローラは必ず1つになります(1対多)。
ルートコントローラの参照
コントローラの親子関係はネストすることができ、親、子、孫、…と作成することができます。
サンプルアプリでも、以下のように親子関係をネストさせています。
PageController->FavoriteController->[ContextMenuController, LoopCarouselController]
親子関係がネストしている場合に、一番大元のコントローラ(ルートコントローラ)を、rootControllerプロパティで参照することができます。
__name: 'h5.ui.ContextMenuController',
hoge: function(){
this.parentController; // FavoreiteController
this.rootController; // PageController
}
};
var favoriteController = {
__name: 'youtube.controller.FavoriteController',
// FavoriteControllerは子コントローラにContextMenuControllerを持つ
_contextMenuController: contextMenuController,
hoge: function(){
this.parentController; // PageController
this.rootController; // PageController
}
};
var pageController = {
__name: 'youtube.controller.PageController',
_favoriteController: favoriteController,
hoge: function(){
this.parentController; // null
this.rootController; // PageController (thisと同じ)
}
};
親コントローラとルートコントローラの関係は以下のようになります。
親子に分割すべき例
親子に分割せずに全ての機能を一つのコントローラに記述できないこともないですが、互いに疎なコントローラの単位に分割しておくのが設計上望ましいです。汎用的な部品は再利用できたり、連携するコントローラの仕様が変わった時の対応も楽になります。
機能単位での分割
機能単位としてコントローラを分割しておくと、画面からある機能を追加したり削除したりする変更があった時に、コントローラを入れ替えて、親コントローラを少し書き換えるだけで対応可能になります。
全て一つのコントローラとして書いてしまうと、機能連携する箇所を全て書き換えなければなりません。
共通部品を抽出して分割
他の画面で使いまわせる汎用的な機能を持つ部分は、別コントローラとして分割するべきです。
サンプルアプリのFavoriteControllerを例にとると、FavoriteControllerはLoopCarouselControllerを子コントローラとして持ちます。LoopCarouselControllerはDOM要素をリストにしてスクロールで一周できるようにする(カルーセル部品にする)コントローラです。
LoopCarouselControllerはカルーセル部品として汎用的であり、お気に入り動画に限らずどんなDOM要素でもカルーセルとして表示する機能を持つ汎用的な部品となります。
そのため、『お気に入りリストの一覧をカルーセル部品として表示する機能』のうち、お気に入りリストを管理するFavoriteController、カルーセル部品を管理するLoopCarouselController、の2つに機能を分割しています。
コントローラの設計に関しては、開発ガイド コントローラの設計(未稿)をご覧ください。