如何在 Next.js (Pages Router) v16 中处理时区

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

问题

当应用程序在显示时间时未考虑用户的所在地,会引发混淆。一个安排在“下午 3:00”的会议,对于身处纽约、伦敦或东京的用户来说,意味着不同的时间点。如果服务器以 UTC 或其自身的时区渲染时间,其他地区的用户会看到错误的本地时间,从而导致错过会议和排程错误。同一个时间戳必须根据每个用户的时区进行不同的解释。

解决方案

将时间戳存储为通用格式,例如 ISO 8601 或 Unix 时间戳,然后在客户端根据用户的本地时区格式化显示。使用浏览器的国际化 API 检测用户的时区,并将其传递给 react-intl 的格式化函数。这确保了每个用户都能看到调整为其自身时区的时间,从而消除事件发生时间的歧义。

步骤

1. 在客户端检测用户的时区

浏览器的 Intl.DateTimeFormat().resolvedOptions().timeZone 返回用户的 IANA 时区标识符,例如 America/New_YorkEurope/London。创建一个自定义 hook 来获取此值。

import { useState, useEffect } from "react";

export function useUserTimeZone() {
  const [timeZone, setTimeZone] = useState<string>("UTC");

  useEffect(() => {
    const detected = Intl.DateTimeFormat().resolvedOptions().timeZone;
    setTimeZone(detected);
  }, []);

  return timeZone;
}

此 hook 仅在客户端运行,避免了水合不匹配问题,并在检测到时区之前默认使用 UTC。

2. 使用 FormattedDate 按用户时区格式化日期

timeZone 选项传递给 react-intl 的格式化方法,以控制用于显示的时区。使用带有检测到的时区的 <FormattedDate> 组件。

import { FormattedDate } from "react-intl";
import { useUserTimeZone } from "../hooks/useUserTimeZone";

interface EventDateProps {
  timestamp: string;
}

export function EventDate({ timestamp }: EventDateProps) {
  const userTimeZone = useUserTimeZone();

  return (
    <FormattedDate
      value={new Date(timestamp)}
      timeZone={userTimeZone}
      year="numeric"
      month="long"
      day="numeric"
      hour="numeric"
      minute="2-digit"
      timeZoneName="short"
    />
  );
}

timeZone 属性确保日期以用户的本地时区而非服务器或 UTC 格式化显示。

3. 使用 useIntl 以命令式格式化时间

在需要格式化字符串而不是组件的场景中,可以使用 useIntl 钩子访问命令式格式化 API。

import { useIntl } from "react-intl";
import { useUserTimeZone } from "../hooks/useUserTimeZone";

interface MeetingTimeProps {
  startTime: string;
}

export function MeetingTime({ startTime }: MeetingTimeProps) {
  const intl = useIntl();
  const userTimeZone = useUserTimeZone();

  const formattedTime = intl.formatDate(new Date(startTime), {
    timeZone: userTimeZone,
    hour: "numeric",
    minute: "2-digit",
    timeZoneName: "short",
  });

  return <span title={startTime}>{formattedTime}</span>;
}

formatDate 方法接受一个包含 DateTimeFormatOptions 的第二个参数,其中包括 timeZone,从而可以完全控制时间戳的显示方式。

4. 将服务器时间戳作为 ISO 字符串传递

getServerSideProps 中,在请求时获取数据并将其作为 props 传递给页面组件。将日期序列化为 ISO 8601 字符串以保留时区信息。

import { GetServerSideProps } from "next";

interface Event {
  id: string;
  title: string;
  startTime: string;
}

interface EventPageProps {
  event: Event;
}

export const getServerSideProps: GetServerSideProps<
  EventPageProps
> = async () => {
  const event = {
    id: "1",
    title: "Team Meeting",
    startTime: new Date("2025-02-15T15:00:00Z").toISOString(),
  };

  return {
    props: { event },
  };
};

export default function EventPage({ event }: EventPageProps) {
  return (
    <div>
      <h1>{event.title}</h1>
      <EventDate timestamp={event.startTime} />
    </div>
  );
}

ISO 字符串保留了精确的时间点,允许客户端格式化将其准确转换为任何时区。