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

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

目次

はじめに

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

Promise に関する既存の記事

過去の Promise に関する記事はこちらです。

Promise を使う利点

kintone で Promise を使う利点は大きく 2 つあります。

  • レコード作成時などに、処理を待ってからレコードを保存できる。(同期的処理)
    「あるアプリ A のレコードを保存時、アプリ B のレコードを取得し、その値を利用」というようにレコードの保存時などに kintone API を使って他のデータを取得したり変更したり、同期的に処理できます。
    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 は便利です。
コールバック関数で複数のデータを取得する場合と比較すると差は明らかです。

  • 例)商品アプリ(アプリ ID: 1)から商品 A, B, C(レコード ID: 1, 2, 3)の金額を取得して、その合計を表示する(コールバック関数では Submit 時に処理待ちができないので詳細レコード表示時とします)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    (() => {
      'use strict';
      kintone.events.on('app.record.detail.show', (event) => {
        // 商品アプリからデータ(レコードID: 1) を取得する
        kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}, (resp1) => {
          // 商品アプリからデータ(レコードID: 2) を取得する
          kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2}, (resp2) => {
            // 商品アプリからデータ(レコードID: 3) を取得する
            kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3}, (resp3) => {
              // 取得したデータを合計して表示
              alert(Number(resp1.record.価格.value) + Number(resp2.record.価格.value) + Number(resp3.record.価格.value));
            });
          });
        });
      });
    })();

このように、コールバック関数で書くと、複数 API を呼び出すときに関数が入れ子となってしまい、非常に読みづらくなってきます。
エラー処理をいれるとさらに複雑になります。

  • エラー処理も含めた例

     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
    
    (() => {
      'use strict';
      kintone.events.on('app.record.detail.show', (event) => {
        // 商品アプリからデータ(レコードID: 1) を取得する
        kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}, (resp1) => {
          // 商品アプリからデータ(レコードID: 2) を取得する
          kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2}, (resp2) => {
            // 商品アプリからデータ(レコードID: 3) を取得する
            kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3}, (resp3) => {
              // 取得したデータを合計して表示
              alert(Number(resp1.record.価格.value) + Number(resp2.record.価格.value) + Number(resp3.record.価格.value));
            }, (err3) => {
              // エラーを表示
              alert('error', err3.message);
            });
          }, (err2) => {
            // エラーを表示
            alert('error', err2.message);
          });
        }, (err1) => {
          // エラーを表示
          alert('error', err1.message);
        });
      });
    })();

上記のように複数のデータを取得したときなど複雑な記述にどうしてもなってしまいますので、コールバック形式よりも Promise 形式をぜひ使うようにしましょう。

kintone.Promise の使い方

同期的処理をさせたい場合は Promise を return すればよい、と前述しましたが、kintone.api() を使わなくとも明示的に Promise を返却することもができます。
基本的には kintone.api() で Promise オブジェクトを return するとよいです。
ただし、処理が複雑な場合などはこちらのほうが見やすいため、覚えておくとよいでしょう。

ぜひこちらの例も確認してください。
kintone.Promise とは

  • kintone.Promise で Promise オブジェクト作成

     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) => {
        // new kintone.Promise()とすることでPromiseオブジェクトを作成
        // 引数 resolve は成功したとき, reject はエラー処理を書くことができる
        return new kintone.Promise((resolve, reject) => {
          kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp) => {
            // 商品アプリから取得したデータを見積もりアプリのフィールドに代入して保存
            event.record.合計.value = Number(resp.record.価格.value) || 0;
            // returnではなく、成功時のハンドラresolveをつかってeventを返却する
            resolve(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 という書き方を使うことでもっと同期的処理をシンプルに書くことができるようになりました。
ただし、Internet Explorer 11 など一部ブラウザーで動作しませんので、使うときは注意しましょう。
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 で動作を確認しています。