12k
All articles

在浏览器中显示人类可读的时间

介绍如何借助 Intl.DateTimeFormat、Intl.RelativeTimeFormat、Intl.DurationFormat 及 Temporal 在浏览器中格式化 UTC 时间戳,无需引入第三方库。

OpenReplay Team
OpenReplay Team
在浏览器中显示人类可读的时间

您的服务器以 UTC 格式存储时间戳。您的用户分布在数十个时区。这两个现实之间的差距——原始 ISO 字符串与”2 小时前”——决定了您的界面是感觉原生还是陌生。

现代浏览器现在可以在 JavaScript 中处理人类可读的时间,无需第三方库。本文介绍您应该使用的原生 API:Intl.DateTimeFormatIntl.RelativeTimeFormatIntl.DurationFormat,以及用于时区感知逻辑的 Temporal API。

核心要点

  • 使用 Intl.DateTimeFormat 处理区域感知的绝对时间戳,直接传递 IANA 时区标识符以避免手动偏移计算。
  • 使用 Intl.RelativeTimeFormat 配合一个小型辅助函数来生成自然短语,如”昨天”或”3 小时前”。
  • 使用 Intl.DurationFormat 处理经过时间和倒计时,但需要检查浏览器支持,因为基准可用性直到 2025 年才到来。
  • Temporal API 用显式、不可变的类型替代了容易出错的 Date 对象——在新项目中采用它,同时在现有代码库中依赖 Intl 格式化器。

使用 Intl.DateTimeFormat 处理绝对时间戳

当用户需要精确的日期和时间时,Intl.DateTimeFormat 会自动处理本地化。它尊重用户的区域设置,并根据地区惯例格式化日期。

const date = new Date('2026-03-15T14:30:00Z')

const formatter = new Intl.DateTimeFormat('en-US', {
  dateStyle: 'medium',
  timeStyle: 'short',
  timeZone: 'America/New_York'
})

console.log(formatter.format(date)) // "Mar 15, 2026, 10:30 AM"

该 API 直接接受 IANA 时区标识符,消除了手动偏移计算。对于用户本地格式化,可以自动检测时区:

const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone

这会返回类似 "Europe/London""Asia/Tokyo" 的值,您可以直接将其传递给格式化器。

使用 Intl.RelativeTimeFormat 处理相对时间

社交动态、通知和活动日志受益于相对时间戳。Intl.RelativeTimeFormat 生成诸如”3 天前”或”2 小时后”的短语,并具有适当的本地化。

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

rtf.format(-1, 'day')    // "yesterday"
rtf.format(-3, 'hour')   // "3 hours ago"
rtf.format(2, 'week')    // "in 2 weeks"

numeric: 'auto' 选项在适当时生成自然语言(“yesterday”)而不是字面数字(“1 day ago”)。

您需要自己计算差值——该 API 格式化数值,而不是日期。一个简单的辅助函数可以实现:

function getRelativeTime(date) {
  const now = Date.now()
  const diffInSeconds = Math.round((date - now) / 1000)
  const units = [
    { unit: 'year', seconds: 31536000 },
    { unit: 'month', seconds: 2592000 },
    { unit: 'day', seconds: 86400 },
    { unit: 'hour', seconds: 3600 },
    { unit: 'minute', seconds: 60 },
    { unit: 'second', seconds: 1 }
  ]

  for (const { unit, seconds } of units) {
    if (Math.abs(diffInSeconds) >= seconds) {
      const value = Math.round(diffInSeconds / seconds)
      return new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
        .format(value, unit)
    }
  }
  return 'just now'
}

上述月份和年份值使用固定的秒数近似值。对于跨越不同月份长度或闰年的日历精确差异,请使用 Temporal 或专用日期库,而不是固定的秒数常量。

请注意,Math.round 偶尔会将值推入下一个单位(例如,89 秒除以 60 后四舍五入为 1,产生”1 分钟前”而不是”89 秒前”)。如果您需要更严格的边界,请改用 Math.trunc

使用 Intl.DurationFormat 格式化持续时间

对于经过时间、倒计时或视频长度,Intl.DurationFormat 在各个区域提供一致的输出:

const duration = { hours: 2, minutes: 45, seconds: 30 }

const df = new Intl.DurationFormat('en', { style: 'long' })
console.log(df.format(duration)) // "2 hours, 45 minutes, 30 seconds"

const dfShort = new Intl.DurationFormat('en', { style: 'digital' })
console.log(dfShort.format(duration)) // "2:45:30"

该 API 在 2025 年达到跨引擎基准支持,并在现代浏览器中可用,但您仍应确认与您的特定支持矩阵的兼容性。

使用 Temporal API 实现更安全的日期处理

Temporal API 解决了 JavaScript Date 对象长期存在的问题——可变性、时区混淆和解析不一致。截至 2026 年,Temporal 在现代 Chromium 和 Firefox 版本中受支持,但功能检测仍然至关重要,因为它尚未在所有浏览器中普遍支持。

if (typeof Temporal !== 'undefined') {
  const now = Temporal.Now.zonedDateTimeISO('America/Los_Angeles')
  console.log(now.toString())
}

Temporal 为不同概念提供不同的类型:Temporal.PlainDate 用于日历日期,Temporal.PlainTime 用于挂钟时间,Temporal.ZonedDateTime 用于时区感知的时刻。这种明确性防止了困扰基于 Date 的代码的错误。

对于新项目,Temporal 提供了最清晰的基础。对于现有代码库,Intl 格式化器与传统的 Date 对象无缝协作。

实用建议

将时间戳存储为 UTC ISO 8601 字符串。在客户端使用 Intl API 格式化它们。避免解析非 ISO 日期字符串——它们在不同浏览器中的行为不一致。

在格式化多个时间戳时重用格式化器实例。创建一次格式化器并重复调用 format() 比每次调用都重新创建一个要快得多。

使用语义化 HTML 以提高可访问性:

<time datetime="2026-03-15T14:30:00Z">March 15, 2026</time>

结论

原生平台现在可以处理曾经需要 Moment.js 或类似库的功能。Intl.DateTimeFormat 涵盖绝对时间戳,Intl.RelativeTimeFormat 涵盖相对短语,Intl.DurationFormat 涵盖经过时间,而 Temporal API 为时区感知的日期逻辑提供了坚实的基础。对于浏览器中的人类可读时间,这些内置工具就是您所需的全部。

常见问题

我可以在不自己计算时间差的情况下使用 Intl.RelativeTimeFormat 吗?

不可以。该 API 仅将数值和单位格式化为本地化字符串。您必须自己计算两个日期之间的差异,并在将它们传递给 format 方法之前选择适当的单位。本文中显示的辅助函数是该计算的常见模式。

Temporal API 现在可以安全地在生产环境中使用吗?

截至 2026 年,Temporal 在现代 Chromium 和 Firefox 版本中可用,但尚未在所有浏览器中普遍支持。在调用 Temporal 方法之前始终使用功能检测,如果需要广泛的兼容性,请考虑使用 polyfill。仅对于格式化,Intl API 已经具有广泛的支持,并且可以很好地与传统的 Date 对象配合使用。

为什么我应该重用 Intl 格式化器实例而不是每次都创建新实例?

构造 Intl 格式化器涉及解析区域数据、时区规则和格式化选项,这会带来可测量的成本。创建一个实例并重复调用其 format 方法比在每次调用时重建格式化器要快得多,特别是在渲染长时间戳列表时。

Intl.DateTimeFormat 会自动使用用户的本地时区吗?

默认情况下,是的。如果您省略 timeZone 选项,格式化器将使用运行时环境时区,在浏览器中这与用户的系统设置匹配。您可以通过 Intl.DateTimeFormat().resolvedOptions().timeZone 显式检索此值,并将其传递给其他格式化器或发送到您的服务器。

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.