我想,下面这段代码,你是不是在开发中常常这样使用来计算距离现在过去了多长时间:
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`
}
别,请别这么做。虽然相对时间暂时看起来像是一个简单的问题,但你应该要意识到相对时间有很多复杂的问题需要解决,比如:
还可能存在其他问题,例如时区问题。这些复杂的问题一旦来到,往往开发者会采用像 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
这个对象。
我先用下面一段代码来样式它的便捷性:
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
是一个很棒的 JavaScript 日期工具库,每个日期都支持以 树摇
的方式单独导出。
其中,date-fns
中内置了一个 intlFormatDistance
函数,它是 Intl.RelativeTimeFormat
的一个小包装器,这个函数做的正是我们需要的。并且,它的大小在2kb以下。
看下面的代码,是不是代码简单了许多:
除此之前,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
这个对象还能为你格式化数字:
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
这些对象,能不能帮你解决你面临的问题。