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').append($.parseHTML(res));
});
});
「読み込み結果の処理」の後にコールバック関数を実行し、DOM 操作を呼び出し側に集約するワケです。
(余談ですが、$(document.createDocumentFragment()) より $('<ul>') の方が 微妙に速そう です。)
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() |
以下のプロパティを持つレスポンス・オブジェクトが引数に与えられる
|
then() を介し*、呼び出し側に値を戻す事が出来る |
success()、error() |
以下のオブジェクトが引数に与えられる
|
呼び出し側に値を戻す事が出来ない |
* 印を成立させるには、次のように 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() にコールバック関数を引き渡す必要がなくなり、呼び出し側は直接、処理結果を受け取る事が出来るようになります。そればかりか、受け取るデータを変えながら次のチェーンにつなげられるというオマケ付きです。
どちらが好み?
jQuery の then() や AngularJS の success()、fail() では jqXHR オブジェクトや展開されたレスポンス・オブジェクトがそのまま引数に渡されるだけで、同じ事は出来ません。
出来る事に違いはないので、どちらを使うかは好みで良いと思いますし、チェーンする機会もあまりないでしょうが、deferred や promise は常にチェーンできるようにしておいた方が良いと思います。まぁ、覚えておいても損しないってことでネ!
※ 今回の場合、factory を使っても書ける し、むしろそう出来る時はそうすべきなのかもしれませんが、練習用として dickeyxxx/angular-boilerplate を参考にしました。