在Javascript中处理时区和夏令时

23

我的单页JavaScript应用程序通过REST调用以JSON格式检索数据。日期使用标准的ISO8601格式(例如2011-02-04T19:31:09Z),使用UTC时区进行格式化。

注册服务时,用户从下拉列表中选择其时区。该时区可能与用户浏览器的时区不同。JavaScript应用程序始终知道用户选择的时区。

我知道如何将UTC字符串转换为日期。我了解JavaScript仅表示本地时区的日期。

但是,我在弄清楚如何显示格式化为用户本地时区之外的时区的日期方面遇到了麻烦。它必须考虑所有日期的DST。内部,我希望处理所有日期为UTC,并仅在显示时间将其转换为另一个时区的日期的字符串表示。我需要在用户资料中选择的时区中显示日期,而不是其浏览器的时区。

我尝试过服务器发送用户浏览器时区和用户资料时区之间的毫秒时差的时区偏移差异。但我发现不能只发送一个偏移值,而需要发送每个日期的偏移量来考虑DST的变化。

有关如何在各种时区格式化日期的建议或示例代码吗?到目前为止,我找到的选项有:

  1. 服务器将日期作为已经格式化为正确时区的字符串发送,客户端不进行任何日期解析或操作。这使得在客户端上执行某些操作变得困难甚至不可能。
  2. 使用诸如https://github.com/mde/timezone-js之类的库,其中包括整个Olson TZ数据库到JavaScript中。这意味着更长的加载时间,更多的内存使用等。
  3. 将timezoneOffsetMillis值与发送到客户端的每个日期一起发送。这会导致混乱的JSON数据和非最佳的REST接口。

有更简单或更好的解决方案吗?


它可能在你的应用程序中无法工作,但在Web上,通常更容易显示某物的年龄,而不是尝试可靠地显示用户时区的日期和时间。例如,在SO上,“6分钟前提问”。 - Alex Jasmin
@Alexandre -- 谢谢,我已经尽可能地这样做了,例如上次登录日期、发布日期等。但是我的应用程序需要显示发生在不同时区的特定日期和时间的事件。我不能使用相对时间或年龄来解决这个问题。 - Tauren
2
我肯定会选择timezone-js,这将是“正确”的做法。您还可以与以下时区检测脚本配对使用:https://bitbucket.org/pellepim/jstimezonedetect/. - Jon Nylander
@Jon,我同意这是正确的方法,但它肯定会增加一些臃肿。我希望JS从一开始就支持时区。感谢您指向jstimezonedetect。我以前看过它的演示页面,但忘记了。 - Tauren
在这种情况下,我会使用zoneinfo文件将tz工作卸载到服务器上,并通过json返回本地日期时间,格式化。 - Jon Nylander
3个回答

5

使用2是一个不好的想法,正如你所指出的那样,它会增加加载时间。如果我是你,我会结合1和3来做。我不认为这会使JSON数据混乱或REST接口非最优。

这是一个典型的权衡,在协议上接受一些复杂性,以简化客户端代码。


谢谢,但我有一个想法可以保持JSON的清晰。如果我将所有应该显示在用户本地时区的日期以UTC格式发送,例如2011-02-03T09:31:35Z,那会怎样?任何应该显示在特定时区的日期都将使用该时区的格式发送,例如2011-02-03T14:00:00-0800。当我最初解析日期时,我可以在日期对象的自定义属性中保留偏移量。我要试一试这个方法。 - Tauren
1
@Tauren:使用这种方法时,要注意客户端进行任何日期计算(添加或删除时间)时的自动时区转换。例如,如果您将24小时添加到日期中并跨越夏令时转换,则在转换为字符串时,时间将显示为偏移1小时。此外,在我们向后调整时钟时,JavaScript中的那个小时是不确定的。当我们向前调整时钟时,被跳过的那个小时是不可表示的(所有这些都假设本地时间遵循夏令时),即使那可能是用户选择的时区中的时间。 - Mike M. Lin

4

快进到2015年,当涉及到从其他时区格式化日期并使用特定的语言环境时,有两个选项。

在这个例子中,我使用法国语言环境,并将使用一个(UTC+5)的日期,即2016年3月27日2:15,这在西欧地区没有观察到(因为夏令时变化),时钟从2点到3点),这是一个常见的错误来源。

为了最大限度的可移植性,请使用moment.js库。Moment附带80多种语言环境

  1. Using timestamp and UTC offset:

    moment(1459026900000).utcOffset(300).locale('fr').format("LLLL")
    // "dimanche 27 mars 2016 02:15"
    
  2. Using array of integers (note that in moment, month is 0-based, just like in native JS Date object)

    moment.utc([2016,3-1,27,2,15]).locale('fr').format("LLLL")
    // "dimanche 27 mars 2016 02:15"
    

您可以通过在浏览器的开发工具上运行代码来测试这些方法 http://momentjs.com/

这里的棘手之处在于使用 moment.utc,它创建了一个带有 UTC 标志的时刻日期对象,这意味着当该日期被格式化时,它不会被转换为用户的时区,而只是按原样显示(并且由于 UTC 不遵循 DST,因此不容易出现 DST 错误)。

未来的本地化解决方案:使用 Intl 对象

(截至2015年末,Chrome、Firefox、IE11支持此解决方案,但Safari 9仍不支持)

使用整数数组:

    new Intl.DateTimeFormat('fr-FR', { timeZone: 'UTC', weekday: 'long',
          month: 'long', year: 'numeric', day: 'numeric', hour: 'numeric',
          minute: 'numeric' })
      .format(Date.UTC(2016,3-1,27,2,15))
    // "dimanche 27 mars 2016 02:15"

关键在于再次使用 Date.UTCtimeZone: 'UTC',以确保提供的日期不会被转换为用户的本地时区。
请注意,在这两种情况下,我们都使用UTC方法,只是为了确保日期将按原样使用而不进行转换。但重要的是要意识到这些日期是“假的UTC日期”(它们表示给定任意时区中的时间,而不是UTC时间),仅应用于日期格式化和显示 - 不应传递。

2019年注:还要检查 https://date-fns.org/,它比 moment.js 小得多,也许从捆绑大小的角度来看更合适。 - jakub.g

0

我也遇到了同样的问题。我猜解决方法是#1。你应该将日期的所有组成部分(年、月、日、小时)发送到客户端和服务器之间的特殊容器中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接