最も多く使われているであろう, AngularJSの拡張コンポーネント, ui-router.
擬似的に画面遷移が必要となるようなアプリケーションをAngularJSで開発する際には、事実上必須といっても過言じゃない.
今回はui-routerに含まれているresolveを活用するお話.
ui-routerとresolve
ui-routerの基本的な使い方を説明したい訳ではないので、そこら辺は割愛して, resolveの使いどころから説明していく.
下記のようなstate定義があったとする.
angular.module('sample', ['ui.router'])
.config(function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state('app', {
url: '/',
templateUrl: 'app/index.html',
})
.state('app.customer', {
url: 'customer',
controller: function () {
// 処理色々...
},
controllerAs: 'customerCtrl',
templateUrl: 'app/customer.html'
})
;
$urlRouterProvider.otherwise('/');
});
state customer
は「顧客一覧」は画面を表しており, 顧客データを取得するためにはREST APIを実行しなくてはならないような状況を考えよう.
customer
stateのViewを描画するためには, 顧客一覧データが必要になる.
もちろん、Controllerの初期化時に, $http
を利用してデータを取得しても構わないが, ここでresolveである.
state定義に次にように, resolve
を追加してみる:
.state('app.customer', {
url: 'customer',
resolve: {
customers: function ($http) {
return $http.get({
url: '/api/v1/customer/'
});
}
},
controller: function (customers) {
console.log(customers);
// 処理色々...
},
controllerAs: 'customerCtrl',
templateUrl: 'app/customer.html'
})
resolveは「Controllerの実行前にControllerにInjectionすべきデータを生成する」機能である.
resolve
には {ControllerにInjectしたい名称: 取得処理を記述したfunction}
形式のObjectを渡す.
上記の例では, customers
(=顧客一覧) というオブジェクトをControllerに渡しており, customers
の取得処理には $http
を用いている.
このようにすれば, Controllerが実行された時点で顧客一覧のデータが手元に渡っていることが確約されて嬉しいわけだ。
ここで重要なのは, resolveに渡す関数はPromiseを返却できるという点.
実は, resolve
は 関数の戻り値に併せて, 自動的に下記を判別した上で挙動を切り替えるように実装されている.
- 関数が通常の値をreturnする場合: そのままControllerの引数とする
- 関数がPromiseを返却する場合: 非同期の完了を待ってから,
then()
の引数をControllerの引数とする
従って, 先ほどの例のresolve
部分を単純な配列返しの関数に書き換えても、問題なく動作する.
.state('app.customer', {
url: 'customer',
resolve: {
customers: function () {
return ['Alice', 'Bob'];
}
},
controller: function (customers) {
console.log(customers);
// 処理色々...
},
controllerAs: 'customerCtrl',
templateUrl: 'app/customer.html'
})
なお, ngRouterにも同名のresolve
というプロパティが定義できるようになっているが, こちらはドキュメントを見る限り、非同期には対応していない模様.
$resolve
さて、今回のエントリで本当に語りたかったのはここから.
ui-routerにおけるresolveの機構は $stateProvider
の話のみにとどまらない.
なぜなら、$resolve
というServiceになっており, 外部から利用可能となっているからだ.
$resolve
を利用すると、データストア系のServiceに対して, データ取得処理を簡単に切り替えることが出来る.
- プロダクトからの利用時: API経由でのデータ取得
- karmaでの単体テストやngdocのexample経由での利用時: configに直接データを記述
例として、次のようなシーンを想定してほしい:
「データのキャッシュ機構を担うServiceを作っているが, キャッシュ元データの取得処理は.config
で設定できるようにしたい
そこで、下記のようなServiceとProviderを作ってみる.
angular.module('sample').provider('myAwesomeService', function () {
var cached;
var provider = {
// データの取得処理を利用者側に移譲したいため, Providerのプロパティ化.
getData: function () {return null;},
$get: function ($q) {
// Service定義
return function () {
var deferred = $q.defer();
if(!cached) {
cached = provider.getData();
}
deferred.resolve(cached);
return deferred.promise;
};
}
};
return provider;
});
上記のServiceは, キャッシュ元のデータ取得部分 getData
をProviderのプロパティとして切り出しているため, module.config
から下記のようにgetDataを差し替えることが出来る:
angular.module('sample')
.config(function (myAwesomeServiceProvider) {
// 利用者側の設定部分.
myAwesomeServiceProvider.getData = function () {
return 'hoge';
};
});
ここで、「キャッシュデータを$http
を使ってAjaxで取得したい」となったらどうなるだろう?
module.config
の引数である関数には、ProviderかConstしかインジェクションできないため, getDataが$http
を利用することは出来ない.
そこで$resolve
である.
上記のmyAwesomeService
におけるデータ取得箇所を $resolve
を使って書き換えると下記のようになる.
angular.module('sample').provider('myAwesomeService', function () {
var cached;
var provider = {
// データの取得処理を利用者側に移譲したいため, Providerのプロパティ化.
getData: function () {return null;},
$get: function ($q, $resolve) {
// Service定義
return function () {
var deferred = $q.defer();
if(cached) {
deferred.resolve(cached);
}else{
// $resolveの利用.
$resolve.resolve({data: provider.getData}).then(function (result) {
cached = result.data;
deferred.resolve(cached);
});
}
return deferred.promise;
};
}
};
return provider;
});
$resolve.resolve
の引数は $stateProvider
におけるresolve利用時と同じく、{key: invokable}
の形式である.
上記の例では, provider.getData
が invokable
に相当するため, Moduleのconfigで.getData
に設定する関数に任意のServiceをインジェクト出来るようになったわけだ。
それだけなら、AngularJS組み込みの$inject#invoke
を使っているのと大差ないが、 $resolve
の優れているところは, invokavle
の関数が同期/非同期のどちらでも関係なく動作するように実装されているところ.
(ngRouterとui-routerのresolveの違い)
従って、.config
に次のような $q
を使った非同期処理を設定しても, 先ほどのmyAwesomeServiceは動作する.
angular.module('sample')
.config(function (myAwesomeServiceProvider) {
// 利用者側の設定部分.
myAwesomeServiceProvider.getData = function ($q) {
var defer = $q.defer();
defer.resolve('foo')
return defer.promise;
};
});
まとめ
- ui-router使うときは、resolveで簡単データインジェクト
- 自作Service + Providerでも
$resolve
が利用可能