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 | ||||
restangular |
だいぶrestangular寄りな判定ですが、使い始めた当日は結構つまづきました。 原因はAPI側がRESTfulになっておらず、restangularの推測するURLとズレが生じて思うようにapiまで辿りつけなかったことです。
機能的には$resource
もrestangularも大きな違いはありませんでしたが、コード量がかなり減らせること、またRESTfulなアプリケーションを構築するという意味で導入してみるのもいいかもしれないですね。