时间格式化

时间在界面上有三种截然不同的形态,把它们混为一谈是常见的困惑来源。ranuts 为每一种提供独立的函数:

读者真正想知道的 函数 输出示例
这件事具体发生在什么时候? formatDate 2026-07-25 14:05:09
这段时间有多长 formatDuration 01:01:01
距离现在多久之前 formatRelative 3 天前5m

formatDuration

把经过的秒数格式化成冒号分隔的时钟时长,也就是播放器进度条上常见的那种形态。不足一小时用 mm:ss,超过则展开为 hh:mm:ss

参数

参数 说明 类型 默认值
seconds 经过的秒数;负数会被夹到 0 number 必填

返回值

string:时长字符串;输入不是有限数时返回 ''

import { formatDuration } from 'ranuts/utils';

formatDuration(0); // '00:00'
formatDuration(65); // '01:05'
formatDuration(3661); // '01:01:01'
formatDuration(NaN); // ''

NaN 返回空串是刻意的:播放器在元数据加载完成前读 video.duration 拿到的就是 NaN,此时显示空白比 NaN:NaN 得体。

formatRelative

描述某个时间点相对于另一个时间点的位置,比如「3 天前」「2 小时后」。

本地化交给平台的 Intl.RelativeTimeFormat,它自 2020 年起在所有主流浏览器可用,且已经掌握各语言的复数与词形规则。formatRelative 只补上 Intl 有意留白的那部分:决定用哪个单位来表达这段间隔。

Intl 一样,它只报告单一单位:3 天 6 小时的间隔算作「3 天前」,不会说成「3 天 6 小时前」。

参数

参数 说明 类型 默认值
value 要描述的时间点 number | string | Date 必填
options 见下表 FormatRelativeOptions {}
选项 说明 类型 默认值
now 参照的时间点 number | string | Date 当前时间
locale BCP 47 语言标签;compact 风格会忽略它 string | string[] 运行时语言
style 'long' | 'short' | 'narrow' | 'compact' RelativeStyle 'long'
numeric 'auto' 会换用「昨天」这类习惯说法,'always' 保留数字 'always' | 'auto' 'auto'

返回值

string:描述文本;两端任一无法解析时返回 ''

import { formatRelative } from 'ranuts/utils';

const twoHoursAgo = Date.now() - 2 * 3600_000;

formatRelative(twoHoursAgo, { locale: 'zh-CN' }); // '2 小时前'
formatRelative(twoHoursAgo, { locale: 'en-US' }); // '2 hours ago'
formatRelative(twoHoursAgo, { locale: 'en-US', style: 'short' }); // '2 hr. ago'
formatRelative(Date.now() + 60_000, { locale: 'zh-CN' }); // '1 分钟后'
formatRelative(Date.now() - 86_400_000, { locale: 'zh-CN' }); // '昨天'
formatRelative(Date.now() - 86_400_000, { locale: 'zh-CN', numeric: 'always' }); // '1 天前'

compact 风格

compact 是列表条目旁边那种紧凑角标:

formatRelative(Date.now() - 30_000, { style: 'compact' }); // '30s'
formatRelative(Date.now() - 5 * 60_000, { style: 'compact' }); // '5m'
formatRelative(Date.now() - 3 * 3600_000, { style: 'compact' }); // '3h'
formatRelative(Date.now() - 2 * 86_400_000, { style: 'compact' }); // '2d'

parseVttTimestamp / parseVttCueTiming

解析 WebVTT 字幕的时间信息,即 .vtt 文件里 hh:mm:ss.mmm --> hh:mm:ss.mmm 这样的行。

parseVttTimestamp 把单个时间戳(hh: 部分可选)解析成秒数;parseVttCueTiming 解析一整行 cue 时间信息,即用 --> 分隔的两端,并忽略结尾附带的 cue 设置(比如 align:start line:0),返回 { start, end }

import { parseVttTimestamp, parseVttCueTiming } from 'ranuts/utils';

parseVttTimestamp('00:00:05.000'); // 5
parseVttTimestamp('01:05.250'); // 65.25
parseVttTimestamp('不是时间戳'); // undefined

parseVttCueTiming('00:00:00.000 --> 00:00:05.000'); // { start: 0, end: 5 }
parseVttCueTiming('00:00:05.000 --> 00:00:10.000 align:start line:0'); // { start: 5, end: 10 }

两者在输入不匹配时都返回 undefined,不会抛出异常,这样字幕文件里格式错误的一行可以直接跳过,不会中断整个解析过程。

注意事项

  1. 单位选择formatRelative 取间隔真正填满的最粗单位,再在该单位内取整。当取整结果正好达到下一个单位的临界点(比如 59.6 分钟会取整成「60 分钟」)时,会自动进位,于是显示为「1 小时前」。
  2. 对称取整:先对绝对值取整再补回符号。因为 JavaScript 里 Math.round(-1.5)-1,否则 90 分钟前会显示「1 小时前」,而 90 分钟后却显示「2 小时后」。
  3. 格式化器复用Intl.RelativeTimeFormat 实例按 locale/style/numeric 组合缓存,因此渲染一百条时间戳的列表只会构造一个格式化器,而不是一百个。
  4. 降级:在没有 Intl.RelativeTimeFormat 的运行时上会回退到 compact 形态,而不是抛错。