如何在网页上优雅显示本地时间

Posted by Frank Lin on Fri, Oct 24, 2025

最近在做一些传统的服务端渲染项目,后端直接出 HTML,没有现代前端构建流程。需求很简单:把数据库里的 UTC 时间戳显示成用户本地时间,统一格式为 YYYY-MM-DD HH:MM:SS TZ(例如 2025-10-24 10:30:00 PDT)。如果用 React/Vue,我可能会直接用 Luxondate-fns-tz 之类的库,但对于我的这种场景,不想为了格式化时间专门引入复杂的构建流程。

JavaScript 中提供了 toISOString()toGMTString() 两种格式:

< new Date().toISOString()
> '2025-10-23T17:30:02.427Z'
< new Date().toGMTString()
> 'Fri, 24 Oct 2025 17:30:05 GMT'

我认为,服务端最稳妥的做法是返回 ISO 8601 格式(RFC 3339 的子集)。它比 toGMTString() 更精确,能同时兼容秒和毫秒,而且用字符串比 Unix timestamp 少心智负担——不必猜是秒还是毫秒,也避免 int32 的溢出问题。

现在需求来了:将这类 ISO string 传到前端后,前端如何用尽可能简短的代码,重新将其格式化成前面所说的格式。似乎没有简单的一行调用、不用第三方库的方法,纯粹使用 JavaScript Date 内建方法渲染成上面这样的日期格式,同时附带上例如 PDTPST 这样时区缩写。

JS 中的 Date 对象 toString() 方法,以我的测试环境为例,默认渲染出来的字符串是

< new Date()
> Fri Oct 24 2025 10:10:40 GMT-0700 (Pacific Daylight Time)

这样的问题是:

  1. 太长(网页的表格里容易写不下);
  2. 是英文(不适用于英文环境下显示中文网页);
  3. 时区信息冗余且不容易心算。

为了将 UTC 时间转换为本地时间,我们先要拿到本地的时区信息。浏览器里提供了 Intl 这个方法,我们可以直接调用 Intl.DateTimeFormat().resolvedOptions().timeZone 来获得用户所在的标准时区名。

< Intl.DateTimeFormat().resolvedOptions().timeZone
> 'America/Los_Angeles'

这个没问题,可是我希望拿到的是 PDTPST 这样的时区缩写,得输入具体时间做一次转换。

< new Intl.DateTimeFormat(undefined, {
<     timeZone: 'America/Los_Angeles',
<     timeZoneName: 'short'
< }).format(new Date())
> '10/24/2025, PDT'

拿到时区后,我们再来格式化日期和时间。这里面用到了一个小技巧,瑞典的本地格式(sv-SE)中,时间的格式恰好是 YYYY-MM-DD 格式,可以省一点拼接的代码。

< new Date().toLocaleString('sv-SE', {
<     timeZone: 'America/Los_Angeles',
<     year: 'numeric',
<     month: '2-digit',
<     day: '2-digit',
<     hour: '2-digit',
<     minute: '2-digit',
<     second: '2-digit',
<     hour12: false
< });
> '2025-10-24 10:12:00'

结合了 Intl.DateTimeFormatsv-SE 的本地化格式,现在差不多就可以拼装起来了。我的完整实现如下:

 1// Format timestamp with timezone abbreviation
 2function formatTimestamp(utcTimeString) {
 3    try {
 4        const utcDate = new Date(utcTimeString);
 5        if (isNaN(utcDate.getTime())) {
 6            console.warn('Invalid time:', utcTimeString);
 7            return utcTimeString;
 8        }
 9
10        const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
11
12        // Get timezone abbreviation
13        const parts = new Intl.DateTimeFormat(undefined, {
14            timeZone: userTimeZone,
15            timeZoneName: 'short'
16        }).formatToParts(utcDate);
17        const timeZoneName = parts.find(part => part.type === 'timeZoneName')?.value || '';
18
19        // Format date and time in YYYY-MM-DD HH:mm:ss format
20        const dateTimeWithoutTZ = utcDate.toLocaleString('sv-SE', {
21            timeZone: userTimeZone,
22            year: 'numeric',
23            month: '2-digit',
24            day: '2-digit',
25            hour: '2-digit',
26            minute: '2-digit',
27            second: '2-digit',
28            hour12: false
29        });
30
31        return dateTimeWithoutTZ + ' ' + timeZoneName;
32    } catch (error) {
33        console.warn('Time conversion failed:', utcTimeString, error);
34        return utcTimeString;
35    }
36}

这样,在前端显示后端来的时间戳时候,可以直接把这些包在一个 span 或者 div 里,给它一个一致的 class 名,就能一次性把所有这类全转换成我们想要的格式了。当然,对于日期较多的网页,可以把 Intl.DateTimeFormat 实例抽取出来复用,避免重复计算。

当然这个格式也不是完全没有问题,比如 CST 可能代表「美国中部标准时间」也可能是「中国标准时间」。类似的情况也有不少。不过,从用户自己的角度出发、dashboard 上渲染自己地区所在时间,不涉及其他时区的话,这个歧义的问题不太明显。如果你要在国际协作的系统中使用,推荐用 timeZoneName: ’longGeneric’,显示成 “Pacific Time” 或者 GMT-7 这种无歧义写法。我个人在单时区 dashboard 类项目上仍然偏好缩写形式,节省空间且易读。

附注:关于瑞典“文明国”梗

给一位同事看了这个代码,果然对 sv-SE 提出了疑问。当我告诉他瑞典的日期格式后,他表示:“Blows my mind! Turns out that Sweden is the only civilized country in the world!”

瑞典的日期格式确实是全球少数严格遵守 ISO 8601 的,本地化最“文明”的国家之一——也难怪成了开发者口耳相传的小彩蛋。显示时间的通用解法,也许永远都只是“局部最优”。

附注2:为什么选择 sv-SE 而不是 en-CA 等其他 locale?

在早期浏览器(尤其是 Chrome / Node 12 / 旧 Edge)中,sv-SE 是极少数 在所有主流实现中都返回纯数字连字符格式(YYYY-MM-DD) 的 locale。而 en-CA 在早期有时会输出类似 2025-10-24, 10:12:00 a.m.,或者在某些环境下带上 AM/PM 标识,具体依赖操作系统的区域数据(CLDR 版本)。

换句话说,sv-SE 早年是更可预测、更稳定的选择。很多 Stack Overflow 上的回答、GitHub 代码片段都是在那个年代形成的共识。现在情况不一样了,2020 年之后,采用 en-CA 之类的 locale 也可以达到类似的效果。对于 SSR 项目的这种小函数,“能用就别动”比“政治正确”更重要。即使未来 CLDR 更新导致瑞典改格式(几乎不可能),worst case 是显示稍有偏差,不会导致功能失效。对于内部工具和小项目,这个风险完全可以接受。

据说某些老版本的 Safari 在某些 timeZoneName 情况下可能返回空字符串,不过我暂时没有收到这类反馈。

防抬杠预警:本文提到的方法适用场景

适合:

  • 传统 SSR 项目(Django/Flask/Rails/PHP 等)
  • 内部工具、后台管理系统
  • 零构建流程的简单页面
  • 单时区用户场景

不适合:

  • 多时区协作系统(建议用 Luxon 的 setZone()
  • 需要复杂时区计算(时差、夏令时转换等)
  • 现代前端项目(直接 npm install date-fns-tz)

总结

没有完美的时间格式,但有足够优雅的折中方案。

40 行原生 JS 就能解决 95% 的显示问题,剩下 5%,交给更复杂的库吧。



comments powered by Disqus