如何在 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 hook,访问命令式格式化 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 字符串可以保留精确的时间点,使客户端格式化能够准确转换为任意时区。