Day.js 作为 Moment.js 的极简替代品,具有体积小、API 类似 、TypeScript 支持良好等优点。最近在优化项目时,发现项目中存在 dayjs 和 moment 混用的情况,决定将 moment 全面替换掉,但 Day.js 其并不是 Moment.js 的直接替代品,在替换过程中发现了一些需要注意的地方。

Day.js 的 immutable VS Moment.js 的 mutable

熟悉函数式编程或 Redux 中 Reducer 的同学,对于 immutable 和 mutable 应该有很深刻的了解,这里仅简单说下区别:

  • immutable 会直接操作当前对象,造成当前对象的属性变化
  • mutable 不会直接操作当前对象,而是返回处理后新的对象

下面以项目中遇到的 “根据调度周期和首次调度时间需要计算出最近 10 次的调度时间” 这个场景为例,之前使用 Moment.js 的代码(为简单起见,调度周期的单位假定为天):

import moment from "moment";

function getNext10ScheduleTime(firstSchedule: number, period: number = 1) {
  const timeEntity = moment(firstSchedule);
  const result = [];
  for (let i = 0; i < 10; i++) {
    result.push({
      scheduleTime: timeEntity.valueOf(),
      scheduleText: timeEntity.format("YYYY-MM-DD HH:mm:ss"),
    });
    timeEntity.add(period, "d"); // A
  }
  return result;
}

此时便不能简单地直接将 moment() 的方法调用改为 dayjs(),因为 A 行利用了 Moment.js 的 mutable 特性,直接对 timeEntity 进行了操作,以便下次循环时取得新值。如果使用 Day.js 改写,则应为:

import dayjs from "dayjs";

function getNext10ScheduleTime(firstSchedule: number, period: number = 1) {
  let timeEntity = dayjs(firstSchedule);
  const result = [];
  for (let i = 0; i < 10; i++) {
    result.push({
      scheduleTime: timeEntity.valueOf(),
      scheduleText: timeEntity.format("YYYY-MM-DD HH:mm:ss"),
    });
    timeEntity = timeEntity.add(period, "d"); // B
  }
  return result;
}

Day.js 默认是 immutable 的,所以 B 行的 add() 调用后 timeEntity 本身没有变化,所以需要将返回值重新赋值。其实对于 Moment.js 来说,add() 也会返回一个值,但这个值是对 timeEntity 的直接修改后的结果。

所有涉及到日期对象的操作修改,如 add()subtract()year()set()startOf()endOf() 等操作,都符合上文所述,需要注意,也正是因为 Moment.js 的 mutable 特性,所以才想要使用 Day.js 将其替换。如果你的项目中大量依赖此类逻辑的话,Day.js 通过插件提供了一种不推荐的方案用以适配此类情况。

Day.js 的插件系统

Day.js 默认情况下包含了常用的大部分 API,但有些不常用的功能是通过插件提供的,一个插件包含了某些功能的实现及声明文件的提供。如 dateOfyear()、通过提供代表日期的对象构造 dayjs 实例等,这些都需要手动引入:

// dayjs-facade.ts
import dayjs from "dayjs";
import dayOfYear from "dayjs/plugin/dayOfYear";
import objectSupport from "dayjs/plugin/objectSupport";

// configure plugin like this
dayjs.extend(dayOfYear);
dayjs.extend(objectSupport);

export default dayjs;

可以将插件的配置都放置于这一个文件中,使用时从此文件引入即可。

目前的最新版本(1.10.7)objectSupport 插件提供的声明文件有些问题,在使用对象构造 dayjs 实例时 ts 的语法检查会提示 没有与此调用匹配的重载

dayjs-objectSupport-declaration-error.png

社区中已有用户提供了 pr 解决,不过目前还没合入发布版本,所以可以暂时使用 as any 将参数类型做一下强制类型转换,或者单独使用 set() 方法进行分别设置。

替换过程中需要必要的测试

在替换过程中,遇到不能确定会不会引起差异的情形,最好针对自己的情形书写用例进行验证,避免可能因版本或者是某些 API 实现上存在差异的情况。可查看在替换过程中我书写的用例以做参考。当然如果项目本身就有比较健全的单元测试,这一步也可以直接在项目中验证。