instantiate()でServiceを動的に生成! 引数も渡す!
AngularJSをお使いの皆さん、APIのひとつであるinstantiate()
関数を使ったことがありますか? 今までのService実装の常識がひっくり返る便利な使い方に気付いたので紹介します。
前置きがあるので、instantiate()を使うまで飛んでもかまいません。
背景
angular.module('myApp', ['ngRoute', 'ngResource']);
function SampleCtrl (SharedBookArray)
{
console.log('SampleCtrl constructor')
}
angular.module('myApp')
.controller('SampleCtrl', [
'SharedBookArray',
SampleCtrl
]);
function SharedBookArray ($resource)
{
console.log('SharedBookArray constructor')
}
angular.module('myApp')
.service('SharedBookArray', [
'$resource',
SharedBookArray
]);
例としてSharedBookArray
というServiceがあったとしましょう。いくつかのControllerで、データベースAPIから取得した本の情報を保持・共有するためのものです。
--
ここで、お気に入りの本も別途データベースから取ってくる必要が生まれました。SharedFavBookArray
のServiceを用意します。
function SharedFavBookArray ($resource)
{
console.log('SharedFavBookArray constructor')
}
angular.module('myApp')
.service('SharedFavBookArray', [
'$resource',
SharedFavBookArray
]);
丸コピペです。例ではコンストラクタ関数にlog()
しかありませんが、実際にはAPIに問い合わせる関数や取得した配列の操作に関する関数が複数あるとします。
これらをSharedBookArray
、SharedFavBookArray
とコピペ量産してしまうと重複したコードが増えていくのは目に見えています。
目的
SharedBookArray
、SharedFavBookArray
はどちらも本の情報群を扱うので関数も同じものを用意したい、なのにServiceは複数個用意する必要がある。これでは効率がよくありません。
抽象化されたSharedBookArray
一つで『Service生成時に対象を指定できれば便利』だと考えました。
課題
AngularJSのServiceといえば、事前にDIするサービス群を指定しています。ここから自動的に注入されるインスタンスが決定され、あとはコアが勝手にやってくれます。
angular.module('myApp')
.service('MyService', [
'ProviderA',
'ProviderB',
'ProviderC', // この辺
'ProviderD',
MyService
]);
ここを動的に決定できればよさそう。
instantiate()を使う
ここからが本題です。
実はAngularJSではServiceインスタンスを自動で生成させず、手動で生成させる余地が残されています。それがauto::$injector#instantiate()
です。
大半のコアAPIはng
Moduleに所属していますが、$injector
はauto
Moduleに属しており、コア実装内でも頻繁に使われる縁の下の力持ちです。この$injector
にはAngularJSを使い倒す上で重要な関数が揃っています。
injector()とinstantiate()
あまり耳慣れない関数なので、使い方に慣れるまではエラーの連発でしたが、慣れてしまうと手順自体は短いです。
function SampleCtrl ($rootElement)
{
var SharedBookArray;
// ここから
var injector = $rootElement.injector();
var service = injector.instantiate(
GenSharedBookArray, {domain: 'fav'}
);
// ここまでが重要
SharedBookArray = service;
}
// ...
function GenSharedBookArray ($resource, domain)
{
// ...
}
angular.module('myApp')
.service('GenSharedBookArray', [
'$resource',
'domain',
GenSharedBookArray
]);
angular.injector()
と案内されているこの関数は$rootElement#injector()
としても使えます(公式Doc)。
angular.injector()
は事前に必要なModule名を指定する必要があり、取りこぼすとエラーを連発させるので、あまり使い勝手が良いと思えません。$rootElement#injector()
だと必要なModuleは既に$rootElement
に定義されているので、安心して使うことができます。
$rootElement.injector()
の戻り値は$injector
です(公式Doc)。$injector#instantiate()
を使ってServiceを動的に生成します。
謎だったinstantiate()の第2引数
公式Docでさえ薄い説明で、書籍『AngularJSリファレンス』でも使い方が触れられなかった謎の第2引数instantiate(Type, [locals])
のlocalsですが、ここが今回の要。
var service = injector.instantiate(
GenSharedBookArray, {domain: 'fav'}
); /* ~~~~~~~~~~~~~~~ */
ここにはObjectを与えるようで、key-valueの形式で記述します。ここでのペアはauto::$provide#value()
(公式Doc)として扱われるようで、
function GenSharedBookArray($resource, domain)
{ /* ~~~~~~ */
// ...
}
引数domain
に渡ります。もちろんkey名は任意です。AngularJSがエラいなと感心した点として、試しにinstantiate
の第2引数を省略したときはちゃんと「domain
が見つからない」とエラーを出します。
アノテーションの記述
/* 丸ごと不要
angular.module('myApp')
.service('SharedBookArray', []);
*/
動的生成するので、静的な生成の定義は不要になりますが、minify対策としてアノテーションを記述すべきです。
function GenSharedBookArray($resource, domain)
{
// ...
}
GenSharedBookArray.$inject = ['$resource', 'domain']; // この行
初期値の定義
省略しても初期値を使って動作させるには次のようにします。
angular.module('myApp')
.value('domain', 'default'); // ここに初期値
これはServiceと同じソースに書いておくと良さそうです。Value名はmyApp
内で重複すると上書きされるので、命名規則も考えておきます。
使い道が無いと思っていたvalue()
はこうやって使うんですね!
もう少し使いやすく
長くなってきましたが、あと少し。
今のままではControllerごとにinjector()
が隠蔽されずに書かれているので、これをFactoryでラップしてみます。
function SharedBookArrayFactory ($rootElement)
{
return function (v) {
var locals = (!v) ? void 0 : {domain: v};
var injector = $rootElement.injector();
var service = injector.instantiate(
GenSharedBookArray, locals
);
return service;
};
}
angular.module('myApp')
.factory('SharedBookArrayFactory', [
'$rootElement',
SharedBookArrayFactory
]);
使うときは簡単。
function SampleCtrl (SharedBookArrayFactory)
{
var SharedBookArray = SharedBookArrayFactory('general');
var SharedFavBookArray = SharedBookArrayFactory('fav');
var SharedDefaultBookArray = SharedBookArrayFactory();
}
angular.module('myApp')
.controller('SampleCtrl', [
'SharedBookArrayFactory',
SampleCtrl
]);
おっ、イイ感じだね!
$injector
はAngularJSでも特に根幹を担うAPIです。特にコアなので公式Docや多くの解説でもふわっと書かれがちですが、理解すると強い武器になるでしょう。ただし自由度が高いのでご利用は計画的に。
この組み合わせを気付くきっかけとなった書籍『AngularJSリファレンス』は、ほぼ全てのAPIが網羅され、もちろん日本語で、とても読みやすいです。ある程度公式Docを読み込んだつもりでも発見が多かったのでお薦めします。
それでは!
Discussion