2014-12-16

AngularJSの$resourceの意外なハマりポイント

AngularJSを利用するメリットの1つとして、AngularJSが内包している$resourceを利用することで、バックエンドが提供するREST APIとの対話部分が簡潔に記述できることが挙げられます。

ところが、$resourceの表面的な振る舞いを理解しただけでは、意外なところにハマりポイントがあるものです。今日はその辺りを少し紹介します。

AngularJS Advent Calendar 2014 - Adventar16日目の記事です。

$resourceの使い方

$resourceの細かい内容については本家ドキュメントQiitaでググるといいと思います。

$resourceにURLを渡すだけで以下のような基本的なWebAPIが実行できるようになります。$resourceを使う場合、servicefactoryの中で利用することがほとんどですね。

{ 
  'get':    {method:'GET'},
  'save':   {method:'POST'},
  'query':  {method:'GET', isArray:true},
  'remove': {method:'DELETE'},
  'delete': {method:'DELETE'}
};

また、PUTや異なるエンドポイント(URL)など、先ほどの基本的なAPIをカスタムしたい場合は、actions($resourceに渡す3つめの引数)で使うことで簡単にAPIを追加できるので、使いこなすことが出来るとバックエンドとのWebAPI連携の部分で非常に重宝します。

user.service.js

function User($resource) {
  return $resource('/api/user/:id', {
    id: '@id'
  }, {
    //PUT /api/user/:id
    updata: {
      method: 'PUT'
    },
    //GET /api/user/me
    me: {
      url: '/api/user/me'
    }
  });
});

angular.module('app').factory('User', User);

“$resourceの戻り値は実際の値ではない、参照である。”というハマりポイント

し、しらなかったぜ・・・

結論から言うと、$resourceの戻り値は参照なので、そこから直接プリミティブ型の値を取り出して他で使う場合には、タイミングによってundefinedになったりならなかったりするということです。
(ほとんどの場合、Objectをデータバインドして参照経由で実際の値を見ているので、あんまり問題にならないと思います。)

このハマりポイント、あまり遭遇するケースはないかも知れませんし、原因がわからないままなんとなく回避している人もいるかと思います。私の場合、ui-routerのresolveを使ってcontrollerで必要な情報を取得することが多かったため、よく遭遇していました。

app.route.js

function Router($stateProvider){

  $stateProvider
    .state('main', {
      url: '/',
      templateUrl: 'app/main/main.html',
      controller: 'MainCtrl',
      controllerAs: 'vm',
      // ここでUserリストを事前に取得
      resolve: {
        user: function(User) {
          return User.get({
            id: 1 // ✌(-‿-)✌
          });
        }
      }
    });

}

angular.module('app').config(Router);

main.controller.js

function MainCtrl(user, socket) {
  var vm = this;
  vm.user = user;

  // WebSocketのチャットルームに参加
  // [TODO]タイミングによってuser.roomIdがundefined
  socket.emit('chatRoom:join', {
    id: user.roomId
  });
}

angular.module('app').controller('MainCtrl', MainCtrl);

こちらのStack Overflowを参考にしたのですが、改めて公式ドキュメント読むと書いてありましたね。

It is important to realize that invoking a $resource object method immediately returns an empty reference
($resourceを実行するとね、すぐに空の参照を返すから、心して使え。このボケがぁ!・・・超約)

json - AngularJs using $resource service. Promise is not resolved by GET request - Stack Overflow

$resourceとの正しい(安全な)付き合いかた

より安全なコードの書き方は、$resourceが返すpromiseを、次のように$promiseから取り出して処理すると安全です。

main.controller.js

function MainCtrl(user, socket) {
  var vm = this;

  user.$promise.then(function(user) {
    vm.user = user;
    // WebSocketのチャットルームに参加
    socket.emit('chatroom:join', {
      id: user.roomId
    });
  });
}

angular.module('app').controller('MainCtrl', MainCtrl);

各コントローラでpromiseを処理するのが面倒な場合は、15日目AngularJS - Promiseを使おう - Qiita(@teyosh)で紹介されているような、promiseを処理するためのfactoryを作ってラップするといいと思います。
(たしか、AngularJSアプリケーション開発ガイドのサンプルもそうなってたはず。)

まとめ

今日は$resourceを使うと便利ですが、ちょっとハマるよという話をしました。

AngularJSはフロントエンドの実装を大変楽にしてくれるフレームワークです。しかし同時に、その裏でフレームワークが隠蔽している技術の難しさを軽視することはできないと改めて思い知りました。

今回のようなケースは、Javascriptの実装についてある程度の経験がないとあたりがつけにくい問題だったと思います。(ドキュメント読めは置いといて・・・)
AngularJSに限らず、JSフレームワークを利用する場合は、チームの中に本当のJavascripter(Hackerともいう)がいるかが、成功の秘訣のような気がします。