🅰️

instantiate()でServiceを動的に生成! 引数も渡す!

2021/07/04に公開

AngularJSをお使いの皆さん、APIのひとつであるinstantiate()関数を使ったことがありますか? 今までのService実装の常識がひっくり返る便利な使い方に気付いたので紹介します。

前置きがあるので、instantiate()を使うまで飛んでもかまいません。

背景

myApp
angular.module('myApp', ['ngRoute', 'ngResource']);
SampleCtrl
function SampleCtrl (SharedBookArray)
{
  console.log('SampleCtrl constructor')
}
      
angular.module('myApp')
.controller('SampleCtrl', [
  'SharedBookArray',
  SampleCtrl
]);
SharedBookArray
function SharedBookArray ($resource)
{
  console.log('SharedBookArray constructor')
}

angular.module('myApp')
.service('SharedBookArray', [
  '$resource',
  SharedBookArray
]);

例としてSharedBookArrayというServiceがあったとしましょう。いくつかのControllerで、データベースAPIから取得した本の情報を保持・共有するためのものです。

--

ここで、お気に入りの本も別途データベースから取ってくる必要が生まれました。SharedFavBookArrayのServiceを用意します。

SharedFavBookArray
function SharedFavBookArray ($resource)
{
  console.log('SharedFavBookArray constructor')
}

angular.module('myApp')
.service('SharedFavBookArray', [
  '$resource',
  SharedFavBookArray
]);

丸コピペです。例ではコンストラクタ関数にlog()しかありませんが、実際にはAPIに問い合わせる関数や取得した配列の操作に関する関数が複数あるとします。

これらをSharedBookArraySharedFavBookArrayとコピペ量産してしまうと重複したコードが増えていくのは目に見えています。

目的

SharedBookArraySharedFavBookArrayはどちらも本の情報群を扱うので関数も同じものを用意したい、なのにServiceは複数個用意する必要がある。これでは効率がよくありません。

抽象化されたSharedBookArray一つで『Service生成時に対象を指定できれば便利』だと考えました。

課題

AngularJSのServiceといえば、事前にDIするサービス群を指定しています。ここから自動的に注入されるインスタンスが決定され、あとはコアが勝手にやってくれます。

angular.module('myApp')
.service('MyService', [
  'ProviderA',
  'ProviderB',
  'ProviderC', // この辺
  'ProviderD',
  MyService
]);

ここを動的に決定できればよさそう。

instantiate()を使う

ここからが本題です。

実はAngularJSではServiceインスタンスを自動で生成させず、手動で生成させる余地が残されています。それがauto::$injector#instantiate()です。

大半のコアAPIはngModuleに所属していますが、$injectorautoModuleに属しており、コア実装内でも頻繁に使われる縁の下の力持ちです。この$injectorにはAngularJSを使い倒す上で重要な関数が揃っています。

injector()とinstantiate()

あまり耳慣れない関数なので、使い方に慣れるまではエラーの連発でしたが、慣れてしまうと手順自体は短いです。

SampleCtrl
function SampleCtrl ($rootElement)
{
  var SharedBookArray;

  // ここから
  var injector = $rootElement.injector();
  var service = injector.instantiate(
    GenSharedBookArray, {domain: 'fav'}
  );
  // ここまでが重要

  SharedBookArray = service;
}
// ...
GenSharedBookArray
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ですが、ここが今回の要。

SampleCtrl
var service = injector.instantiate(
  GenSharedBookArray, {domain: 'fav'}
);                 /* ~~~~~~~~~~~~~~~ */

ここにはObjectを与えるようで、key-valueの形式で記述します。ここでのペアはauto::$provide#value()公式Doc)として扱われるようで、

GenSharedBookArray
function GenSharedBookArray($resource, domain)
{                                   /* ~~~~~~ */
  // ...
}

引数domainに渡ります。もちろんkey名は任意です。AngularJSがエラいなと感心した点として、試しにinstantiateの第2引数を省略したときはちゃんと「domainが見つからない」とエラーを出します。

アノテーションの記述

/* 丸ごと不要
angular.module('myApp')
.service('SharedBookArray', []);
*/

動的生成するので、静的な生成の定義は不要になりますが、minify対策としてアノテーションを記述すべきです。

GenSharedBookArray
function GenSharedBookArray($resource, domain)
{
  // ...
}

GenSharedBookArray.$inject = ['$resource', 'domain']; // この行

初期値の定義

省略しても初期値を使って動作させるには次のようにします。

angular.module('myApp')
.value('domain', 'default'); // ここに初期値

これはServiceと同じソースに書いておくと良さそうです。Value名はmyApp内で重複すると上書きされるので、命名規則も考えておきます。

使い道が無いと思っていたvalue()はこうやって使うんですね!

もう少し使いやすく

長くなってきましたが、あと少し。

今のままではControllerごとにinjector()が隠蔽されずに書かれているので、これをFactoryでラップしてみます。

SharedBookArrayFactory
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
]);

使うときは簡単。

SampleCtrl
function SampleCtrl (SharedBookArrayFactory)
{
  var SharedBookArray        = SharedBookArrayFactory('general');
  var SharedFavBookArray     = SharedBookArrayFactory('fav');
  var SharedDefaultBookArray = SharedBookArrayFactory();
}
      
angular.module('myApp')
.controller('SampleCtrl', [
  'SharedBookArrayFactory',
  SampleCtrl
]);

おっ、イイ感じだね!


Plunker 今回のまとめ

$injectorはAngularJSでも特に根幹を担うAPIです。特にコアなので公式Docや多くの解説でもふわっと書かれがちですが、理解すると強い武器になるでしょう。ただし自由度が高いのでご利用は計画的に。

この組み合わせを気付くきっかけとなった書籍『AngularJSリファレンス』は、ほぼ全てのAPIが網羅され、もちろん日本語で、とても読みやすいです。ある程度公式Docを読み込んだつもりでも発見が多かったのでお薦めします。

それでは!

Discussion