ui-routerにおけるresolveの威力 #AngularJS

  • 95
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

最も多く使われているであろう, AngularJSの拡張コンポーネント, ui-router.
擬似的に画面遷移が必要となるようなアプリケーションをAngularJSで開発する際には、事実上必須といっても過言じゃない.

今回はui-routerに含まれているresolveを活用するお話.

ui-routerとresolve

ui-routerの基本的な使い方を説明したい訳ではないので、そこら辺は割愛して, resolveの使いどころから説明していく.

下記のようなstate定義があったとする.

app/index.js
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は 関数の戻り値に併せて, 自動的に下記を判別した上で挙動を切り替えるように実装されている.

  1. 関数が通常の値をreturnする場合: そのままControllerの引数とする
  2. 関数がPromiseを返却する場合: 非同期の完了を待ってから, then()の引数をControllerの引数とする

従って, 先ほどの例のresolve 部分を単純な配列返しの関数に書き換えても、問題なく動作する.

app/index.js
.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.getDatainvokable に相当するため, 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 が利用可能