如何在 TanStack Start v1 中处理时区
以用户的本地时区显示时间
问题
当应用程序显示时间时,它们表示的是一个单一的时刻,但根据查看者的位置不同,显示的时间会有所不同。如果服务器以其自身的时区或 UTC 渲染“8:00 PM”,不同时区的用户将看到不正确的本地时间,从而导致对事件实际发生时间的混淆。用户期望时间能够自动反映其本地时区,但如果没有正确处理,他们会看到服务器在渲染时使用的时区时间。这种不匹配会导致用户错过预约、误解日程安排,并对应用程序的可靠性失去信任。
解决方案
将 timeZone 选项传递给日期格式化方法,以控制日期的解释和显示方式。formatDate 函数接受包含时区配置的 Intl.DateTimeFormatOptions。以通用格式(如 ISO 8601 字符串或 Unix 时间戳)存储时间戳,然后在客户端进行格式化,此时浏览器的时区会自动可用。当未指定 timeZone 选项时,Intl.DateTimeFormat API 使用用户的默认时区,确保时间能够正确显示为每个用户的位置,而无需手动检测时区。
步骤
1. 创建一个支持时区的日期格式化组件
使用 useIntl 钩子访问 intl 对象及其 formatDate 方法。此组件接受一个 ISO 时间戳,并将其格式化为用户的本地时区。
import { useIntl } from "react-intl";
interface LocalTimeProps {
timestamp: string;
showDate?: boolean;
showTime?: boolean;
}
export function LocalTime({
timestamp,
showDate = true,
showTime = true,
}: LocalTimeProps) {
const intl = useIntl();
const date = new Date(timestamp);
const formatted = intl.formatDate(date, {
...(showDate && {
year: "numeric",
month: "short",
day: "numeric",
}),
...(showTime && {
hour: "numeric",
minute: "2-digit",
timeZoneName: "short",
}),
});
return <time dateTime={timestamp}>{formatted}</time>;
}
FormattedDate 组件和 formatDate 方法使用了带有 DateTimeFormatOptions 的 Intl.DateTimeFormat API。当未显式提供 timeZone 时,浏览器会自动应用用户的时区,将 UTC 时间戳转换为本地时间。
2. 使用组件显示事件时间
在需要显示时间戳的地方导入并使用 LocalTime 组件。该组件会根据每个用户的浏览器设置自动处理时区转换。
import { createFileRoute } from "@tanstack/react-router";
import { LocalTime } from "~/components/LocalTime";
export const Route = createFileRoute("/events/$eventId")({
component: EventDetail,
});
function EventDetail() {
const event = Route.useLoaderData();
return (
<div>
<h1>{event.title}</h1>
<p>
开始时间:<LocalTime timestamp={event.startTime} />
</p>
<p>
结束时间:<LocalTime timestamp={event.endTime} />
</p>
</div>
);
}
每个用户都会看到转换为其本地时区的事件时间。来自服务器的 ISO 时间戳会根据用户的语言环境和时区偏好进行解析和格式化。
3. 格式化时间并显示明确的时区
当用户需要知道时间所代表的时区时,可以在格式化选项中包含时区名称。
import { useIntl } from "react-intl";
interface ExplicitTimezoneProps {
timestamp: string;
timezone?: string;
}
export function ExplicitTimezone({
timestamp,
timezone,
}: ExplicitTimezoneProps) {
const intl = useIntl();
const date = new Date(timestamp);
const formatted = intl.formatDate(date, {
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
timeZone: timezone,
timeZoneName: "long",
});
return <time dateTime={timestamp}>{formatted}</time>;
}
指定 timeZone 选项会强制格式化器以特定时区显示时间。当 timezone 未定义时,浏览器会使用用户的本地时区。这对于无论用户身在何处都需要显示特定时区的时间非常有用。
4. 创建一个用于显示相对时间的辅助工具
对于最近的事件,可以显示类似“2 小时前”的相对时间,同时在工具提示中保留绝对时间。
import { useIntl } from "react-intl";
interface RelativeTimeProps {
timestamp: string;
}
export function RelativeTime({ timestamp }: RelativeTimeProps) {
const intl = useIntl();
const date = new Date(timestamp);
const now = Date.now();
const diffInSeconds = Math.floor((now - date.getTime()) / 1000);
const absoluteTime = intl.formatDate(date, {
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
let value: number;
let unit: Intl.RelativeTimeFormatUnit;
if (diffInSeconds < 60) {
value = -diffInSeconds;
unit = "second";
} else if (diffInSeconds < 3600) {
value = -Math.floor(diffInSeconds / 60);
unit = "minute";
} else if (diffInSeconds < 86400) {
value = -Math.floor(diffInSeconds / 3600);
unit = "hour";
} else {
value = -Math.floor(diffInSeconds / 86400);
unit = "day";
}
const relativeTime = intl.formatRelativeTime(value, unit);
return (
<time dateTime={timestamp} title={absoluteTime}>
{relativeTime}
</time>
);
}
相对时间会更新显示以呈现更人性化的持续时间,而 title 属性则保留了精确的本地时间,供用户悬停查看。两种格式都会自动遵循用户的时区和语言环境。