angularjswebapi

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

December 16, 2014

6 min read

mitsuruogMitsuru Ogawa

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 ともいう)がいるかが、成功の秘訣のような気がします。