kintoneのJavaScriptでつかう非同期処理Promiseの勘所

概要

kintone.Promiseの解説です。

kintone.apiは全て非同期実行です。よって例えば、レコード保存時、他のアプリのレコードの検索結果を用いて、入力のエラーをチェックしたいといった場合、そのまま処理を書いてもうまく動きません。

うまく動かない例

「他のアプリのレコードを検索」部分が非同期で動作するため、検索完了前にエラーチェックのプログラムに処理が進んでしまいます。

// 他のアプリのレコードを検索
kintone.api(kintone.api.url('/k/v1/records'), "GET", app: XX, query: XX, fields: XX },
 function (resp) {
 return;
 }, function (err) {
 return;
 });

// 上の処理を待たずに、こっちに来ちゃう。
// エラーチェックしてイベントを返す
if (isErr) {
 event.error = 'ERROR!';
}
return event;

こんな時、JavaScriptでよくあるのは、コールバック関数を使う方法です。

// 他のアプリのレコードを検索
kintone.api(kintone.api.url('/k/v1/records'), "GET", app: XX, query: XX, fields: XX },
 function (resp) { // 処理成功時に実行される関数

// ここに何らかのエラーチェックを書く

if (isErr) {
 event.error = 'ERROR!';
 }
 return event;
 }, function (err) { // 処理失敗時に実行される関数
 return;
 });

return event;

この書き方には2つ問題があります。

  1. 最下行のreturn event;でeventが返されてしまうので、結局エラーチェックが効かない。
  2. 仮に↑の問題が解決したとしても、最初の検索結果Aを用いてBを検索して、その検索結果Cを用いて・・・
    というような複雑な処理になってくると、関数の入れ子が深くなりとても読みづらい。

そこで登場するのがJavaScriptのPromiseという仕様です。

が、JavaScriptのPromiseはブラウザによって実装されていないケースがあります(主にIE)。これをkintoneの独自の拡張により、主要なブラウザでkintone上でPromiseを使えるようにしたのが、kintone.Promiseです。

以降、kintone.Promiseについて、幾つかの段階に分けて、使い方を解説します。

kintone.Promise

Lv1. return eventの代わり

kintone開発者サイトに出ている、もっとも基本的な使い方です。

このように使います。


// レコード追加画面の保存前処理
kintone.events.on('app.record.create.submit', function(event) {
  var record = event.record;
  var params = {
    'app': kintone.app.getId(),
    'query': 'limit 1',
    'fields': ['$id']
  };

  return new kintone.Promise(function(resolve, reject) {
    kintone.api('/k/v1/records', 'GET', params, // *1
    function(resp) { // *2
      resolve(event); // *3
    }, function() {
      reject(event);
    });
  });
});

ポイントを解説します。

return new kintone.Promise

イベントの戻り値に、kintone.Promiseのオブジェクトを指定します。すると、kintoneのフレームワーク側で、このPromise内にある関数の実行を約束してくれます。つまり、プログラムは次のような流れで実行されます。

  1. kintone.api … *1
  2. function(resp) { … *2
  3. resolve(event); … *3
  4. resolve(event)にあるeventが上位にreturnされる。

これで、apiをリクエストしたとき、そのレスポンスを待つ前にeventがreturnされてしまう、ということがなくなります。eventがresolve()というメソッドで囲われていますが、ひとまずこういう書き方をしないといけないのだと思っておいてください。

Lv2. 複数のapi結果を待つ

Lv1.では一つのapiリクエストのレスポンスを待って、その結果を受けてeventをreturnするケースを示しました。これが複数ある時はどうすれば良いでしょうか。たとえば、サブテーブル内のあるフィールドについて、apiリクエストが必要なエラーチェックをしなければならないようなときは、別のアプローチが必要になります。実装例を見てみましょう。


let createUpdateEvents = ["app.record.create.submit", "app.record.edit.submit", "app.record.index.edit.submit"];
kintone.events.on(createUpdateEvents, function (event) {
    let promiseAry = [];
    let errMsgAry = [];

    // サブテーブルに配置した「担当者」フィールド(ユーザ選択)に
    // 指定したユーザ(必ず1名)が担当者グループに所属しているかをチェックするPromise
    for (let i = 0; i < subTable.length; i++) { let staffField = subTable[i].value['担当者']; if (staffField.value.length > 0) {
        let checkStaff = new kintone.Promise(function (resolve, reject) {
            kintone.api('/v1/user/groups', 'GET', { code: staffField.value[0].code },
                function (resp) {
                    if (/*所属チェック*/) {
                        staffField.error = '担当者グループに所属するユーザーを選択してください';
                        errMsgAry.push('エラー発生');
                        reject();
                    }
                    resolve();
                }, function (resp) {
                    errMsgAry.push(resp.message);
                    reject();
                }
            );
        });
        promiseAry.push(checkStaff);
    }
}

// 生成したPromiseを全て待つ
return kintone.Promise.all(promiseAry)
    .then(function () {
        /* 必要な処理実施 */
        return event;
    }).catch (function () {
        event.error = errMsgAry;
        return event;
    });
});

ポイントを解説します。

return kintone.Promise.all(promiseAry)

複数のPromiseについて、全ての完了を待つメソッドが、kintone.Promise.all()です。引数にはPromiseの配列を指定します。配列の中身は、サンプルプログラムの前半のfor文で、サブテーブルの行数に応じて動的にnewしています。

resolve(), reject()

kintone.apiのコールバック関数内に、以下のような2つの記述があります。


resolve();
reject();

これはそれぞれ、生成したPromiseが正常終了したこと、異常終了したことを示しています。
Promise内ではreturnを使わず、必ず関数の最後にresolveもしくはrejectを記載しなければいけません。

.then(), .catch()

kintone.Promise.allの先にある.then(), .catch()には、Promiseの処理結果に対応した処理を記述することができます。promiseAryにあるPromiseのうち、全てがresolve()だったら、then()に処理が移ります。一方、一つでもreject()があったら、catch()に処理が移ります。

このように、.then(・・・).catch(・・・)と言うようにメソッドをつなげていく形を、メソッドチェーンと呼びます。また、Promiseで使われるメソッドチェーンのことを、プロミスチェーンと呼んだりもします。

Lv.3 複数の処理をつなげる

この記事の最初に挙げた、

最初の検索結果Aを用いてBを検索して、その検索結果Cを用いて・・・というような複雑な処理になってくると、関数の入れ子が深くなりとても読みづらい。

という問題の解決策を提示します。

Lv.2 で解説した”.then()”は、つなげることができます。


// チェック1→チェック2→更新処理
return checkUniqueProcess([from.year(), from.month() + 1, event])
.then(checkProcess1)
.then(checkProcess2)
.then(function () {
    /* 更新処理 */
    console.log('プロセス実行完了(正常)');
    return event;
})
.catch(function (error_msg) {
    console.log('プロセス実行完了(エラー)');
    console.log(error_msg);
    event.error = error_msg;
    return event;
});

こちらのサイトの解説がシンプルかつ秀逸です。

promiseの.then()には、手前の処理の結果を.then()に渡すことができます。単一の値であれば、return result; を、.then(result)で受ければ良いです。複数の値の時はこれを配列にします。


new kintone.Promise(function (resolve, reject) {
    return [result1, result2];
})
.then(function (result_ary) {
    result1 = result_ary[0];
    result2 = result_ary[1];
    // 必要な処理。。。。
});

Lv.4 kintone.Promiseをreturnできないイベント

公式サイトのkintoneイベントに関する記載を引用します。

下記のイベントのハンドラー内でkintone.Promiseオブジェクトをreturnすると、非同期処理の実行を待ってイベントの処理を開始します。
レコード追加画面の保存実行前イベント
レコード編集画面の保存実行前イベント
レコード詳細画面の削除前イベント
プロセス管理のアクション実行イベント
レコード一覧画面の「保存ボタン」クリック時イベント
レコード一覧画面のレコード削除前イベント
レコード追加画面の保存成功後イベント
レコード編集画面の保存成功後イベント
レコード一覧画面のインライン編集の保存成功後イベント

つまり上記以外のイベントでは、kintone.Promiseオブジェクトをreturnできません。

kintone.Promiseオブジェクトをreturnできないが、apiの結果をつかってページの内容を書き換えたいような時には、次のような方針の実装を検討します。

  1. kintone.Promiseを単独で用いる。(returnより手前で書く)
  2. thenやapiコールバック関数内で、eventを使わず、kintone.app.record.get();kintone.app.record.set();で、画面上のフィールド値の更新を試みる。

2.の方針は結構大切です。プログラム例を以下に示します。


var changeUserEvents = ['app.record.create.change.kintoneユーザー', 'app.record.edit.change.kintoneユーザー'];
kintone.events.on(changeUserEvents, function (event) {
    var kintoneUser = event.record['kintoneユーザー'].value;
    event.record['ログイン名'].value = kintoneUser[0].code;

    kintone.api(kintone.api.url('/v1/users'), "GET", { codes: [kintoneUser[0].code] },
        function (resp) {
            let u = resp.users[0];
            let rec = kintone.app.record.get();
            rec.record['姓'].value = u.surName;
            rec.record['名'].value = u.givenName;
            rec.record['ふりがな姓'].value = u.surNameReading;
            rec.record['ふりがな名'].value = u.givenNameReading;
            kintone.app.record.set(rec);

        }, function (err) {
            console.log('ユーザ情報の取得に失敗しました');
            return;
        });
    return;
}, function (err) {
    console.log('ユーザ情報の取得に失敗しました');
    return;
});

なお、イベントによってはkintone.app.record.get()がnullを返すことがあります。そうすると、もうお手上げです。nullを返すことが確認済みであるイベントとして、

  • app.record.index.edit.change

があります。一覧画面でのレコード編集にて、カスタマイズを実装したい場合は注意が必要です。