JavaScriptフレームワークに興味あるし、Angular.jsを使ってみようかな・・・
そんな純真無垢なあなたを混沌の世紀末に引きずり込むのが、ほかでもないTutorialなのです。
TutorialではほぼControllerしか出てこないので、素直にこの通り書いているとまず間違いなく3カウントでControllerにコードが集中するいわゆるFat Controllerになり、せっかくMVCフレームワークも地獄の荒野になります。
実は、Angular.jsでまず目を通すべきなのはDeveloper GuideのConceptual Overviewです。これを読めばどう処理を分割するかがきちんと書かれていますが、以下ではそれ+経験をもとにAngular.jsで正しくMVCを使用するためのポイントをまとめました。
Angular.jsの3原則
1.ControllerはイベントハンドリングとData Bindingに集中する
これはAngular.jsに限らず、あらゆるフレームワークで言えることです。JavaScriptフレームワークにおけるControllerの役割は以下2つで、それに集中していることが望ましいです。
-
バインディング(Model -> View)
Controller内で管理するModel(またはそのリスト)の変更を、Viewに反映します。具体的にはng-bind
/ng-model
、ng-repeat
の設定に該当します。Angular.jsではこれらを設定するだけで自動的に変更が反映されます。 -
イベントハンドリング(View -> Model)
イベントの発生を検知し、対応する動作(関数)を呼び出します。具体的にはng-click
等の設定に該当します。
呼び出す関数は、Controllerに直接実装する以外に、Modelクラス、Serviceによる実装などの選択肢があります。この関数の実装をどのように分割するのかが、Fat Controllerを避けるポイントとなります(後述)。
2.Viewが複雑になったらDirective・Filterを使う
サイト内で共通で使用するデザインなどは、これを別途templateにしdirectiveとしてコンポーネント化することができます。
下記では、template.html
にデザインを分離し、それをmyTemplate
というdirectiveにして使用しています。
<div ng-repeat="m in models">
<my-template></my-template> <!-- Directiveを呼び出し -->
</div>
<script>
var myApp = angular.module("myApp",[]);
・・・
myApp.directive("myTemplate",function(){ //template.htmlへ切り出し
return {
restrict: 'E',
templateUrl: 'template.html'
};
})
</script>
directiveについての詳細は公式ドキュメント、また手前味噌ですがこちらの記事を参照ください。
これでデザインをコンポーネント化できるほか、表示用のロジックをControllerから分離することができます。
FilterはいわゆるHelperのように使用することができ、数値を通貨で表示したりカンマ区切りで表示したりといった、細かいところに使えます。
例としては、以下のような感じです。date
は最初から組み込まれているFilterなので、何にも書かないでもこれだけで動作します。Filterへは、:
で引数を渡すことができます。
<div ng-app>
{{1288323623006 | date:'yyyy/MM/dd'}}<br>
</div>
また、こちらも組み込まれているfilter
(紛らわしいですが、filterというFilterです)を配列に適用することで絞り込みもできます(この使い方のイメージのほうが強いかもしれません)。
<div ng-app ng-init="names = ['john','risa','mike','jhonasan','kensiro']">
<input type="text" ng-model="searchText" />
<div ng-repeat="n in names | filter:searchText" >{{n}}</div>
</div>
3.Serviceによるコンポーネント化を行う
ng-click
で呼び出される関数が数百行あって、Controller内には特定のイベントでしか使わないグローバル変数があふれる・・・という事態になっては、JavaScriptフレームワークを利用するメリットがありません。ここは適切にコンポーネント化を行いましょう。そのためにAngular.jsに用意されている仕組みが、Serviceになります。
ここでいったん用語を整理しておきますが、Angular.jsを構築するオブジェクトは以下2つに分けられます。
- Service
ユーザーがAPIを自由に決められるもの。なお、すべてsingletonとして扱われます(インスタンスは一つしか作成されず、使いまわされる)。 - Specialized object
controllerや directiveなど、Angular.jsによりAPIが定められているもの。
そしてServiceの作成方法(recipe)には以下の種類があります。
Value
値を定義するServiceを作成するrecipeです。具体的にはサーバーサイドのAPIのURLなど、アプリケーション全体の定数管理などに利用します。
var myApp = angular.module('myApp', []);
myApp.value('clientId', 'a12345654321x');
Factory
Valueだけでは処理を記述できないので、引数を取った関数を記載できるrecipeです。
先程のclientId
をFactoryベースで書き直すと、以下のようになります。
myApp.factory('clientId', function clientIdFactory() {
return 'a12345654321x';
});
当然、下記のように関数も記載できます。
myModule.factory('greeter', function($window) {
return {
greet: function(text) {
$window.alert(text);
}
};
});
このようにfactory
は単一の値だけでなく、関数やオブジェクトなども返却でき、その意味では最も汎用的に利用できるrecipeとなります。
Service
JavaScriptクラスを生成するrecipeとなり、「ServiceをService recipeで作る」と、そうした意味になります。このため、Angular.js界隈で「Service」といった場合、どちらの文脈で話しているか判断する必要があります。
function Greeter(clientId) {
this.clientId = clientId;
this.greet = function() {
return "Hello " + this.clientId;
}
}
myApp.service('Greeter', ["clientId", Greeting]);
ValueやFactoryと異なり、指定(return)した値がそのまま利用されるのでなく、オブジェクトがnewされて利用されます(上記の場合、new Greeterが行われます。ただ、シングルトンとして扱われるためnewされるのは1回のみです)。
既存資産がオブジェクト指向に則りJavaScriptクラスを利用して設計されているなら、Serviceとの相性が良くなります。
Provider
Providerは一番汎用的なrecipeで、他のrecipeはすべてProviderの簡易記法に該当します。
Factory/Serviceと異なり、config
でも利用できるという特性がありますが、逆に言うとconfig
でどうしても利用したい場合を除き使うべきでないようです。
Constant
Valueと同様ですが、config
でも利用できるという特性があります。見た感じとドキュメントの位置的なものから、Providerとセットで使うような印象を受けます。普通の定数/パラメーター管理ならValueで十分でしょう。
上記の詳細は、公式のProvidersを参照ください。かなり詳しく書かれています。
過去、FactoryとServiceどちらを使えばよいのか、ということがたびたび議論されてきましたが、1.2.7
以降のドキュメント整備とValue
recipeの登場により、明確に境界を引くことができるようになりました。
値の管理ならValue、関数ならFactory、JavaScriptクラスならServiceを利用する。
定数管理はValue、それ以外はFactoryを利用して記述しておき、Factoryで書いていたけれども一定のまとまりが出てきたもの、あるいはconstructorによる初期化処理が必要になったなどしてクラス化したくなった場合、Serviceにするというのが素直かなと思います(既に手元にJavaScriptクラス資産がある場合この限りでない)。
Controllerを除くSpecialized object(directiveやfilterなど)もFactoryで実装されており、全体としてFactoryで実装するというのは筋にかなっているかなと思います。
Serviceにする場合でも、上記のとおりrecipeの切り替えによる記載の変更はわずかであり、切り替えの負担はそれほどないと思います。
Service recipeを利用した処理のモジュール化の実例として、サンプルを作ってみました。
Serviceに切り出す対象の処理は、「Modelにメソッドとして実装するのも、Controllerに実装するのも適切ではない処理」ということなのでどんなサンプルにするか悩みましたが、こんな形のゲームっぽいものにしました。
1秒ごとに草がはえていき、牛を投入すると草を食べてくれるが5回食べるとおなか一杯で死んじゃうというものです。
Modelでは牛の状態(今までどれだけ草を食ったか)、Controllerでは経過秒数を管理しています。そして、経過秒数による状態の評価(草&牛の数の計算)をServiceとして切り出しました。
これをControllerで実装していたらまさに「ロジックがControllerに混在している」状態となり、かといって、牛Modelで扱うには不適当・・・なので、まさにServiceの出番なはず。
こんな視点でServiceをうまく使うことができれば、ゲームなどModelの状態変化が複雑に絡み合う場合でもうまく実装できると思います(そしてAngular.jsの場合単体テストが簡単になるというのもうれしいポイント)。
上記の3原則を覚えておけば、お前のAngular.jsはもうMVCではない・・・と言われないハズ。