AngularJSのサービス
AngularJSのサービスは、アプリケーション全体に渡って振る舞いや状態を保持できる関数、あるいはオブジェクトである。それぞれのAngularJSのサービスは一度だけインスタンス化されるので、アプリケーションは常に同じインスタンスにアクセスする。AngularJSのサービスで実装することができるものは、繰り返し行う処理、状態の共有、キャッシュ、ファクトリなどである。
なぜAngularJSのサービスが必要なのか
AngularJSのコントローラーは、以下のような仕事に優れている。
- 取得するモデルを決定し、それをHTMLに表示する
- ユーザーとのインタラクション
- 表示ロジック
コントローラーはステートフルだが、短命である。シングルページアプリケーションにおいて、コントローラーは何度も破棄と再生成が繰り返される。
サービス VS コントローラー
以下は、コントローラーの役割の違いを示したものである。
|コントローラー|サービス|
|表示ロジック|ビジネスロジック|
|ビューと直接結びつく|ビューからは独立している|
|UIを動かす|アプリケーションを動かす|
|特定の画面専用|再利用可能|
コントローラーは、データの取得・表示、ユーザインタラクション、UIのスタイリングと表示に責任を持つ。
サービスは、サーバの呼び出し、共通のバリデーションロジック、アプリケーションレベルの保存処理や、再利用可能なビジネスロジックに責任を持つ。
AngularJSの「$」
AngularJSによって提供されるすべてのサービスには、「$」という接頭辞がついている。たとえば、$log、$http、$windowなどである。これによって、サービスの名前を見たときに、AngularJSの組み込みのものかがすぐにわかる。逆に言えば、独自のサービスを作る際には、それらに「$」という接頭辞をつけてはいけない。組み込みのサービスか否かの判別が難しくなるからである。
組み込みのAngularJSサービスを利用する
サービスを使うコントローラーの定義方法は以下のようになる。
angular.module('myApp', [])
.controller('MainCtrl', function($log) { // unsafe when minified
// do something
});
angular.module('myApp', [])
.controller('MainCtrl', ['$log', function($log) { // safe
// do something
}]);
最初の定義方法では、AngularJSは無名関数の仮引数の情報を使用して依存するオブジェクトを調べる。この方法の場合、JavaScriptのminifyによって$logの名前が書き換えられた際に動作しなくなる。
第2の定義方法では、controller() の第1引数にはコントローラー名の文字列を、第2引数には配列を渡している。配列の最初の引数は依存するサービス名の文字列で、第2引数の無名関数では引数に$logを指定している。この方法の場合、文字列の’$log’はminifyされても変わらないので、minifyを行っても正常に動作する。
冗長ではあるが、第2の定義方法が推奨される。
注入の順番
依存するオブジェクトを文字列で定義すると、文字列の記述の順番にオブジェクトが渡される。
angular.module('myApp', [])
.controller('MainCtrl', ['$log', '$window', function($window, $log) {
// do something
}]);
文字列によって依存オブジェクトを指定している場合、AngularJSは無名関数の仮引数の情報を無視して、文字列の並んでいる順番に変数を渡す。したがって、上記コードでは、$logが$windoに、$windowが$logに渡される。
依存オブジェクトの指定文字列と、無名関数の仮引数の順番は一致している必要がある。
一般的なAngularJSのサービス
$window
グローバルなwindowオブジェクトのラッパー。グローバルな状態を避けるために用意され、特にテストにおいて重要。テストでは$windowサービスを簡単にモックできるので、windowではなく$windowを使うべき。
$location
ブラウザのURLの操作を行う。
(ただし、Single Page Applicationとしての範囲内でのURLの操作であり、location.hrefの書き換えのような別ページの遷移はできない)。
$http
HTTPリクエストを扱う。
独自のサービスを作成する
機能をサービスとして実装すべきか決めるには、以下のような基準に基いて決めるとよい。
- 再利用性があるか
- アプリケーションレベルの状態保持を行うものか
- ビューから独立しているか
- サードパーティのサービス(SocketIO等)と結びつくか
- キャッシュやファクトリか
上記基準のいずれかに該当するようであれば、サービスとして実装する価値がある。
シンプルなAngularJSのサービスを作る
angular.module('app', [])
.factory('ItemService', [function() {
var items = [];
return {
list :function() {
return items;
},
add: function(item) {
items.push(item);
}
};
}]);
AngularJSは以下を保証する。
- サービスは必要になるまで実体化されない
- サービス定義関数は一度だけ実行され、インスタンスはそのまま保持される(サービスはシングルトンである)
Factory, Service, Providerの違い
関数型のプログラミングスタイルを実践しているか、関数とオブジェクトを返すことを好むなら、module.factory()を使うべきである。
クラスベースのオブジェクト指向のスタイルを好むなら、service()を使うべきである。
function ItemService() {
var items = [];
this.list = function() {
return items;
}
this.add = function(item) {
items.push(item);
}
}
angular.module('app', [])
.service('ItemService', [ItemService]);
providerは、複雑な初期化処理が必要な場合に使用する。
function ItemService() {
var items = [];
this.list = function() {
return items;
}
this.add = function(item) {
items.push(item);
}
}
angular.module('app', [])
.provider('ItemService', function() {
var haveDefaultItems = true;
this.disableDefaultItems = function() {
haveDefaultItems = false;
};
// サービス初期化時に呼ばれる関数
this.$get = [function() {
var optItems = [];
if (haveDefaultItems) {
optItems = [
{id: 1, label: 'Item 0'},
{id: 2, label: 'Item 1'}
];
}
return new ItemService(optItems);
}];
})
.config('ItemServiceProvider', function() {
// この設定によって動作が変わる
var shouldHaveDefaults = false;
if (!shouldHaveDefaults) {
ItemsServiceProvider.disableDefaultItems();
}
});
config関数は、AngularJSのアプリケーションが起動する前に実行される。