过去,计算事件之间经过或剩余的时间(例如试用期到期、订阅倒计时或 SLA 窗口),并以本地化、人类可读的格式呈现这些时间跨度,需要手动计算或使用外部库。旧版 Date 对象仅跟踪纪元毫秒数,这使得日历月份算术、夏令时 (DST) 转换和单位平衡容易出错。
Temporal API 将 Temporal.ZonedDateTime 与 Temporal.Duration 和 Intl.DurationFormat 配对,以提供夏令时安全时间差、可配置的单位平衡、可感知日历的时长比较和本地化格式。
如何实现
- 获取
Temporal.ZonedDateTime端点:使用Temporal.Now.zonedDateTimeISO()获取系统时区的当前时间,或者在目标事件的时区与用户的系统默认时区不同时,传递显式 IANA 时区字符串Temporal.Now.zonedDateTimeISO(timeZone)。 - 使用
.since()和.until()计算微分:- 使用
now.since(start, options)计算自过去某个事件以来经过的时间。 - 使用
now.until(end, options)计算距离未来事件的剩余时间。 - 传递
largestUnit(例如'year'、'month'、'day'、'hour')和smallestUnit(例如'minute'),以平衡和舍入生成的Temporal.Duration。
- 使用
- 平衡或比较独立的
Temporal.Duration对象:- 使用
Temporal.Duration.from({ minutes: 90 })创建独立时长,并使用duration.round({ largestUnit: 'hour' })重新平衡单位(在跨日历月、年或夏令时转换进行平衡时,提供relativeTo)。 - 使用
Temporal.Duration.compare(d1, d2, { relativeTo })比较两个时长。如果任一时长包含日历单位(years、months、weeks)或跨越夏令时边界,则需要将PlainDate或ZonedDateTime作为relativeTo传递。
- 使用
- 针对人类与机器的格式:
- 使用
duration.toLocaleString(locale, options)(委托给Intl.DurationFormat)来显示本地化的面向用户的文本。 - 仅对机器可读的 ISO 8601 时长字符串(例如
P1M2DT4H)使用duration.toString()。
- 使用
示例:试用期到期时间差和本地化格式
// 1. Get the current time in an explicit target IANA time zone (or omit the
// argument to use the system's current time zone).
const tz = 'America/New_York';
const now = Temporal.Now.zonedDateTimeISO(tz);
// 2. Combine separate date and time strings directly.
// Do not build an ISO string with template literals; chain Temporal methods instead.
const startDateStr = '2025-01-01';
const startTimeStr = '12:00:00';
const endDateStr = '2025-01-31';
const endTimeStr = '12:00:00';
const start = Temporal.PlainDate.from(startDateStr).toPlainDateTime(startTimeStr).toZonedDateTime(tz);
const end = Temporal.PlainDate.from(endDateStr).toPlainDateTime(endTimeStr).toZonedDateTime(tz);
// 3. Calculate differences using .since() and .until()
// ZonedDateTime differences default to largestUnit: 'hour' (days are not a fixed
// length across DST, so Temporal won't balance into days unless asked). Pass
// largestUnit: 'day' or a calendar unit ('week', 'month', 'year') when you want a
// human-scale breakdown instead of e.g. PT720H. smallestUnit rounds away
// seconds and sub-second noise so the formatted string stays readable.
const timeActive = now.since(start, { largestUnit: 'year', smallestUnit: 'minute' });
const timeRemaining = now.until(end, { largestUnit: 'year', smallestUnit: 'minute' });
// 4. Format durations for display with .toLocaleString() (Intl.DurationFormat)
// instead of manually concatenating unit numbers and suffixes. 'en-US' is used
// here for deterministic output; pass undefined to use the user's locale.
console.log(`Active: ${timeActive.toLocaleString('en-US', { style: 'narrow' })}`);
console.log(`Remaining: ${timeRemaining.toLocaleString('en-US', { style: 'long' })}`);
// Use .toString() for ISO 8601 serialization (e.g., API payloads or <time datetime="...">)
console.log(`ISO 8601: ${timeRemaining.toString()}`);
// 5. Check status via .sign, and compare durations with Temporal.Duration.compare()
const isExpired = timeRemaining.sign < 0;
// Pass relativeTo when comparing durations that may include calendar months/years or DST shifts
const isPastHalfway = Temporal.Duration.compare(timeActive, timeRemaining, { relativeTo: now }) > 0;
示例:平衡独立时长(round 和 Intl.DurationFormat)
// Convert a raw unit count (e.g., 90 minutes) into a balanced breakdown (1 hour, 30 minutes)
const rawDuration = Temporal.Duration.from({ minutes: 90 });
const balanced = rawDuration.round({ largestUnit: 'hour' });
// Format via duration.toLocaleString() or directly with Intl.DurationFormat
const formatter = new Intl.DurationFormat('en-US', { style: 'long' });
console.log(formatter.format(balanced)); // "1 hour, 30 minutes"
策略性实施和最佳实践
- 务必使用
Temporal.ZonedDateTime进行涉及特定时区中发生的现实事件(例如订阅续订或事件倒计时)的计算,并在针对系统默认时区以外的时区进行计算时,将显式timeZone传递给Temporal.Now.zonedDateTimeISO(timeZone)。 - 务必使用 Temporal 方法(例如
Temporal.PlainDate.from(dateStr).toPlainDateTime(timeStr).toZonedDateTime(tz)或Temporal.PlainDate.from(dateStr).toZonedDateTime({ timeZone: tz, plainTime: timeStr }))组合单独的日期和时间字符串,而不是像`${dateStr}T${timeStr}`那样插值 ISO 字符串。 - 务必在
.since()、.until()或.round()中传递largestUnit(例如'day'、'month'、'year'),以便在小时之外平衡时长;ZonedDateTime差值方法默认为'hour'。 - 务必在比较两个时长时使用
Temporal.Duration.compare(d1, d2, { relativeTo }),只要其中一个时长涉及日历单位(months、years)或考虑时区的日期,就提供relativeTo锚点。 - 务必使用
duration.toLocaleString()或new Intl.DurationFormat(locale, { style })('long'、'short'、'narrow'或'digital')设置向用户显示的时长格式,而不是手动插值${duration.years}y ${duration.months}m...。 - 务必将
duration.toString()用于机器可读的 ISO 8601 时长字符串(例如PT1H30M),切勿将其用作面向用户的文案。 - 务必使用
.toPlainTime().toString({ smallestUnit: 'minute' })(而非字符串.slice(0, 5))将PlainTime呈现为HH:MM(例如,对于<input type="time">)。 - 请勿直接修改实例;
Temporal对象是不可变的。.add()、.subtract()、.round()或.with()等操作会返回一个新实例。 - 请勿回退到
Date/.getTime()算术来计算差值本身;Date没有时区感知日历数学,并且只有毫秒精度。 - 务必使用计算出的
Temporal.Duration(或Temporal.ZonedDateTime.compare())的原生.sign属性来检查时长是表示过去/已过期的时间点 (sign < 0) 还是表示未来待开始的时间点。
后备策略
Temporal 的回退和浏览器支持
对于尚不支持原生 Temporal API 的浏览器,请使用功能检测并动态加载 temporal-polyfill(约 15 KB min+gz)。
请注意,导入 temporal-polyfill 不会自动将 Temporal 对象分配给全局范围。如果您的代码依赖于 globalThis.Temporal,请明确分配它。
// Check if Temporal is supported natively
(async () => {
if (typeof Temporal === 'undefined') {
// Load the polyfill conditionally
const module = await import('https://esm.sh/temporal-polyfill');
globalThis.Temporal = module.Temporal;
// Extend Date.prototype if needed
Date.prototype.toTemporalInstant = module.toTemporalInstant;
}
initializeApp();
})();