AngularJSでRESTfulクライアントを実装するためのrestangularを使ってみました。

AngularJSでapiクライアントを実装するには$resourceコンポーネントを使用する事が多いと思いますが、restangularはその進化版とも言うべきパッケージです。 restangularの使い方、また$resourceとの比較を簡単に紹介します。

検証に使用したバージョン

  • angualr 1.4
  • angualr-resource 1.4
  • restangular 1.4

restangularの使い方

  • https://github.com/mgonto/restangular

restangular.jsを読み込む

bower,npm,cdnなど、環境に合わせて読み込んでください。

<script type="text/javascript" src="restangular.js"></script>

factoryを定義する

configで処理を共通化する事が多いため、最初からfactoryにしておいたほうが便利です。

“/accounts”にアクセスするrestangularを定義する

var app = angular.module('my-app', ['restangular']);

// インジェクト名は先頭が大文字Rになるので注意
app.factory('Accounts', [Restangular, function (Restangular) {
    return Restangular.withConfig(function (config) {
    
    }).service('accounts');
}])

configに共通の設定を書いていく

app.factory('Accounts', [Restangular, function () {
    return Restangular.withConfig(function (config) {
+        config.setBaseUrl('/api');
+        config.setDefaultRequestParams({key: 'abcdef'});
    }).service('accounts');
})

RestangularProvider

factoryよりさらに上位で設定を共通化する時に使用します。

RestangularProviderに対しての設定は全インスタンスに対して有効です。

app.config(['RestangularProvider', function (RestangularProvider) {
    RestangularProvider.setBaseUrl('http://my-app');
    RestangularProvider.setRequestSuffix('.json');
}])

データを取得

factoryの定義で使ったservice()はapiのエンドポイントとなるURLをセットするメソッドです。

return Restangular.withConfig(function (config) {
    // ...
}).service('accounts');

この場合、Accountsサービスにはエンドポイントとして/accountsがセットされています。one()を使ってエンドポイントを拡張し、get()でデータを取得します。

それぞれのメソッドはpromiseを返却します。

app.controller('MainCtrl', ['$scope', 'Accounts', function ($scope, Accounts) {
    // GET "/accounts"
    Accounts.getList().then(function (accounts) {
        $scope.accounts = accounts;
    });

    // GET "/accounts/123"
    Accounts.one(123).get().then(function (account) {
        $scope.account = account;
    });

    // GET "/accounts/123/members/456"
    Accounts.one(123).one('members', 456).get().then(function (member) {
        $scope.member = member;
    });

    // GET "/accounts?gender=male"
    Accounts.customGET({'gender': 'male'});
}])

データを更新

返却されたリソースに対してput postなどの操作を行います。

    // PUT "/accounts/123"
    Accounts.one(123).get().then(function (account) {
        $scope.account = account;
        $scope.account.name = 'user2';
        $scope.account.put();
    });

    // POST "/accounts/"
    Accounts.post({name: 'new account'});
    
    // $objectを参照すると、空の配列が返却される(時間が経過するとサーバーから返却された値で埋まる)
    // テンプレートとして使用し、続けてput/postなどの操作を行うことができる
    $scope.accounts = Accounts.getList().$object;
    // ...
    $scope.accounts.post({name: 'new account'});

使ってみた感想

エンドポイントを設定する事で、その後はget() put()を呼び出すだけです。

restangularがURLを生成してくれるため、コード量がとても少なくて済みます。

$resourceの使い方をおさらい

restangularのような外部ライブラリを使わなくても、AngularJSにはもともと$resourceというサービスが用意されています。 これもapiクライアントを実装するためのものなので、最初は外部ライブラリを使わずに$resourceで実装するケースが多いのではないでしょうか。

$resourceを知るとresgangularの特徴がより分かりやすくなるので、まず$resourceの使い方をおさらいしてみましょう。

  • https://docs.angularjs.org/api/ngResource
  • https://docs.angularjs.org/api/ngResource/service/$resource

angular-resource.jsを読み込む

<script type="text/javascript" src="angular-resource.js"></script>

ngResourceモジュールを読み込む

var app = angular.module('my-app', ['ngResource']);

factoryを定義する

// アクション名・メソッド・URLをセットにして定義する
app.factory('Account', ['$resource', function ($resource) {
   return $resource('/api/accounts', {id: '@id'}, {
        get: {method: 'GET'},
        query: {method: 'GET', isArray: true},
        save: {method: 'POST'},
        update: {method: 'PUT'},
        custom: {method: 'GET', url: '/api/accounts/custom/:id'}
    });
}])

データを取得

$resourceのメソッドもpromiseオブジェクトを返却します。

app.controller('MainCtrl', ['$scope', 'Account', function ($scope, Account) {
    Account.get({id:123}).$promise.then(function(account) {
        $scope.account = account;
    });
}])

データを更新

返却されたリソースは$resourceのインスタンスです。

メソッド名に$を付けることで、インスタンスに対しての操作を呼び出すことができます。

// PUT "/accounts/123"
$scope.account.name = 'user2';
$scope.account.$update();

インスタンスの操作でない時は、$を付けずfactory経由で呼び出します。

// POST "/accounts"
Account.save({name: 'new account'});

restangular,$resourceの比較

##restangularが使いにくいと感じたこと

  • RESTfulでないAPIには対応しづらい

restangularのほうが使いやすいと感じたこと

  • isArrayの定義をしなくてもいい
  • トランスフォームが巨大にならない
  • リソースはレスポンスの参照ではない

1-1. RESTfulでないAPIには対応しづらい

$resource

良し悪しは別として、強引なURLにもアクセスできます。

$resource('', {'id': '@id'}, {
    get: {
        url: '/api/accounts/:id'
    },
    getFromCustomId: {
        url: '/api/custom/:id'
    }
});

restangular

取得したデータに対する操作がそのままURLになります。 API側がRESTfulでない場合、restangularの生成するURLと実際のURLが合わないことがあります。

// GET /accounts/123
Restangular.one('accounts', 123).get().then(function (account) {
    account.put();   // PUT /accounts/123
    account.patch(); // PATCH /accounts/123
});

// GET /accounts
Restangular.all('accounts').getList().then(function (accounts) {
    accounts[0].put();           // PUT /accounts/123
    accounts.post({name:'new'}); // POST /accounts
});

2-1. isArrayの定義をしなくてもいい

$resource

isArrayをつい忘れがちでExpected response to contain an object but got an arrayを目にするたびに書き足していました。

また(特異なケースかもしれませんが)、とあるエラーの時だけオブジェクトを返したかったのですがarrayに限定されてしまうのが使いづらいなと感じることがありました。

$resource('', {}, {
    custom: {
        url: '/api/custom/',
        isArray: true  // <-- つい忘れる
    },

restangular

配列かどうかは特に意識しません。

Account.getList();     // ---> [{id:1}, {id:2} ...]
Account.one(1).get();  // ---> {id: 1}

2-2. トランスフォームが巨大にならない

$resource

トランスフォームはリクエスト・レスポンスの時にデータを変換する処理です。

とても便利なので多用していましたが、AngularJS標準のトランスフォームに追加するためコード量が増えてゴチャゴチャするのが難点でした。

$resource('/api/accounts', {}, {
    get: {
        transformResponse: function ($http) {
            var defaults = $http.defaults.transformResponse;
            var transform = function (data) { ... };

            if (angular.isArray(defaults)) {
                return defaults.concat(transform);
            } else {
                return [defaults, transform];
            }
        },
        transformRequest: function ($http) {
            var defaults = $http.defaults.transformRequest;
            ...
        }
    },

ステータスコード判定を入れると、ますます巨大になります。

$resource('/api/accounts', {}, {
    get: {
        transformResponse: function ($http) {
+            var transform = function (data, header, status) {
+                if (status == 200) {
+                } else if (status == 204) {
+                } else if (status == 400) {
+                }
+            };
           // ...
        }
    },

https://docs.angularjs.org/api/ng/service/$http

$http.intercaptorを使うとアプリケーション全体で処理を共通化することができます。 HTTP 403 Forbiddenの場合は別ページにリダイレクト、などの処理だったらこちらのほうが便利です。

restangular

interceptor(リクエスト/レスポンスに対する共通の処理)・transformer(コンテンツの変換)の2つに別れます。 条件判定を入れればいいだけなので、かなりスッキリします。

config.addResponseInterceptor(function (data, operation) {
    var extracted = data;

    if (data.status === 200 && operation === 'get') {
        extracted = data.data;
        extracted.meta = extracted.data.detail;
    }
    return extracted;
})

2-3. リソースはレスポンスの参照ではない

$resource

$resourceはレスポンスデータの参照のため、APIの作り方に左右されます。

// このようなレスポンスが返るとする
// /api/accounts/123 ---> { id: 123, name:'user' }
// /api/accounts/123/status ---> { status: 'success' }

Account.get({id:123}).$promise.then(function (response) {
    $scope.account = response;
    $scope.account.$status();
});

// $status()のレスポンスが返った後の状態、idとnameがなくなってしまう
console.log($scope.account);
// ---> { status: 'success' }

検証コード

回避方法1:APIで完全なデータを返す

/api/accounts/123/status
  ---> { id: 123, name:'user', status: 'success' } を返すようにする

検証コード

回避方法2:factory経由でメソッドを呼ぶ

検証コード

- $scope.account.$status();

+ // factory経由で呼び出す方法に変える
+ Account.status({id:123}).$promise.then(function (response) {
+     $scope.account.status = response.status;
+ });

restangular

$resourceはURLが示すリソース(資源)そのものを扱うのに対し、restangularはリソース(資源)にアクセスするためのメソッドを提供してくれます。 レスポンスをどのようにオブジェクトに保存するかは自由です。

Account.one(123).get().then(function (account) {
  $scope.account = account;

  $scope.account.one('status').get().then(function (data) {
      $scope.account.status = data.status;
  });
});

結論

  導入しやすさ ドキュメントの読みやすさ サンプルの多さ コードのシンプルさ
$resource :star: :star: :star::star::star: :star:
restangular :star: :star: :star: :star::star::star::star::star::star:

だいぶrestangular寄りな判定ですが、使い始めた当日は結構つまづきました。 原因はAPI側がRESTfulになっておらず、restangularの推測するURLとズレが生じて思うようにapiまで辿りつけなかったことです。

機能的には$resourceもrestangularも大きな違いはありませんでしたが、コード量がかなり減らせること、またRESTfulなアプリケーションを構築するという意味で導入してみるのもいいかもしれないですね。