如何在 React Router v7 中处理时区

以用户的本地时区显示时间

问题

当应用程序在显示时间时未考虑用户的所在地,会导致混淆和错误。例如,服务器可能将事件时间存储为 "2024-03-15T20:00:00Z",并直接从该 UTC 值渲染为 "8:00 PM"。不同时区的用户看到相同的时钟时间,但会将其解释为他们的本地时间,从而导致错过预约和日程冲突。根本问题在于,同一时刻在不同的地理位置有不同的表示方式,而在错误的时区显示时间会使其变得毫无意义或具有误导性。

当用户跨地区协作时,这一问题会更加严重。例如,服务器上安排在下午 3:00 的事件,在用户界面中会显示为各地的下午 3:00,尽管同一时刻在东京应显示为早上 6:00,在伦敦应显示为下午 2:00,在纽约应显示为早上 9:00。

解决方案

将所有时间戳存储为通用格式,例如带有 UTC 指示符的 ISO 8601 字符串或 Unix 时间戳。在向用户显示这些时间时,将其转换为 JavaScript 的 Date 对象,并使用尊重浏览器时区的国际化 API 进行格式化。现代浏览器会自动检测用户的本地时区,格式化库利用这一点将 UTC 时间戳转换为正确的本地表示,而无需手动计算偏移量。

这种方法将存储与展示分离。服务器发送一个规范的时间戳,每个客户端根据其自身的时区渲染时间,确保每个人都能看到同一全球时刻的正确本地时间。

步骤

1. 从数据源以 ISO 8601 格式发送时间戳

确保您的 API 或数据加载器返回的时间戳为带有 UTC 指示符的 ISO 8601 字符串或以毫秒为单位的 Unix 时间戳。

export async function loader() {
  const event = await fetchEvent();
  return {
    title: event.title,
    startTime: "2024-03-15T20:00:00Z",
    endTime: "2024-03-15T22:00:00Z",
  };
}

Z 后缀表示 UTC 时间。当此字符串被解析为 JavaScript 的 Date 对象时,浏览器会将其内部存储为 UTC 时间戳。

2. 创建一个使用 react-intl 格式化时间的组件

从 react-intl 导入格式化组件,并传递由 ISO 字符串创建的 Date 对象。

import { FormattedDate, FormattedTime } from "react-intl";
import type { Route } from "./+types/event";

export default function EventDetails({ loaderData }: Route.ComponentProps) {
  const startTime = new Date(loaderData.startTime);
  const endTime = new Date(loaderData.endTime);

  return (
    <div>
      <h1>{loaderData.title}</h1>
      <p>
        <FormattedDate
          value={startTime}
          year="numeric"
          month="long"
          day="numeric"
        />
        {" 于 "}
        <FormattedTime value={startTime} />
        {" 到 "}
        <FormattedTime value={endTime} />
      </p>
    </div>
  );
}

FormattedDateFormattedTime 组件会自动使用浏览器检测到的时区来显示正确的本地时间。纽约的用户会看到 "2024 年 3 月 15 日 下午 3:00",而东京的用户会看到 "2024 年 3 月 16 日 上午 5:00",对于同一个 UTC 时间戳。

3. 对于动态格式化需求,使用命令式 API

当您需要在属性、计算值或非 JSX 上下文中格式化时间时,可以使用 useIntl 钩子。

import { useIntl } from "react-intl";
import type { Route } from "./+types/event";

export default function EventCard({ loaderData }: Route.ComponentProps) {
  const intl = useIntl();
  const startTime = new Date(loaderData.startTime);

  const formattedDate = intl.formatDate(startTime, {
    year: "numeric",
    month: "short",
    day: "numeric",
  });

  const formattedTime = intl.formatTime(startTime, {
    hour: "numeric",
    minute: "2-digit",
  });

  return (
    <div>
      <h2>{loaderData.title}</h2>
      <time dateTime={loaderData.startTime}>
        {formattedDate} 于 {formattedTime}
      </time>
    </div>
  );
}

formatDateformatTime 方法会根据用户的语言环境和时区返回格式化的字符串。dateTime 属性保留了原始的 ISO 字符串以便机器读取,而显示的文本则展示了用户友好的本地时间。