kintone.Promiseを紐解く

昨年2015年7月のアップデートで追加されたkintone.Promiseですが、kintone JavaScript APIとして搭載されたもので、kintoneで使いこなすために様々な工夫がなされています。kintone.Promiseへの理解を深め、使い方のコツに迫るために、少しまとめ直してみたいと思います。

導入背景

まず導入背景ですが、これはcybozu.com developer networkTipskintone API で Promise を使ってみよう!」でサイボウズの北川さんも触れられていますが、大きく次の3点だと考えます。

(1)いわゆるXHRによる同期リクエストが非推奨(Firefox等ではコンソールに警告が出る)
(2)IEではnative Promiseがサポートされていない(Can I use
(3)submit(レコード保存前)イベントで、レコード保存に合わせて何らかの非同期処理(例えば、kintone.api()やkintone.proxy()でAPIをコールする)を実行したくても、submitイベントは、これらの実行完了を待つことを担保する機構がなかった

ここで、(1)、(2)と(3)は大きく趣きが違うことに気づきます。

前者は、非同期処理を同期処理のように扱うという観点で見ると、native Promiseとサポートブラウザの問題ですので、使用ブラウザの限定、jQuery.deffered等の代替が実は元々ありました。

しかし、後者はsubmitイベント中で非同期処理の終了を担保するというkintone JavaScript APIの話ですので、kintoneそのものの対応が必要です。

これだけ聞くと、(3)のために実装されたように感じられるkintone.Promiseですが、これをkintone.Promiseの機能として(1)、(2)と統合的に解決しているところにその深さがあると思っています。

native Promiseとkintone.Promiseの関係

現在ではkintoneのサポート対象外となったバージョンのIE(IE自体native Promiseはサポートされていません)でもkintone.Promiseはリリース当時動作するよう実装されていました(今後は何とも言いがたいところでしょう)。他方、ブラウザでソースを確認するとes6-promise.min.js」が取り込まれており、kintone.Promise周辺をデバッグしているとここに飛ばされるので、kintone.Promiseはnative Promise(ES6 Promise)のPolyfillベースでIEでも動くよう実装されているものと考えられます。恐らく、kintone.Promiseはnative PromiseとこのPolyfillのハイブリッドな継承のような感じで実装されているものと予想しています。

スクリーンショット_2016-02-08_9_55_45

概して、native Promiseが理解できれば、そのままkintone.Promiseが理解できると言って良さそうです。理解する方法の視点でいうと、native Promise理解のための教材(例えば「JavaScript Promiseの本」)がkintone.Promiseの理解にそのまま使えるということになります。kintone.Promiseリリース当初、サイボウズの中の方のスライドで「JavaScript Promiseの本」が参考で引用されていたのを多数見かけましたが、これは、「native Promise」、「es6-promise.min.js(Polyfill)」ベースであることのひとつの裏付けでもあると思われます。

kintone.Promiseがサポートされたイベント

リリース当初「kintone.Promiseがサポートされたイベント」というくくりで整理された
・app.record.create.submit
・app.record.edit.submit
・app.record.index.edit.submit
・app.record.index.delete.submit
・app.record.detail.process.proceed
・app.record.detail.delete.submit
ですが、サポートという言葉遣いがやや曖昧です。

returnできるsubmit/proceedイベント

きちんと読めば書いてあるのですが、具体的には「Promiseオブジェクトをreturnすることで、Promise(中の非同期処理)の解決(resolve)後に、イベントが実行される」ということです。また、本質的には「非同期処理の完了を待って、return eventできる」という言い方もできます。これらのイベント内だけでしかkintone.Promiseを用いた記述ができないという意味ではありません。

今回は、ユーザー選択フィールドにセットしたユーザーが他のレコードでセット済みでないかの重複チェックするサンプルを見てみましょう(ユーザー選択フィールドは選択系フィールドということもあって標準機能に重複チェックはありません)。マスタアプリ等で効果を発揮するカスタマイズ例です。

(function() {
  "use strict";

  kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], function(event) {
    var record = event.record;
    var key_field = 'ユーザー選択';
    var own_id = kintone.app.record.getId(); // 新規でnull、編集時には値がセットされる
    var query = '';

    // ユーザー選択フィールドが空の場合にはここで、return
    if (record[key_field].value.length === 0) {
      return event;
    }

    // key_fieldがユーザー選択フィールドの時のquery
    switch (record[key_field].type) {
      case 'USER_SELECT':
        for (var i = 0; i < record[key_field].value.length; i++) {
          query += key_field + ' in ("' + record[key_field].value[i].code + '") or ';
        }
        query = query.slice(0, -3);
        break;
    }

    // 新規登録(null)と編集(自レコード除外)に対応
    if (own_id) {
      query = query + ' and レコード番号 != ' + own_id;
    }

    // 検索対象のレコードは0または1個なので、limit 1で絞り込み
    query = query + ' limit 1';

    // 重複チェック
    return new kintone.Promise(function(resolve, reject) {
      kintone.api(kintone.api.url('/k/v1/records', true), 'GET', {
        app: kintone.app.getId(),
        query: query
      }, function(resp) {
        // レコードの重複をフィールドエラーにセット
        if (resp.records.length === 1) {
          record[key_field].error = '値がほかのレコードと重複しています。';
          resolve();
          return;
        } else { // 検索レコードが1個でない時には今回rejectしておく
          reject('既存レコードが' + resp.records.length + '件でした。');
          return;
        }
      }, function(err) {
        reject('既存レコードの取得に失敗しました。');
        return;
      });
    }).then(function(success) {
      return event;
    }).catch(function(error) {
      event.error = error;
      return event;
    });
  });

})();

保存前イベントで、次のようにフィールドエラーがセットされます。
スクリーンショット 2016-02-08 15.10.43

submit/proceedイベント以外のイベントでkintone.Promiseを使う

では、これら以外のイベントでkintone.Promiseを使うには?ということですが、言われた通りkintone.Promiseオブジェクトをreturnしなければ良いだけです。

ここでも、ユーザー選択フィールドの重複チェックをchangeイベントでやってみるサンプルを見てみたいと思います。kintone.Promiseオブジェクトをreturnしていないのと、return eventの代わりに kintone.app.record.set() を利用しているところに注目してください(実はこの例では、kintone.api()のコールバック中でkintone.app.record.set()すれば良いのですが)。

(function() {
  "use strict";

  kintone.events.on(['app.record.create.change.ユーザー選択', 'app.record.edit.change.ユーザー選択'], function(event) {
    var record = event.record;
    var key_field = 'ユーザー選択';
    var own_id = kintone.app.record.getId(); // 新規でnull、編集時には値がセットされる
    var query = '';

    // ユーザー選択フィールドが空の場合にはここで、return
    if(record[key_field].value.length === 0 ){
      return event;
    }

    // key_fieldがユーザー選択フィールドの時のquery
    switch (record[key_field].type) {
      case 'USER_SELECT':
        for (var i = 0; i < record[key_field].value.length; i++) {
          query += key_field + ' in ("' + record[key_field].value[i].code + '") or ';
        }
        query = query.slice(0, -3);
        break;
    }

    // 新規登録(null)と編集(自レコード除外)に対応
    if (own_id) {
      query = query + ' and レコード番号 != ' + own_id;
    }

    // 検索対象のレコードは0または1個なので、limit 1で絞り込み
    query = query + ' limit 1';

    // 重複チェック
    new kintone.Promise(function(resolve, reject) {
      kintone.api(kintone.api.url('/k/v1/records', true), 'GET', {
        app: kintone.app.getId(),
        query: query
      }, function(resp) {
        // レコードの重複をフィールドエラーにセット
        if (resp.records.length === 1) {
          resolve();
          return;
        } else { // 検索レコードが1個でない時には今回rejectしておく
          reject('既存レコードが' + resp.records.length + '件でした。');
          return;
        }
      }, function(err) {
        reject('既存レコードの取得に失敗しました。');
        return;
      });
    }).then(function(success) {
      var obj = kintone.app.record.get();
      obj.record[key_field].error = '値がほかのレコードと重複しています。';
      kintone.app.record.set(obj);
    }).catch(function(error) {
      console.log(err);
    });
  });

})();

実行結果は、変更時にエラーがセットされるという点で見た目は同じになりますので、スクショは省略します。なお、ここでひとつ押さえておきたいのは、kintone.Promiseオブジェクトをreturnするとどうなるかですが、次のようなエラーを返します。
スクリーンショット 2016-02-08 15.02.47
「changeイベントではThenableオブジェクトのreturnが許容されていない」とのことで、仕様でうたわれている通りであることが確認できます。ちなみに、Thenableオブジェクトとは、thenをチェーンできるオブジェクトということで、Promiseオブジェクトの通称です。

submit/proceedイベントだけreturnできる理由

ここで考えてみるとちょっと面白いのが、returnできるイベントが現状これらだけである理由です。submitやproceedのイベントが優先されて、他のshowやchangeのイベントが現状returnできないのは「画面(状態)遷移やレコード操作に与える影響の有無」「代替方法の有無」の差だと考えられます。

submit/proceedイベントは導入背景の(3)で述べたように、イベントの実行が非同期処理の終了を担保できていないと、レコード操作への影響は大きいですし、代替方法もないため、どうしようもありません。

他方、show/changeのイベントは、イベント発動時に画面(状態)遷移も伴いませんし、現状でも非同期処理後の結果をレコードやフィールドへ反映するのにreturn eventの代わりにkintone.app.record.set()を用いるという代替方法が存在します。ですので、submit/proceed以外のイベントでreturnできるようにサポートされる日は来るのか、来ないのか・・・という感じだと思います。

kintone.Promiseならではの機能

ここまでお読み頂けると、kintone JavaScript APIでPromise相当の処理を記述するにはkintone.Promiseを使えばsubmit/proceedイベントも操作しやすくなるし、使わない手はないとなると思います。しかし、まだ考察できていないポイントがあります。
・native Promiseにあってkintone.Promiseにないものがあるのではないか
・kintone.Promiseならではの機能はないのか
というところです。ひとつずつ見ていきましょう。

native Promiseにあってkintone.Promiseにないもの

「native Promiseとkintone.Promiseの関係」でも述べましたが、kintone.Promiseはnative Promiseを踏襲しているようでした。そうすると、ドキュメントやTipsには出てきていないながら、一般に使えるPromiseの機能が使えるかを確認してみたくなります。例えば、並行処理を担うkintone.Promise.all()kintone.Promise.race() といったメソッド、resolve・rejectのPromiseオブジェクトを直接返すkintone.Promise.resolve()kintone.Promise.reject() 等ですが、これらはいずれも利用可能でした。

kintone.Promiseならではの機能

「returnできるsubmit/proceedイベント」もそうですが、kintone.api() や kintone.proxy() がkintone.Promiseオブジェクトを返すというところでしょう。これはjQuery.ajax()等で似たものを見かけますが、非常にサッパリとPromise処理が記述できますので、rejectを明示的・詳細に処理しなくて良い時にはバンバン使うと良いと思います。

例として、先のsubmitイベントのサンプルをnew kintone.Promise() を用いずに書き換えてみたいと思います。

(function() {
  "use strict";

  kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], function(event) {
    var record = event.record;
    var key_field = 'ユーザー選択';
    var own_id = kintone.app.record.getId(); // 新規でnull、編集時には値がセットされる
    var query = '';

    // ユーザー選択フィールドが空の場合にはここで、return
    if(record[key_field].value.length === 0 ){
      return event;
    }

    // key_fieldがユーザー選択フィールドの時のquery
    switch (record[key_field].type) {
      case 'USER_SELECT':
        for (var i = 0; i < record[key_field].value.length; i++) {
          query += key_field + ' in ("' + record[key_field].value[i].code + '") or ';
        }
        query = query.slice(0, -3);
        break;
    }

    // 新規登録(null)と編集(自レコード除外)に対応
    if (own_id) {
      query = query + ' and レコード番号 != ' + own_id;
    }

    // 検索対象のレコードは0または1個なので、limit 1で絞り込み
    query = query + ' limit 1';

    // 重複チェック
    return kintone.api(kintone.api.url('/k/v1/records', true), 'GET', {
      app: kintone.app.getId(),
      query: query
    }).then(function(resp) {
      // レコードの重複をフィールドエラーにセット
      if (resp.records.length === 1) {
        record[key_field].error = '値がほかのレコードと重複しています。';
      }
      return event;
    }).catch(function(err) {
      event.error = '既存レコードの取得に失敗しました。'
      return event;
    });
  });

})();

resolve、rejectの明示がなくなり、行数も減ってスッキリしました。

まとめ

今回の内容は、ある程度慣れてこないとハラオチしにくい内容だったかもしれません。また、Promiseの真骨頂であるthenのチェーンが1段しかない例だったため、かえってイメージしにくい点もあったかもしれません。しかし、実はkintone.Promiseの本質的なところを全部書けたかなぁと思ったりしまていますが、いかがだったでしょうか?

このように背景的なところや現状での対応方法を自分なりにでも押さえておくのは、書き方の幅を広げてくれたり、逆によろしくない書き方をせずに済むと考えていますので、皆さんの参考にもして頂けたら幸いです。

それでも、やはり書き足した方が分かり良い内容もあったかと思いますので、機会があればまた詳しく突っ込んだkintone.Promiseの記事をお届けできればと思っています。

【補足:関連イベント】

kintone.Promiseについては、デブサミ2016の「【18-F-3】【上級】これでもう怖くない、kintone Promise」というセッション(R3instituteの金春さん担当)がありますので、ぜひチェックしてみてください!

 

弊社ではREST APIやJavascriptAPIを活用したカスタマイズ開発を1週間20万円という定額料金で提供しています。
kintoneのカスタマイズ開発でお困りな事や一度プロに相談したいといった事がありましたら、弊社までお気軽にお問い合わせください。

 

同じカテゴリーの記事