kintoneのJSカスタマイズでMoment.jsを使うのはやめたほうがいい


kintone JavaScriptカスタマイズにおける日付の取り扱い

kintoneで日付フィールド・日時フィールドなどを扱ったJSカスタマイズを行う場合、何らかの日付操作ライブラリなどに頼るのが慣例だと思います。

kintoneのJavaScriptカスタマイズにおいて日付操作ライブラリのデファクトスタンダードはMoment.jsだと認識しています。Cybozu CDNにもMoment.jsは公開されていますし、いろんなTipsで見かけます。現場でも使われがちな肌感があります。

しかし、個人的にはMoment.jsを使うことはあまりオススメしません。

使わないほうがいい理由

  • mutable(可変)であるため
  • ファイルサイズが大きいため(59KB)

僕が使わないほうがいいと思っている理由はこの2つです。特に1つ目は、かなり重大なバグを生み出してしまう可能性があります。どういうことなのか下記に例を示します。

サンプル: 保存時に保存日時と計算された日付を表示するアプリ

下記画像のように保存時に現在の日時を示すフィールドと加算日数分を加算した加算後の日時が表示されるためのフィールドを用意しました。ユーザーは加算日数のみ入力し残り2つは自動で表示されるようにしたいとします。

  • サンプルアプリのレコード作成画面
  • 実際に保存時に動作してほしいこと

上記のように動作してほしい場合、Moment.jsを使いつつ、素直に考えれば下記のようなコードになるかと思います。

間違った日付操作.js
(function() {
    "use strict";

    // 保存時のイベントハンドラ
    kintone.events.on(["app.record.create.submit", "app.record.edit.submit"], function(event) {
        const record = event.record;

        // 現在の日時
        const nowDatetime = moment();
        // 加算日数分、加算された日時
        const addedDatetime = nowDatetime.add(record.加算日数.value, 'day');

        // recordに反映
        record.現在の日時.value = nowDatetime.format();
        record.加算後の日時.value = addedDatetime.format();

        return event;
    });
})();

間違いようのないシンプルなコードだと思います。しかしながら、実際に動作させると下記のようになります。

  • 期待する動作と違った挙動になった

この通り、意図・予想しない動きにより、他の変数や動作にも影響を与えてしまい、バグにつながってしまいます。

なぜ現在の日時加算後の日時は同一の値になってしまったのか

前述の通りmutableだからなのですが、具体的には、下記の部分のとおり既存の値まで変更されてしまっていたからです。これはMoment.jsの仕様です。このような、何らかの操作によりもとの値に対しても影響を与えてしまうことをmutable破壊的と呼びます。

回避策

これを回避するには関数を利用してもimmutable(不変)なライブラリを利用するのが一番ラクです。
僕の場合はDay.jsを好んで使っています。Day.jsは2KBほどなのでファイルサイズ的にもメリットが大きいです。
Day.js
Day.jsはMoment.jsライクな書き方で日付操作ができますので、今までMoment.jsを使っていた人にとっては扱いやすいものだとおもっています。

Day.jsをつかった場合.js
(function() {
    "use strict";

    // 保存時のイベントハンドラ
    kintone.events.on(["app.record.create.submit", "app.record.edit.submit"], function(event) {
        const record = event.record;

        // 現在の日時
        const nowDatetime = dayjs(); // <- ここをMoment.jsからDay.jsに変更するだけ
        // 加算日数分、加算された日時
        const addedDatetime = nowDatetime.add(record.加算日数.value, 'day');

        // recordに反映
        record.現在の日時.value = nowDatetime.format();
        record.加算後の日時.value = addedDatetime.format();

        return event;
    });
})();

Day.jsの場合はもとの変数に影響を与えない(immutable)なのできちんと意図したとおりに動作します。

他にも比較的新しいライブラリであれば基本的にはimmutableになっていますので用途にあったものを探してみてください。Moment.jsの後発であるluxonというのもあります。

それでもMoment.jsを使い続ける場合

新しくはじめるカスタマイズであればそのタイミングで用途に合った日付操作ライブラリを利用すればよいですが、既存のプロジェクトで既にMoment.jsを利用していて変えづらいなどの場合は、下記のような対応が考えられます。

案1. 日付計算を行い場合は.clone()を必ず使うようにする

moment.clone()はもとの値をコピーします。コピーした値を変更してもコピー元には影響を及ぼさないですので、そのように書くことを意識付ければ一応は回避できます。ですが、複数人で開発している際などみんなが気をつけていないといけませんのでいずれバグを起こしてしまうと思われます。

サンプル.js
(function() {
    "use strict";

    // 保存時のイベントハンドラ
    kintone.events.on(["app.record.create.submit", "app.record.edit.submit"], function(event) {
        const record = event.record;

        // 現在の日時
        const nowDatetime = moment();
        // 加算日数分、加算された日時
        // .clone()してから.add()することでnowDatetimeには影響を与えない
        const addedDatetime = nowDatetime.clone().add(record.加算日数.value, 'day')

        // recordに反映
        record.現在の日時.value = nowDatetime.format();
        record.加算後の日時.value = addedDatetime.format();

        return event;
    });
})();

案2. moment-immutableを利用する

Moment.jsをimmutableな動作をさせるためにつくられたmoment-immutableというライブラリが公開されています。
moment-immutable
Moment.jsを読み込ませたあと、このライブラリを読み込ませることにより、moment.jsも自動的にimmutableとなり、もとの値に影響を与えなくなります。

最後に

ほかにもみなさんのオススメ日付操作ライブラリの使い方やMoment.jsを使いながらバグを起こさないためにどうすればいいかなどいい案があれば教えて下さい!
また、mutable/immutableの問題は実はMoment.jsにだけ関係したことではありません。関数宣言や変数操作であっても起こりうることですので別途記事にしたいと思います。