JavaScript での時刻操作に Moment.js ではなく Day.js を利用し続けている理由

昨日、拙作の Nuxt.js プラグインである @nuxtjs/dayjs の v1.2.0 をリリースしました。

このプラグイン自体は2019年3月に開発をはじめて、おおよそ一年半ほど管理してるのですが、それ以前から JavaScript での時刻操作では Day.js を使ってきました。

Moment.js のプロジェクト終了が告知され、時刻操作ライブラリに注目が集まっていることなので、今一度 Day.js の採用理由についてまとめてみます。

Day.js について

iamkun によって開発されている時刻操作のライブラリです。Moment や date-fns などは Organization によって管理されていますが、時刻操作ライブラリとしては珍しく個人による成果物となっています。

https://day.js.org/

色々と特徴自体はあるのですが、選定理由はおおよそ以下の 3 つと捉えて良いと思います。

  • Moment.js に酷似した API となっており、機械的な移行を行いやすい
  • 最終的な成果物のファイルサイズが 2KB 程度であり、大幅な軽量化が期待できる
  • 型定義がそのレポジトリ自体によって管理されている

ざっくりいうと Moment.js から一番移行しやすくて一番軽い時刻操作ライブラリ というところでしょうか。date-fns のような Function 単位を好む人もいると思いますが、私は表現力と、後述のモックとの相性によって、 Dayjs オブジェクトのような、特定のオブジェクトを経由するほうが好みです。

コード例

最もベーシックな利用方法は、現在時刻のオブジェクト化と、 String のパースでしょうか。これまで Moment.js では、以下のように利用していたかと思います。

import moment from 'moment'

moment().format() // 現在時刻を ISO8601 形式で出力
moment().format('YYYY/MM/DD') // YYYY/MM/DD 形式で出力
moment('YYYY/MM/DD 00:00:00') // 今日の 0 時の Moment Object を作成

Day.js では、基本的にこういったベーシックな API は愚直な置換で動作します。

import dayjs from 'dayjs'

dayjs().format() // 現在時刻を ISO8601 形式で出力
dayjs().format('YYYY/MM/DD') // YYYY/MM/DD 形式で出力
dayjs('YYYY/MM/DD 00:00:00') // 今日の 0 時の Moment Object を作成

その他 dayjs().add(-1, 'days') などの相対時刻の操作も、 moment と同様の API でサポートされており、おおよそ同じ感覚で利用できます。

差分について

基本的にはほぼ同じ感覚で使えるので、差分についてのみ言及します。

Locale の取り扱いは別途

一応いくつかある差分としては、Day.js はファイル削減のためにデフォルトでは Locale (曜日名のローカライズなどの)データを最小限のみ保持しています。そのため、例えば日本ロケールを利用したい場合、 Moment.js では以下のように設定するだけでしたが、

import moment from 'moment'

moment.locale('ja')

Day.js では、以下のように別途 import する必要があります。

import dayjs from 'dayjs'
import 'dayjs/locale/ja'

dayjs.locale('ja')

とはいえ曜日の日本語名が必要な場合などもあまり多くないと思いますが、利用時にハマらないようにご注意ください。

プラグインシステムとタイムゾーン

Moment.js はかなり重厚なライブラリでしたが、 Day.js は基本的にはシンプルなライブラリであろうとしています。そのため、複雑な機能のいくつかは、オプトインのプラグインとして提供されています。

追加でのパッケージインストールをする必要はありませんが、存在を把握しておくと何かと楽になります。

例えばタイムゾーンの管理。これまで Moment.js では moment と moment-timezone で実現していましたが、 Day.js では UTC プラグインと Timezone プラグインの組み合わせで実現します。

// https://day.js.org/docs/en/plugin/timezone#docsNav

import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'

dayjs.extend(utc)
dayjs.extend(timezone)

dayjs.tz('2014-06-01 12:00', 'America/New_York')
dayjs('2014-06-01 12:00').tz('America/New_York')
dayjs.tz.guess()
dayjs.tz.setDefault('America/New_York')

こんな感じ。こちらも覚えておくと便利なのと、オフィシャルドキュメントに公式のプラグインとカスタムプラグインの作成方法の両方が掲載されているので、一読しておくことをお勧めします。

https://day.js.org/docs/en/plugin/plugin

おわりに

本当は欠点について書こうと思っていて、それが moment-timezone 相当の機能がないことだったんですが、執筆時点から見て先月(2020/08)にオフィシャルでサポートされていたことによって、ただ褒めるだけの記事になりました。

今後 Temporal の正式制定などによって、時刻操作ライブラリは消えゆく存在であることは間違いありません。Moment.js のサポート終了の理由もその辺りが関連していることが公式に言及されています。

この移行期において、かしこくやり抜くための戦略として、軽量でありながら Moment.js の API を色濃く受け継いだ Day.js はかなりコスト対効果に優れるライブラリであることは間違いないので、移行先に悩んでいるかたについては、迷ったらとりあえず Day.js にしておくことをお勧めします。

余談: @nuxtjs/dayjs とテストコードについて

興味ある人向けとして、 @nuxtjs/dayjs を作った理由について最後に少しまとめておきます。

プログラムによる時刻操作において多くのケースにおいて課題となるのは、テストコードにおける Date Object の取り扱いです。frozen な Date Object を作成できないと、そのテストの結果は常に変化し、 Flaky なテストとなってしまいます。

そしてこれは多くの場合、時刻操作ライブラリを直接的に import していることや、直接 new Date を行っていることに起因します。

そのため開発初期の段階である程度工夫しておきたいですが、面倒なのも事実。@nuxtjs/dayjs は、その辺りをまとめて解消するために開発しています。

というのも、 @nuxtjs/dayjs は Nuxt.js の inject 機能によって提供されており、 Vue/Nuxt v2.x の時点では型定義を別途行った上で、最終的には実行時に this.$dayjs へとインスタンスが注入される挙動となっています。

この挙動によって、vue-test-utils の mocksに載せやすく、ただ利用しているだけで、テストコードを後から書くケースでもゼロコストでダミーデータに差し替えられるという特徴を備えています。

時刻操作で痛い目を見たことがある人は結構多いと思うので、 Nuxt.js ユーザーの人はよろければ利用してみてください。

https://github.com/nuxt-community/dayjs-module