jQueryとAngularJSにおけるAjaxの微妙な違い

AngularJS の勉強、始めました。

最初はそのプログラミングに関する独特のお約束事項にイラッとしましたが、キモであろう DI を「疎な関係のクラスを(実行時に)結びつけるのに必要な仕組み」と割り切り、DI – 猿でも分かる! Dependency Injection: 依存性の注入 で引用されている ITpro 記事クラス図 をコードから想像できるようになったところで、それなりに面白くなってきました。

何より「jQuery が要らなくなる!」のがとっても快感なんです 😉 (つらい時もあるけど…)。

さて今回は、jQuery まみれのページを AngularJS で書き換えた時に気付いた Ajax の動作 − 非同期通信の戻り値をいつどこで DOM に反映するか − に関する話題を書いてみます。

jQueryの場合

検索のクエリを渡すと Github のリポジトリを返す関数を例にとってみます。

function get_repos(queries) {
    var url = '...';
    queries = encodeURIComponent(queries.replace(/\s+/g, ' '));

    $.ajax({
        url : url + queries,
        type: 'GET'
    })

    .done(function (data, textStatus, jqXHR) {
        // 読み込み結果の処理
        ;
    })

    .fail(function (jqXHR, textStatus, errorThrown) {
        // エラー処理
        ;
    });
}

私がよくやりがちなのが、「読み込み結果の処理」の所で DOM をゴニョゴニョとやる事です。当然「Github リポジトリ検索」をビューから切り離せなくなり、サービスとして独立させる事が出来ません。

ではどうするか? async:false とは何か。或いは、非同期処理を諦めるのはまだ早い! に紹介されているようにオブザーバー・パターンを使うのも手ですが、ここでは簡単に次のようコールバックを使うことにします。

function get_repos(queries, callback) {
    ...// コールバック関数を指定 ↑

    .done(function (data, textStatus, jqXHR) {
        var res = ...; // 読み込み結果の処理
        callback(res);
    })

    .fail(function (jqXHR, textStatus, errorThrown) {
        // エラー処理
        ;
    });
}

これに対する呼び出し側は例えば次の通りです(XSS 対策は文脈に合わせてネ)。

$('#submit').on('click', function () {
    get_repos($('#queries').val(), function (res) {
        $('#repos').html($.parseHTML(res));
    });
});

「読み込み結果の処理」は目的とするデータの抽出に専念し、DOM 操作を呼び出し側に集約するワケですネ。

(余談ですが、$(document.createDocumentFragment()) より $('<ul>') の方が 微妙に速そう です。 最後に一発 $.html() が良さげでした。)

AngularJSの場合

サービス部分は $http サービスを使えば、ほぼ jQuery と同様に出来ます(他にも get_users() とかを追加していく想定です)。

angular.module('myServices', [])
.service('github', ['$http', function ($http) {
    this.get_repos = function (queries, callback) {
        var url = '...';
        queries = encodeURIComponent(queries.replace(/\s+/g, ' '));

        $http({
            url: url + queries,
            method: 'GET'
        })

        .success(function (data, status, headers, config) {
            var res = ...; // 読み込み結果の処理
            callback(res);
        })

        .error(function (data, status, headers, config) {
            // エラー処理
            ;
        });
    };
}]);

ちなみにコントローラー部分は次のようになります。

angular.module("myApp", ['myServices'])
.controller('myCtrl', ['$scope', 'github', function ($scope, github) {
    $scope.submit = function () {
        github.get_repos($scope.queries, function (res) {
            $scope.repos = res.items;
        });
    };
}]);

AngularJS、もう1つのパターン

Angular には、$http 関数の成功/失敗を処理する関数 success()error() 以外に、もう1つ then() という関数があります(というか、こちらの方が古くてプリミティブ、たぶん)。両者の違いは次の通りです。

コールバック関数 コールバック関数への引数 コールバック関数の戻り値
then() 以下のプロパティを持つレスポンス・オブジェクトが引数に与えられる

  • data: レスポンスのボディ部分
  • status: レスポンスの HTTP ステータス・コード
  • headers: ヘッダーを取得する関数
  • config: リクエスト時の設定を格納したオブジェクト
  • statusText: レスポンスの HTTP ステータス・テキスト
then() を介し*、呼び出し側に値を戻す事が出来る
success()error() 以下のオブジェクトが引数に与えられる

  • data: レスポンスのボディ部分
  • status: レスポンスの HTTP ステータス・コード
  • headers: ヘッダーを取得する関数
  • config: リクエスト時の設定を格納したオブジェクト
呼び出し側に値を戻す事が出来ない

* 印を成立させるには、次のように get_repos() から then() を介して $http の戻り値である Promise オブジェクト を返してやるのがミソです。

.service('github', ['$http', function ($http) {
    this.get_repos = function (queries) {
        ...
        // ↓ この return がミソ
        return $http({
            url: url + queries,
            method: 'GET'
        })

        .then(
            function (response) {
                return response.data;
            },
            function (response) {
                alert(response.data.message);
                return {items: []};
            }
        );
    };
}]);

こうすると、サービス関数 get_repos() にコールバック関数を引き渡す必要がなくなり、呼び出し側は then() を介して目的のデータを受け取れるようになります。そればかりか、受け取るデータを変えながら次のチェーンにつなげられるというオマケ付きです。

どちらが好み?

jQuery の then() や AngularJS の success()error() では jqXHR オブジェクトや展開されたレスポンス・オブジェクトがそのまま引数に渡されるだけで、同じ事は出来ません。

出来る事に違いはないので、どちらを使うかは好みで良いと思いますし、チェーンする機会もあまりないでしょう。しかしどのパターンにおいても deferredpromise は常にチェーンできるようにしておいた方が良いと思います。まぁ、覚えておいても損しないってことでネ!

※ 今回の場合、factory を使っても書ける し、単純さが変わらないのならそうする より単純な方を選ぶべきなのかもしれませんが、練習用として dickeyxxx/angular-boilerplate を参考にしました。

2015年1月13日 追記

正確性を期すためとは思いますが、 promise オブジェクトの元である $q日本語訳 が微妙にわかりずらい(失礼ッ、もちろん翻訳の労には感謝の意と敬意を表します)ので、私なりの注釈をつけた意訳を載せておきます。また promise の概念については、2011年の拙作「jQueryのDeferredとPromiseで応答性の良いアプリを-基本編」を参考にしてください。

The Promise API

deferred のインスタンスが作成されると、新しい promise のインスタンスが作成され、(注:deferred オブジェクトのプロパティとして) deferred.promise で参照できるようになります。

promise オブジェクトの目的は、延期されていたタスク(deferred task)が実行を完了した時、関係者がその結果にアクセス出来るようにすることにあります。

メソッド

then(successCallback, errorCallback, notifyCallback) – promise が解決されたか(resolved)あるいは否決されたか(rejected)に関わらず、 then は結果が得られるとすぐに成功または失敗のコールバックを非同期に呼び出します。その際コールバックには、ただ1つの引数: 解決の結果または否決の理由 が渡されます(注:各コールバックに結果を引き渡せるということです)。

加えて promise が解決または否決されるまで、その進行を示すために notify コールバックが0回以上呼び出されます。

このメソッドは successCallback または errorCallback の戻り値を介して、解決または否決された(状態の)新しい promise を返します(注:結果を引き渡しながらチェーンできるということです)。また notifyCallback メソッドの戻り値を介しても通知されます(注:notifyCallback が呼び出された時もチェーンされるということです)。ただし notifyCallback メソッドからは promise を解決または否決(の状態に)することはできません。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

海外からの投稿は受け付けていません