编程 容器里 TZ=UTC,SSR 日期少一天:Intl.DateTimeFormat 的默认值与边界

2026-11-01 00:03:59

容器里 TZ=UTC,SSR 日期少一天:Intl.DateTimeFormat 的默认值与边界

同一个 ISO 时间串,SSR 渲染出的日期和浏览器里差一天、差 8 小时,起因是容器默认 TZ=UTC,浏览器是 Asia/Shanghai。另一种症状是 0 点被格式化成 24:00,来自 hourCycle: 'h24'。

下面按「默认值 → 选项互斥 → 引擎差异」逐条列,每条给最小可复现代码和判据。

timeZone 默认取运行时区,不是 UTC

MDN 中文原文:「默认值是运行时的时区,与 Date.prototype.toString() 使用的时区相同」。这是规范行为。

docker run --rm -e TZ=UTC node:20-alpine node -e "const d=new Date('2025-11-01T00:00:00Z');console.log(new Intl.DateTimeFormat('zh-CN',{dateStyle:'medium'}).format(d))"

输出 2025年11月1日;本机 TZ=Asia/Shanghai 时,同一个串带上 timeStyle 会是 2025年11月1日 08:00。更典型的是 2025-10-31T17:00:00Z:上海显示 11 月 1 日,UTC 显示 10 月 31 日——差一天就出在这里。

判据:Intl.DateTimeFormat().resolvedOptions().timeZone 打印的是运行时区。SSR 侧必须显式传 timeZone,不能依赖默认值。

时区名还要写对:Asia/Beijing 不存在,会抛 RangeError: Invalid time zone specified: Asia/Beijing,北京在 IANA 里是 Asia/Shanghai。

dateStyle/timeStyle 与组件选项互斥

规范:指定了任何 date-time 组件选项(weekday、year 等)时,dateStyle/timeStyle 必须为 undefined。V8 报:

TypeError: Invalid option : option dateStyle and option weekday cannot be used at the same time

二选一:

new Intl.DateTimeFormat('zh-CN', { dateStyle: 'long', timeStyle: 'short' })
new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })

hourCycle:h24 下 0 点输出 24:00

取值 h11 / h12 / h23 / h24。hour12: true 映射到 h11 或 h12(视 locale),false 映射到 h23;h24 下午夜是 24:00。

new Intl.DateTimeFormat('en-US',{hour:'2-digit',minute:'2-digit',hourCycle:'h24',timeZone:'UTC'})
  .format(new Date('2025-01-01T00:00:00Z'))  // "24:00"

把 h24 换成 h23,得 "00:00"。排班、账单这类场景直接写 hourCycle:'h23'。

hour12:false → h23 是规范映射;早期 V8 对 hour12:false 给的是 h24,属引擎差异,需在目标 Node 版本上自行验证。另外 options 同时给 hour12 和 hourCycle 时,MDN 明确 hour12 会覆盖 hc 扩展键与 hourCycle。

small-icu 构建会静默退化成 en-US

Node 官方二进制带 full-icu,但 --with-intl=small-icu 或部分无服务端 runtime 只剩 en-US 数据。

判据两条:Intl.DateTimeFormat.supportedLocalesOf(['zh-CN']) 返回 [];process.config.variables.icu_small === true。

症状是不报错,zh-CN 静默失效,输出 11/1/2025 而不是 2025/11/1,toLocaleDateString('zh-CN') 同理。

Etc/GMT+8 的符号是反的

Etc/GMT+8 实际是 UTC−8,POSIX 遗产。固定东八区要么写 Asia/Shanghai(带 DST 历史与未来规则),要么写 +08:00 偏移标识符(无 DST,仅规范新增)。有 DST 的地区(如 America/New_York 3 月 / 11 月)只加偏移会错一小时。

没有 pattern 语法

不支持 'yyyy-MM-dd HH:mm:ss'。自定义串用 formatToParts 拼,parts 里 type 是 year/month/day/literal 等。它还能拿到 relatedYear,用于 chinese、hebrew 这类非公历日历。

日历差异也是数据层面的坑:ja-JP-u-ca-japanese 输出 24/12/19(平成/令和年号),th-TH 默认佛历,2025 年显示 2568。做年份对比、排序时不要直接比格式化后的字符串。

每行 new 一个 formatter

每次 new Intl.DateTimeFormat 都要解析 locale、建 formatter,比复用实例贵一到两个数量级(倍数需自行 benchmark)。列表页、日志页常见每行 new Intl.DateTimeFormat().format(...)。

修法是模块级 Map,按 locale + JSON.stringify(options) 缓存实例。

date-only 字符串再解析

new Date('2025-11-01') 按 UTC 解析,这是 ECMAScript 规范行为;new Date('2025-11-01 00:00:00')(空格分隔)不是规范格式,解析结果依引擎而定,老 Safari 返回 Invalid Date。只用 ISO 8601,并在格式化时显式给 timeZone。

formatRange 与 Temporal

formatRange 在某些 locale 下会合并区间(同日只写一次日期),跨月、跨年时分隔符与重复字段的处理各引擎不完全一致,账单周期显示要做快照测试。

新版规范里 format() 可接受 Temporal.PlainDate / PlainDateTime 等对象,MDN 已列出各类型的默认组件;但 Temporal 在 Node / 浏览器的可用性要看版本与开关,落地前先用 in 运算符检测。

修复顺序

  1. 数据层固定存 ISO 8601 UTC 字符串,不存格式化结果。
  2. SSR 与前端都显式写 timeZone。
  3. options 收敛到一个 format 模块,并缓存 formatter 实例。
  4. 加一条快照测试:固定 UTC 时间 + 固定 locale。
  5. 部署环境跑一次 supportedLocalesOf 自检。
复制全文 生成海报 Intl.DateTimeFormat JavaScript 时区 前端 i18n

推荐文章

程序员茄子在线接单