• 使用浏览器内置的Intl.RelativeTimeFormat对象计算日期差
  • 发布于 2个月前
  • 188 热度
    0 评论
我想,下面这段代码,你是不是在开发中常常这样使用来计算距离现在过去了多长时间:
// 堆代码 duidaima.com
import moment from 'moment' // 61k (gzipped:19.k)
function Relative(props) {
const timeString = moment(props.date).fromNow()
return <>
  {timeString}
</>
}
可是,你竟然用一个大小为 20k (还是压缩过的,没压缩 61k)的包,只用来做日期的转换? really? 你想要的只是进行一个日期上的转换啊。
现在,经过我这么一吓唬,你可能会想,能不能这么做:
function nativeGetRelativeTime(unit = 'days', amount = 2) {
  return `in ${amount} ${unit}s`
}
别,请别这么做。虽然相对时间暂时看起来像是一个简单的问题,但你应该要意识到相对时间有很多复杂的问题需要解决,比如:
缩写:一般不会显示 “1天前”? 一般会显示 “昨天”、“明天” 或 “明年” 这样的词
将来和过去: 比如我们不会显示“在 -2 天内”,而是显示 “2天前”

还可能存在其他问题,例如时区问题。这些复杂的问题一旦来到,往往开发者会采用像 momentJs 和 dayjs 这样的库来解决问题。虽然,下面这段代码也可以试着解决你的问题:
function nativeGetRelativeTime(locale, unit, amount) {
  if (locale === 'zh') {
    const isPlural = amount !== 1 && amount !== -1
    const isPast = amount < 0
    if (amount === 1 && unit === 'day') return '明天'
    if (amount === -1 && unit === 'day') return '昨天'
    if (amount === 1 && unit === 'year') return '明年'
    if (isPast) {
      return `${amount} ${unit}${isPlural ? 's' : ''} 前`
    }
    return `in ${amount} ${day}${isPlural ? 's' : ''}`
  }

}

但是,还是请你别这么做。因为这看起来似乎变得复杂了。而我向你推荐的一个内置对象能帮助你解决相对时间的问题。

Intl.RelativeTimeFormat

重申一遍,当你遇到这些情况时,要记住,目前现代前端中已经有有很多解决常见问题的内置解决方案了,可以方便的进行使用。而面对本文提到的相对时间问题,我要说的就是 Intl.RelativeTimeFormat 这个对象。

我先用下面一段代码来样式它的便捷性:

const rtf = new Intl.RelativeTimeFormat('en', {
  numeric: 'auto'
})

rtf.format(1, 'day') // 'tomorrow'
rtf.format(-2, 'year') // '2 years ago'
rtf.format(10, 'minute') // 'in 10 minutes'
而且,它能很好地处理不同时区的问题:
const rtf = new Intl.RelativeTimeFormat('es-ES', {
  numeric: 'auto'
})
rtf.format(1, 'day') // 'mañana'
rtf.format(-2, 'year') // 'hace 2 años'
rtf.format(10, 'minute') // 'dentro de 10 minutos'
你甚至可以只传入navigator.language 作为第一个参数:
const rtf = new Intl.RelativeTimeFormat(
  navigator.language // ✅
)
除此之外,Intl.RelativeTimeFormat 支持的单位包括: "year", "quarter", "month", "week", "day", "hour", "minute", 和 "second"
现在,回到我们最初的例子,我们用 Intl.RelativeTimeFormat 来改写一下:
首先,我们先写一个简单的包装函数来处理相对时间的转换:
function Relative(props) {
  const timeString = getRelativeTimeString(props.date)
  
  return <>
    {timeString}
  </>
}
接着,我们使用 Intl.RelativeTimeFormat 对象来实现 getRelativeTimeString 函数:
export function getRelativeTimeString(
  date: Date | number,
  lang = navigator.language
): string {
  const timeMs = typeof date === "number" ? date : date.getTime();
  const deltaSeconds = Math.round((timeMs - Date.now()) / 1000);
  const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity];

  const units: Intl.RelativeTimeFormatUnit[] = ["second", "minute", "hour", "day", "week", "month", "year"];

  const unitIndex = cutoffs.findIndex(cutoff => cutoff > Math.abs(deltaSeconds));
  const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1;
  const rtf = new Intl.RelativeTimeFormat(lang, { numeric: "auto" });
  return rtf.format(Math.floor(deltaSeconds / divisor), units[unitIndex]);
}
date-fns
但是如果你不想自己的解决方案,那么也有一个开源的解决方案。date-fns 是一个很棒的 JavaScript 日期工具库,每个日期都支持以 树摇 的方式单独导出。其中,date-fns 中内置了一个 intlFormatDistance 函数,它是 Intl.RelativeTimeFormat  的一个小包装器,这个函数做的正是我们需要的。并且,它的大小在2kb以下。
看下面的代码,是不是代码简单了许多:

Intl.DateTimeFormat
除此之前,Intl.DateTimeformat 还提供格式化日期和时间:
new Intl.DateTimeFormat('en-US').format(new Date())
// -> 1/23/2023

new Intl.DateTimeFormat('en-US', {
  dateStyle: 'full'
}).format(new Date())
// -> Monday, January 23, 2023

new Intl.DateTimeFormat('en-US', {
  timeStyle: 'medium'
}).format(new Date())
// -> 4:17:23 PM

new Intl.DateTimeFormat('en-US', {
  dayPeriod: 'short', hour: 'numeric'
}).format(new Date())
// -> 4 in the afternoon
Intl.NumberFormat
同时,Intl.NumberFormat 这个对象还能为你格式化数字:
new Intl.NumberFormat('en', { 
  style: 'currency', currency: 'USD' 
}).format(123456.789)
// -> $123,456.79

new Intl.NumberFormat('de-DE', { 
  style: 'currency', currency: 'EUR' 
}).format(123456.789)
// -> 123.456,79 €

new Intl.NumberFormat('pt-PT', {
  style: 'unit', unit: 'kilometer-per-hour'
}).format(50));
// -> 50 km/h

(16).toLocaleString('en-GB', {
  style: 'unit', unit: 'liter', unitDisplay: 'long',
}));
// -> 16 litres
目前,所有主流浏览器以及 Node.js 和 Deno 都支持 Intl.RelativeTimeFormat。如果你还在使用像 momentJs 这样的大型数据处理库,不妨考虑考虑Intl.RelativeTimeFormat, Intl.DateTimeFormat 这些对象,能不能帮你解决你面临的问题。

用户评论