Promise の利用

ここまでの情報だけでも Web アプリの開発に取りかかれますが、Promise を使うと、本格的なロジックの実装作業を効率化できます。新規に Web アプリを作成する場合は Promise の利用をお勧めします。

Promise は非同期呼び出しを効率的に実装するための技術です。Kii Cloud SDK では Promises/A+ に基づく実装をサポートしています。

Kii Cloud SDK の API では、Promise を使ってコールバックと同じ機能を実現できます。ここまでの説明では、Kii Cloud の呼び出しをコールバックを使って実装する方法を示しましたが、同じ処理を Promise によって実装できます。

Promise そのものの利用方法は Kii Cloud の機能外であるため、詳細は一般の Web サイトや、専門書籍などをご覧ください。ここでは Promise の概要と Kii Cloud SDK に特有の情報について説明します。

API は基本的にコールバックと Promise の両方に対応しています。Web アプリ全体でどちらかに統一するのが理想ですが、API ごとにコールバックと Promise を使い分けたり、既存の Web アプリを順次 Promise に移行していったりすることもできます。

Promise の概要

Promise を使うと、コールバックを何重にも呼び出すような複雑な処理をシンプルに記述できます。

以下は非同期処理をコールバック関数によって実装する擬似コードです。api1()api2()api3()api4() の順に処理が実行されます。

api1(param1, function(param2) {
  api2(param2, function(param3) {
    api3(param3, function(param4) {
      api4(param4, function(param5) {
        console.log(param5);
    });
  });
});

API を何度も連続して呼び出すと、ネストが深く複雑になり、いわゆる「コールバック地獄」と呼ばれる状況に陥ります。特に、本来、上から下に流れるプログラムの構造がつかみにくくなるのは問題です。コールバック関数の 実行順序 にも示したように、コールバック関数の内側が最後に実行されるため、記述順と実行順が異なり、理解が難しくなります。

Promise を使って同じ処理を書き直したものが以下の擬似コードです。

api1(param1).then(
  function(params) {
    return api2(params);
  }
).then(
  function(params) {
    return api3(params);
  }
).then(
  function(params) {
    return api4(params);
  }
).then(
  function(params) {
    console.log(params);
  }
);

API を連続実行しても、ネストは一定以上に深くならず、上から下に流れるプログラムの構造は保たれます。

これらは説明用の簡単なサンプルですが、実際の実装例を見ると明確です。以下は Kii Cloud の機能を実装した例です。

Promise 版の方がプログラムの構造を理解しやすいことがわかります。

Hello Kii での実装

Hello Kii は Promise を使った実装も用意しています。

Promise 版に切り替えるには、index.html ファイルの以下の箇所を書き換えます。

変更前

<html>
  <head>
    ......
    <script type="text/javascript" src="login-page-callback.js"></script>
    <script type="text/javascript" src="list-page-callback.js"></script>
    ......

変更後

<html>
  <head>
    ......
    <script type="text/javascript" src="login-page-promise.js"></script>
    <script type="text/javascript" src="list-page-promise.js"></script>
    ......

実現している機能は同じであるため、修正後も外部的な動作には変化がありません。

*-callback.js ファイルと *-promise.js ファイルの差分を確認すれば、同じ機能がどのように実装されているかを比較できます。

ログイン API の実装例

ここでは、Promise による実装例として、ログイン API について取り上げます。ログイン API は login-page-callback.js ファイルと login-page-promise.js ファイルの performLogIn() 関数に、次のように実装されています。

コールバック

KiiUser.authenticate(username, password, {
  success: function(theUser) {
    console.log("User authenticated: " + JSON.stringify(theUser));
  },
  failure: function(theUser, errorString) {
    console.log("Unable to authenticate user: " + errorString);
  }
});

Promise

KiiUser.authenticate(username, password).then(
  function(theUser) {
    console.log("User authenticated: " + JSON.stringify(theUser));
  }
).catch(
  function(error) {
    var errorString = error.message;
    console.log("Unable to authenticate user: " + errorString);
  }
);

いずれも、画面から取得したユーザー名とパスワードを使って authenticate() メソッドを呼び出します。

コールバックによる実装では、authenticate() メソッドの引数として、success ブロックと failure ブロックにコールバック関数を指定します。API の完了時にそれらの関数が呼び出されます。

Promise による実装では、API にコールバック関数を直接渡しません。Hello Kii では、API から返る Promise オブジェクトの then() メソッドに成功時に呼ばれる関数を、catch() メソッドに失敗時に呼ばれる関数を指定しています。その他の指定方法については、JSDoc 参照時の注意点 を参照してください。

Promise でもコールバックと同様に、authenticate() メソッドは呼び出されるとすぐに呼び出し元に制御を戻します。authenticate() メソッドの処理が完了すると、then() メソッドまたは catch() メソッドに指定された関数が Promise のライブラリーから呼び出されます。

このセクションではメソッドの呼び出し方の違いについて説明しましたが、Promise の利点はコールバック地獄を避けられる点にあります。Hello Kii の処理を Promise チェーンでつなげる例について、連続実行の実装例 で詳しく説明します。

複数の戻り値の処理

Promise では複数の戻り値を直接受け取れないため、API によっては戻り値の処理に注意が必要です。成功時に複数の戻り値を受け取る場合、Promise では次のような実装になります。

bucket.executeQuery(queryObject).then(
  function(params) {
    var queryPerformed = params[0];
    var result = params[1];
    var nextQuery = params[2];
    console.log("Execute query: got " + result.length + " objects");
  }
);

これはデータ一覧画面で検索クエリーを実行する API の例です。list-page-promise.js ファイルに実装されています。

コールバックでは success ブロックの関数で queryPerformedresultnextQuery を受け取りますが、Promise では then() メソッドの関数で 1 個の params 配列を受け取って、その配列要素から queryPerformedresultnextQuery を取得します。

同様に、クエリーが失敗した場合、コールバックでは failure ブロックの関数で queryPerformederrorString を受け取りますが、Promise では catch() メソッドの関数で error を受け取り、その message プロパティからエラーメッセージを取得します。ログイン API の実装例 のコードを参照してください。

値の取得方法は、API ごとに異なります。戻り値の仕様は、JavaScript リファレンスガイド のサンプルコードの "Promise" タブで確認できます。

API の連続実行

次に、API の連続実行の方法について説明します。Promise を使用する最大の目的は、Promise チェーンによる API の連続実行にあります。

Kii Cloud SDK でネットワークアクセスを行う API は Promise オブジェクトを返します。then() メソッドに指定された関数からさらに Promise オブジェクトを返して、その次の then() メソッドに処理をつなげることができます。

擬似コードを以下に示します。

// Step A
KiiCloudAPI.api1(param).then(
  function(params) {
    // api1 succeeded.
    // Step B
    ......
    return KiiCloudAPI.api2(params);
  }
).then(
  function(params) {
    // api2 succeeded.
    // Step C
    ......
    return KiiCloudAPI.api3(params);
  }
).then(
  function(params) {
    // api3 succeeded.
    // Step D
  }
).catch(
  function(error) {
    // API failed.
    // Step E
    console.log("failed to execute: " + error.message);
  }
);
// Step F

ここでは、ネットワークアクセスを伴う Kii Cloud SDK の API として、KiiCloudAPI.api1()KiiCloudAPI.api2()KiiCloudAPI.api3() の 3 つを呼び出しています。

成功時はソースコード中のコメントのとおり、その直下の then() メソッドの関数が呼び出されます。

失敗時は、その直下の catch() メソッドの関数が呼び出されます。この例では、catch() メソッドが最後に 1 つだけ置かれているため、3 つの API のどれが失敗した場合でも共通のエラー処理を行います。catch() メソッドを使わなければ、非同期 API で発生したエラーは無視されます。

各 API の成功/失敗と、そのときに実行されるブロックをまとめると次のようになります。表の「実行経路」の列は、ソースコード中の // Step A// Step F とコメントされている箇所が実際に実行される順番を表します。コールバックの場合と同様に、// Step AKiiCloudAPI.api1() を呼び出した後は // Step F が実行され、その後で他の関数が上から順に実行される点にご注意ください。

api1() api2() api3() 実行経路
成功 成功 成功 // Step A// Step F// Step B// Step C// Step D
成功 成功 失敗 // Step A// Step F// Step B// Step C// Step E
成功 失敗 // Step A// Step F// Step B// Step E
失敗 // Step A// Step F// Step E

なお、Promise チェーンの処理中にエラーが発生した場合に処理を中止するには、Promise.reject() メソッドを呼び出してエラーにするなどの対処が必要です。特に、Promise チェーンの途中に catch() メソッドを置いた場合、その位置までに発生したエラーはその catch() メソッドで処理できますが、reject() メソッドを呼び出さなければ次の then() メソッドに処理が続きます。reject() メソッドについて詳しくは、Web 上の技術情報をご覧ください。

連続実行の実装例

API の連続実行の実装例として、Hello Kii の Promise 版の実装を書き換えてみます。

ログイン画面の performLogIn() 関数とデータ一覧画面の openListPage() 関数には、それぞれ次のようなコードが含まれています。コメントや解説上余分な処理は省略しています。

ログイン処理

var username = document.getElementById("username-field").value;
var password = document.getElementById("password-field").value;
KiiUser.authenticate(username, password).then(
  function(theUser) {
    console.log("User authenticated: " + JSON.stringify(theUser));
  }
).catch(
  function(error) {
    var errorString = error.message;
    alert("Unable to authenticate user: " + errorString);
  }
);

全件取得処理

var queryObject = KiiQuery.queryWithClause(null);
queryObject.sortByDesc("_created");
var bucket = KiiUser.getCurrentUser().bucketWithName(BUCKET_NAME);
bucket.executeQuery(queryObject).then(
  function(params) {
    var queryPerformed = params[0];
    var result = params[1];
    var nextQuery = params[2];
    console.log("Execute query: got " + result.length + " objects");
).catch(
  function(error) {
    var errorString = error.message;
    alert("Unable to execute query: " + errorString);
  }
);

これら 2 つの処理をつないで、authenticate() メソッドと executeQuery() メソッドを連続で実行するようにします。つまり、"Log In" ボタンがクリックされると、指定されたユーザーでログインし、そのユーザースコープの Bucket である myBucket にある KiiObject の件数をコンソールに出力する処理に変更します。

まず、authenticate() メソッドから返る Promise オブジェクトの then() メソッド(第 1 の then メソッド)内の関数に、全件取得処理のコードを埋め込みます。ただし、executeQuery() メソッドから返る Promise オブジェクトの then() メソッドまで埋め込むとコールバックを使う場合と同様にネストが深くなるため、executeQuery() メソッドから返る Promise オブジェクトを第 1 の then() メソッド内の関数の戻り値とします。

ここまでで以下のようなコードになりますが、まだ不完全な実装です。

var username = document.getElementById("username-field").value;
var password = document.getElementById("password-field").value;
KiiUser.authenticate(username, password).then(
  function(theUser) {
    console.log("User authenticated: " + JSON.stringify(theUser));
    var queryObject = KiiQuery.queryWithClause(null);
    queryObject.sortByDesc("_created");
    var bucket = KiiUser.getCurrentUser().bucketWithName(BUCKET_NAME);
    return bucket.executeQuery(queryObject);
  }
).catch(
  function(error) {
    var errorString = error.message;
    alert("Unable to execute query: " + errorString);
  }
);

次に、第 2 の then() メソッドを追加して、executeQuery() メソッドの成功時に実行したい処理をチェーンします。同時に、catch() メソッドを authenticate() メソッドと executeQuery() メソッドで共通して使えるように、エラーメッセージを書き換えます。

最終的に次のようになります。

var username = document.getElementById("username-field").value;
var password = document.getElementById("password-field").value;
KiiUser.authenticate(username, password).then(
  function(theUser) {
    console.log("User authenticated: " + JSON.stringify(theUser));
    var queryObject = KiiQuery.queryWithClause(null);
    queryObject.sortByDesc("_created");
    var bucket = KiiUser.getCurrentUser().bucketWithName(BUCKET_NAME);
    return bucket.executeQuery(queryObject);
  }
).then(
  function(params) {
    var queryPerformed = params[0];
    var result = params[1];
    var nextQuery = params[2];
    console.log("Execute query: got " + result.length + " objects");
  }
).catch(
  function(error) {
    var errorString = error.message;
    alert("Failed to execute: " + errorString);
  }
);

ポイントは、非同期処理を行う API が返す Promise オブジェクトを then() メソッド内の関数の戻り値として返し、その次の then() メソッドにチェーンするという点です。このようにすれば、複数の API を連続実行する場合でも、サンプルコードの書き換えによって目的の機能を実現できます。

JSDoc 参照時の注意点

JSDoc のサンプルコードは、以下のように then() メソッドの第 2 引数としてエラー処理を記述する形式で書かれています。

// Example to use Promise
KiiUser.authenticate("myusername", "mypassword").then(
  function(theAuthenticatedUser) {
    // Do something with the authenticated user.
  },
  function(error) {
    // Do something with the error response.
  }
);

この形式は、成功時の処理と失敗時の処理を同時に指定している完全な Promise のコードです。

ここまでで見たような Promise チェーンを実現したい場合は、次のように catch() メソッドを使う形に変形するほうが簡単です。

// Example to use Promise
KiiUser.authenticate("myusername", "mypassword").then(
  function(theAuthenticatedUser) {
    // Do something with the authenticated user.
  }
).catch(
  function(error) {
    // Do something with the error response.
  }
);

元の形式では API ごとに失敗処理を記述できます。catch() メソッドを使う場合、catch() メソッドの終了後、次の then() メソッドにチェーンされる点に注意してください。


次は...

チュートリアルの最後に、Kii Cloud を理解するためのヒントとなる情報や、実際のモバイルアプリを作成するときに知っておいた方がよい機能を紹介します。

次のステップへのヒント に移動してください。

より詳しく学びたい方へ