kintoneにおけるPromiseの書き方の基本

著者名:kintoneエバンジェリスト 村濱 一樹 (External link)

目次

information

このページではPromiseの使い方について説明していますが、async/awaitを使うことでコードがより簡潔になり読みやすくなります。
async/awaitについてはこちらの記事を参照ください。
Promiseのかわりにasync/awaitを使ってみよう

はじめに

今までにJavaScriptカスタマイズをしたことがある方は、「Promise」について聞いたことや使ったことがあると思いますが、プログラミングに慣れていないと難しい概念だと思います。
すでにPromiseに関する記事はこのcybozu developer network内にいくつか公開されています。
ですが、「Promiseって結局どういう風に書くんだっけ?」というときのために、書き方について重点的にこの記事でまとめたいと思います。

Promiseを使う利点

レコード作成時などに、処理を待ってからレコードを保存できます(同期的処理)。
「あるアプリAのレコードを保存時、アプリBのレコードを取得し、その値を利用」というようにレコードの保存時などにkintone APIを使って他のデータを取得したり変更したり、同期的に処理できます。

Promiseに対応しているイベント

Promiseに対応しているイベントは、 イベント を参照ください。
各イベントの仕様において、「イベントオブジェクトで実行できる操作」の項目に「Promise対応」と明記されているものが、Promiseに対応したイベントです。

たとえば、「レコード一覧画面」の中の「レコード一覧画面を表示した後のイベント」を見ると「イベントオブジェクトで実行できる操作」に「Promise対応」とあります。
「レコード一覧画面を表示した後のイベント」はPromiseに対応していることがわかります。

Promiseの書き方の基本

次の例を考えてみます。

  • 例)見積もりアプリ(カスタマイズするアプリ)のレコード保存時、商品アプリ(アプリID: 1)から商品A(レコードID: 1)の⾦額を取得し、その値を見積もりアプリに登録する。

Promiseを利用する(一回)

  • Promiseをつかって同期処理をする。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    (() => {
      kintone.events.on('app.record.create.submit', (event) => {
        // 商品アプリからデータ取得する
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp) => {
          // 商品アプリから取得したデータを見積もりアプリのフィールドに代入して保存
          event.record.価格.value = Number(resp.record.価格.value);
          return event;
        });
      });
    })();

このようにkintone.api()はコールバック関数を省略するとPromiseオブジェクトが戻り値になります。

1
kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}); // これでPromiseオブジェクトが生成される

それをreturnしてあげることによってkintone側でapp.record.create.submit時など、処理を待ってくれるしくみをkintoneは持っています。
Promiseオブジェクトをreturnしないと処理をまってくれないので注意しましょう。
逆にいえば、レコード詳細ページなど、処理を待たせる必要がなければPromiseオブジェクトのreturnは必須でないです。

また、Promiseが正常終了した場合はthen()で結果を受け取ることができます。
エラーが発生した場合は、catch()を使うことでデータ取得に失敗したときなどの制御ができます。

  • thencatchを使った例は次のとおりです。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    (() => {
      'use strict';
      kintone.events.on('app.record.create.submit', (event) => {
        // 商品アプリからデータ取得する(thenを使うことでrespの中にデータが格納される)
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp) => {
          // 商品アプリから取得したデータを見積もりアプリのフィールドに代入して保存
          event.record.価格.value = Number(resp.record.価格.value);
          return event;
        }).catch((resp) => {
          // エラー表示をする
          event.error = resp.message;
          return event;
        });
      });
    })();

Promiseを利用する(複数回)

問題はここからです。
なんとなくPromiseを使っている方にとっては、複数Promiseを利用する方法がわからない方も多いと思います。
そのため、ここでは整理しながらみていきましょう。

  • 例)レコード保存時のイベントで、商品アプリ(アプリID: 1)から商品A, B, C(レコードID: 1, 2, 3)の金額を取得して、その合計を見積もりアプリ(カスタマイズするアプリ)に登録する。

前述の例に加え、さらに取得するレコードを増やしてみます。実際にはレコード一括取得APIを使えば1回のAPI呼び出しで済みますが、今回はProimseの説明のために1レコードずつ合計3レコード取得します。

  • Promiseをつかって同期処理をする(複数)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    
    (() => {
      'use strict';
      kintone.events.on('app.record.create.submit', (event) => {
        const record = event.record;
        record.合計.value = 0;
        // 商品アプリからデータ(レコードID: 1) を取得する
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp1) => {
    
          // 取得したデータを合計して見積もりアプリに代入
          record.合計.value += Number(resp1.record.価格.value);
    
          // 商品アプリからデータ(レコードID: 2) を取得する
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2});
    
        }).then((resp2) => {
    
          // 取得したデータを合計して見積もりアプリに代入
          record.合計.value += Number(resp2.record.価格.value);
    
          // 商品アプリからデータ(レコードID: 3) を取得する
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3});
    
        }).then((resp3) => {
    
          // 取得したデータを合計して見積もりアプリに代入
          record.合計.value += Number(resp3.record.価格.value);
    
          // 最後にeventをreturnすることで反映される
          return event;
        });
      });
    })();

Promiseはthen()で処理を待つことができ、then()は繰り返し使えます。
少し難しいかもしれないですが、ここでも大事なのは、Promiseオブジェクトをreturnしているところです。
レコードIDが「1」の部分のみならず、レコードIDが「2」と「3」のところでもPromiseオブジェクトをreturnしているので、後続のthen()内で、レコードID「2」と「3」の取得結果を利用できます。
また、このように複数回Promiseを行う場合は、上から順番に処理がされます。

  • thenを使って連続して使う方法(上記を抜粋)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      // : 中略
      return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp1) => {
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2});
        // ↑ここでPromiseオブジェクトをreturnしているので、次の行でthenが使える↓
      }).then((resp2) => {
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3});
        // ↑ここでPromiseオブジェクトをreturnしているので、次の行でthenが使える↓
      }).then((resp3) => {
      // : 中略
    
  • catchを加えてエラー制御
    エラー制御をするには前述のものと同様、最後にcatch()をつけることで可能です。
    レコード「1,2,3」のどの取得でエラーが起きてもこのcatch()を使ってエラー制御をできます。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
    (() => {
      'use strict';
      kintone.events.on('app.record.create.submit', (event) => {
        const record = event.record;
        record.合計.value = 0;
        // 商品アプリからデータ(レコードID: 1) を取得する
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp1) => {
    
          // 取得したデータを合計して見積もりアプリに代入
          record.合計.value += Number(resp1.record.価格.value);
    
          // 商品アプリからデータ(レコードID: 2) を取得する
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2});
    
        }).then((resp2) => {
    
          // 取得したデータを合計して見積もりアプリに代入
          record.合計.value += Number(resp2.record.価格.value);
    
          // 商品アプリからデータ(レコードID: 3) を取得する
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3});
    
        }).then((resp3) => {
    
          // 取得したデータを合計して見積もりアプリに代入
          record.合計.value += Number(resp3.record.価格.value);
    
          // 最後にeventをreturnすることで反映される
          return event;
    
        }).catch((resp) => {
    
          // エラー表示をする
          event.error = resp.message;
          return event;
    
        });
      });
    })();

動作確認

実際にアプリを用意して、下記コードを実装しました。
レコード保存時に他のアプリと連携できます。

  • アプリの概要

    • 見積もりアプリのレコードを作成時、該当する商品を商品リストからデータ取得する。
    • 今回はPromiseの説明のためルックアップフィールドは使わない。

  • コード

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    
    /*
     * kintone.Promise sample program
     * Copyright (c) 2019 Cybozu
     *
     * Licensed under the MIT License
    */
    (() => {
      'use strict';
    
      // 商品リストのアプリID
      const PRODUCT_APP = 80;
    
      kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], (event) => {
        const record = event.record;
    
        // レコード取得用の条件を用意
        const params1 = {app: PRODUCT_APP, id: record.商品ID_1.value};
        const params2 = {app: PRODUCT_APP, id: record.商品ID_2.value};
        const params3 = {app: PRODUCT_APP, id: record.商品ID_3.value};
    
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', params1).then((resp1) => {
          // 商品名_1, 価格_1に取得したデータを代入
          record.商品名_1.value = resp1.record.商品名.value;
          record.価格_1.value = Number(resp1.record.価格.value) || 0;
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', params2);
        }).then((resp2) => {
          // 商品名_2, 価格_2に取得したデータを代入
          record.商品名_2.value = resp2.record.商品名.value;
          record.価格_2.value = Number(resp2.record.価格.value) || 0;
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', params3);
        }).then((resp3) => {
          // 商品名_3, 価格_3に取得したデータを代入
          record.商品名_3.value = resp3.record.商品名.value;
          record.価格_3.value = Number(resp3.record.価格.value) || 0;
    
          // データを反映させる
          return event;
        }).catch(() => {
          // エラーが発生した場合はデータ初期化
          record.商品名_1.value = '';
          record.価格_1.value = '';
          record.商品名_2.value = '';
          record.価格_2.value = '';
          record.商品名_3.value = '';
          record.価格_3.value = '';
          alert('商品リストから検索できませんでした。');
          return event;
        });
      });
    })();

Column1: async/awaitで直感的に同期的処理を書く

Promiseの概念を覚えるのは結構面倒です。最近のJavaScriptではasync/awaitという書き方を使うことでもっと同期的処理をシンプルに書くことができるようになりました。
async/awaitの詳細については async function (External link) を確認してください。

  • 例)レコード保存時のイベントで、商品アプリ(アプリID: 1)から商品A, B, C(レコードID: 1, 2, 3)の金額を取得して、その合計を見積もりアプリ(カスタマイズするアプリ)に登録する。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    (() => {
      'use strict';
      kintone.events.on('app.record.create.submit', async (event) => {
        event.record.合計.value = 0;
    
        // 商品アプリからデータ(レコードID: 1) を取得する
        const resp1 = await kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1});
        // 商品アプリからデータ(レコードID: 2) を取得する
        const resp2 = await kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2});
        // 商品アプリからデータ(レコードID: 3) を取得する
        const resp3 = await kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3});
    
        // 取得したデータを合計して見積もりアプリに代入
        event.record.合計.value = Number(resp1.record.価格.value) + Number(resp2.record.価格.value) + Number(resp3.record.価格.value);
        return event;
      });
    })();

Column2: よくある間違い

Promiseを利用する際、次のように書いてしまうことがあるようです。
よくある間違いのパターンと修正点を紹介します。

Promiseを使用しているのにコールバック地獄のような状態に陥るコード
  • 間違った例

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    (() => {
      'use strict';
      kintone.events.on('app.record.create.submit', (event) => {
        const record = event.record;
        record.合計.value = 0;
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp1) => {
          record.合計.value = Number(resp1.record.価格.value);
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2}).then((resp2) => {
            record.合計.value += Number(resp2.record.価格.value);
            return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3}).then((resp3) => {
              record.合計.value += Number(resp3.record.価格.value);
              return event;
            });
          });
        }).catch((resp) => {
          event.error = resp.message;
          return event;
        });
      });
    })();

    複数回Promiseを用いる場合、then()の中にまたthen()を書いてしまうことで入れ子が深くなってしまいます。
    動作はしますが、わかりにくいので Promiseを利用する(複数回) で説明しているように入れ子にせず書くことができます。
    次の正しい例と見比べてみてください。

  • 正しい例

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    (() => {
      'use strict';
      kintone.events.on('app.record.create.submit', (event) => {
        const record = event.record;
        record.合計.value = 0;
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp1) => {
          record.合計.value = Number(resp1.record.価格.value);
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2});
        }).then((resp2) => {
          record.合計.value += Number(resp2.record.価格.value);
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3});
        }).then((resp3) => {
          record.合計.value += Number(resp3.record.価格.value);
          return event;
        }).catch((resp) => {
          event.error = resp.message;
          return event;
        });
      });
    })();
Promiseを使用しているのにthenメソッドチェーンができていないコード
  • 間違った例

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    (() => {
      'use strict';
      kintone.events.on('app.record.detail.show', (event) => {
        let result;
        kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp1) => {
          result = Number(resp1.record.価格.value);
        });
        kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2}).then((resp2) => {
          result += Number(resp2.record.価格.value);
        });
        kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3}).then((resp3) => {
          result += Number(resp3.record.価格.value);
        });
      });
    })();

上記も動作自体はしますが、それぞれのAPI呼び出しが並列に処理されてしまいます。
順番に実行したい場合は前述の正しい例のように、then()の中に次のAPIを呼び出す部分を書く必要があります。

デモ環境

デモ環境で実際に動作を確認できます。
https://dev-demo.cybozu.com/k/309/ (External link)

ログイン情報は cybozu developer networkデモ環境 で確認してください。

information

このTipsは、2019年2月版kintoneで動作を確認しています。