blog

ウェブデザイナーがはじめる AngularJS シリーズウェブデザイナーがはじめるAngularJS:Promise(Deferred)をつかった非同期処理

    • Ryuichi Nonaka
    この記事は書かれてから1年以上経過しており、内容が古い場合があります。

    はじめに

    デザイナーにとって非同期通信は1つの壁かなと思います。
    自分もそうでしたが非同期処理を理解せずにjQueryでAjaxを行い、別の関数(B)から非同期通信の戻り値にアクセスしようとすると取得できている時とできていない時があるわけですね。非同期なわけですからデータの取得状況にかかわらず関数(B)は実行されるわけです。

    Webサイトにちょっとしたデータを表示するだけであればコールバックで処理するだけで事足りるわけですが、AngularJSで作るような複雑なアプリケーションだとそうもいきません。ログイン認証を済ませてユーザー情報を取得し、ユーザーの権限に合わせて情報を何種類か取得しようとするとコールバックが深くなり(コールバック地獄)、加えて特定の処理だけを行うことができずとても不便です。処理方法としても欠点がありメンテナンス性も悪くなります。

    そこで今回使うPromise(Deferred)の出番です。

    Promise(Deferred)とは

    Promiseはコールバックを連続して使うことによる問題を解決できるデザインパターンです。

    非同期的に動作する関数は、本来返したい戻り値の代わりに『プロミスオブジェクト』という特別なオブジェクトを返しておき、(本来返したい)値を渡せる状態になったら、そのプロミスオブジェクトを通して呼び出し元に値を渡す

    非同期処理とPromiseについては引用元であるこちらの記事非同期処理とPromise(Deferred)を背景から理解しようを参考にどうぞ。

    Promiseパターンを活用することで再利用性が上がりエラー処理も容易になります。 AngularJSの$http等はPromiseパターンを前提に設計されており、使い方さえ分かればとても簡単に非同期処理を行えるようになります。

    Promiseを使っていない失敗例

    Promiseを使わずにFactoryから値を取得する失敗例がこちら。Promiseを使っていないので3秒後に帰ってくる値を取得して処理することができません。ボタン押下時に値を取得できていないと「値が取得できません」と表示されます。

    Promiseを使って非同期処理を作ってみる

    非同期処理を行うには$qサービスを使います。$qサービスを使うことでPromiseオブジェクトを作ることができ、呼び出しもとに値を渡すことができます。非同期処理を管理させるにはFactoryを作り処理させるのが一般的なようです。

    Factoryの簡単なサンプルがこちら。

    var service = angular.module("customServices", []);
    service.factory('myService', ['$q', '$timeout', function($q, $timeout) {
      return {
        data: function(){
          //deferのインスタンスを作る(お呪いのようなもの)
          var d = $q.defer();
    
          $timeout(function(){
            d.resolve("Data:1(3秒後に取得完了)"); //非同期処理完了時の値をresolveメソッドに渡す
          }, 3000);
    
          //プロミスオブジェクトを参照もとに返す
          return d.promise;
        }
      }
    }]);
    

    あとはControllerを作り、上記のPromiseオブジェクトを使ってViewに値を表示します。

    var app = angular.module('myApp', ['customServices']);
    app.controller('myController', ['$scope', 'myService', function($scope, myService){
    
      //dataの取得完了後にthenが実行される
      $scope.get_data = function(){
        // Factoryからデータ取得のメソッドを呼び出し、Promiseオブジェクトを格納する
        promise = myService.data();
    
        //取得中であることを明示
        $scope.message = '取得中';
    
        promise.then(function(strs){
          //成功時
          $scope.message = '取得完了';
          $scope.data = strs;
        });
    
      }
    
      $scope.reset = function(){
        $scope.data = '';
      }
    }]);
    

    ビューはこうなります。

    <h1>Example: Promise</h1>
    <p>ひとつの非同期処理が完了したら値を表示する(3秒かかります)。</p>
    <div ng-app="myApp">
      <div ng-controller="myController">
        <button ng-click="get_data()">データを取得</button>
        <button ng-click="reset()">リセット</button>
        <p>{{ message }}</p>
        <p>{{ data }}</p>
      </div>
    </div>
    

    実際に動作するサンプルがこちら。
    データを取得してから値が表示されるはずです。

    複数の非同期処理が終わった後に処理する

    複数の非同期処理が終わった後に何らかの処理を行いたい場合には$qサービスの.all()メソッドを使います。 引数に複数のPromiseオブジェクトを指定しメソッドチェーンで取得後の処理.then()メソッドを指定します。

    var app = angular.module('myApp', ['customServices']);
    app.controller('myController', ['$scope', '$q', 'myService', function($scope, $q, myService){
    
      $scope.get_all = function(){
        //処理中に表示するテキスト
        $scope.data1 = '取得中';
        $scope.data2 = '取得中';
    
        //$q.allを使って複数のPromiseオブジェクトを取得する
        $q.all([
          myService.data1(),
          myService.data2()
        ]).then(function(strs){
          //すべてのPromiseオブジェクトが取得できたら実行される
          $scope.data1 = strs[0];
          $scope.data2 = strs[1];
        });
      }
    
      $scope.reset = function(){
        $scope.data1 = '';
        $scope.data2 = '';
      }
    }]);
    

    動作サンプルがこちら。

    非同期通信を使ったサンプル

    $httpや$resourceサービスは内部的に$qサービスが使われており、戻り値がPromiseオブジェクトになっています。外部のAPIなどから値を取得する場合、return $http(req);のように直接Promiseオブジェクトを返します。

    var service = angular.module("customServices", []);
    service.factory('myService', ['$q', '$http', function($q, $http) {
      return {
        data: function(){
          //リクエスト内容
          var req = {
            method: 'GET',
            url: 'http://api.nukos.kitchen/index.json',
            responseType: 'json'
          }
    
          //プロミスオブジェクトを返す
          return $http(req);
        }
      }
    }]);
    

    Amazon S3に置いたjsonファイルをCORS(Cross-Origin Resource Sharing)で取得してくるサンプルです。

    エラー時の処理はもう少し複雑になるため、ここでは説明を省きますがとても便利なデザインパターンであることがわかると思います。エラー等の処理についてはまた改めて紹介する予定です。

    以上、Promiseの紹介でした。

    次回:ウェブデザイナーがはじめるAngularJS:$httpProviderのInterceptorsを使ってリクエスト・レスポンスを操作する

    参考

    シリーズ

    1. AngularJSをはじめる前に - AngularJSに関するサイトやスライドまとめ
    2. ウェブデザイナーがはじめるAngularJS:AngularJSをはじめる
    3. ウェブデザイナーがはじめるAngularJS:SPA(Single Page Application)をはじめる前に
    4. ウェブデザイナーがはじめるAngularJS:MiddlemanとBowerで作るAngularJSアプリ開発環境
    5. ウェブデザイナーがはじめるAngularJS:ngRouteを使ったシンプルなViewの切り替え
    6. ウェブデザイナーがはじめるAngularJS:AngularUI Routerの基礎知識
    7. ウェブデザイナーがはじめるAngularJS:ngCookiesやngStorageを使ったCookieやlocalStorageへのアクセス
    8. ウェブデザイナーがはじめるAngularJS:DOM操作系ディレクティブを試す
    9. ウェブデザイナーがはじめるAngularJS:イベント系ディレクティブを試す
    10. ウェブデザイナーがはじめるAngularJS:Promise(Deferred)をつかった非同期処理
    11. ウェブデザイナーがはじめるAngularJS:$httpProviderのInterceptorsを使ってリクエスト・レスポンスを操作する
    12. ウェブデザイナーがはじめるAngularJS:Middlemanでng-annotateを使ったMinify対策
    13. ウェブデザイナーがはじめるAngularJS:コントローラ間の連携
    14. ウェブデザイナーがはじめるAngularJS:AngularJS向けのディレクティブが用意されたUI Bootstrap

    コメント・フィードバック