JavaScript中如何处理时区?

1,866 阅读10分钟

在做前后端日志全链路的时候,遇到了前后端时区不一致的情况,就查了些资料学习下,下面就以一篇外网基础文章为主,结合个人知识学习给大家尽可能系统介绍下。


 当我们创建 Web 应用时,我们主要考虑两种类型的时区,其中一个最常见的是服务器时间,用于追踪日期时间的参考手段。

然而有一些案例中,我们需要追踪用户的日期时间。在这些场景中,我们需要使用JavaScript来截取用户浏览器的时间,而且这些用户可能分布在不同的时区,因此处理它需要一点小技巧。

本篇文章将会讨论时区的相关概念和JavaScript的处理方法函数。

Time Zones GMT, Offset, UTC

image.png 不同时区的时钟

在进入细节之前,我们先了解时区的一些专业术语。

1、什么是时区?

一个时区是一个遵循国家法律规定统一当地时间的地区,在一些大国都是自己独特时区的,例如USA,甚至会有多个时区。当然也有统一一个时区的,可能会导致尴尬场景,例如在早上10点中国西部才会迎来日出。

2、GMT - Universal Reference

韩国本地时间正常是 CMT +09:00。这里的 GMT 是格林威治时间(Greenwich Mean Time)的缩写,采用的是位于纬度0的英国皇家天文台的时钟时间。GMT系统起始于Feb. 5, 1925,在 Jan. 1, 1972成为世界时间的标准。

在中国本地时间是GMT +8小时,纽约则是冬季GMT-5小时、夏季GMT-4小时。

3、UTC - Better alternative to GMT

很多人认为GTM和UTC是相同的,而且在很多场景下,两者是可以互换的,但是他们实际是不同的。UTC在1972年创建,用于弥补地球自转逐渐变慢的问题。该时间系统基于国际原子时,使用铯原子的频率来确定时间标准。换而言之,UTC是GMT的精确替代系统,两者实际差别是很小的,因此UTC才是程序开发者的更精确的选择。

关于UTC命名还有段有趣故事:在该系统开发阶段,英语系想命名为CUT(Coordinated Universal Time),法语系则想命名为TUC(Temps Universal Coordonn)。双方最终谁也没能说服对方,最终达成妥协:UTC,因为这个名字含有所有的必要关键词(C、T 和 U)。

4、Offset - Difference from Reference

UTC +9:00 中的 +9:00 意味着本地时间是要比UTC标准时间早9个小时,这种差别称之为“Offset”,它也是存在负值表达式的:-03:00。

国家采用自己独特的时间系统是很常见的,例如航过韩国标准时间被称为KST(Korea Standard Time),它指代一个固定时间偏移,因此KST也被表达为 UTC+9:00。

image.png 以中国为例,在JavaScript中可以通过上述方式获得时区的偏移,单位是分钟,中国刚好是早8个小时。

完整的时区偏移列表和它们的名称可以参考《List of UTC time offsets》。

5、Time Zone !== offset?

正如我之前提到的,我们可以使用时区名和“Offset”互换,并且不需要特意区分它。但是如果将两者混为一谈则是不对的。理由如下:

  • Summer Time(DST)
    尽管对于很多国家来说这个名词很陌生,世界上很多国家都接受了夏季时间的,主要是在英国等欧洲国家。国际上正式统称为:Daylight Saving Time(DST) ,例如美国加利福尼亚州冬季使用PST时区,夏季使用PDT市区(UTC-07:00)相差一个小时,别问为啥知道这个,跨国恋这个都是常识v_v。
  • Does Time Zone Change?
    每个国家都有选择使用哪个时区的权利,因此以为着这个会随着政治和经济调整,例如美国在《能源政策法案》更改了美国夏时制的日期。
  • Time Zone 1: offset N
    一个时区也会对应多个时间偏移。

6、JavaScript and IANA Time Zone Database

IANA Time Zone Database -时区信息数据库,是一个主要应用于电脑程序以及操作系统的,可协作编辑世界时区信息的数据库。主要记录时区以及时区变更,也是使用频率最高的数据库。下面是它的数据库文件格式。

# This file contains a table with the following columns:
# 1.  ISO 3166 2-character country code.  See the file `iso3166.tab'.
# 2.  Latitude and longitude of the zone's principal location
#     in ISO 6709 sign-degrees-minutes-seconds format,
#     either +-DDMM+-DDDMM or +-DDMMSS+-DDDMMSS,
#     first latitude (+ is north), then longitude (+ is east).
# 3.  Zone name used in value of TZ environment variable.
# 4.  Comments; present if and only if the country has multiple rows.
#
# Columns are separated by a single tab.
# The table is sorted first by country, then an order within the country that
# (1) makes some geographical sense, and
# (2) puts the most populous zones first, where that does not contradict (1).

JavaScript中关于时区的技术是相当缺乏的,一旦默认遵循地区时区(操作系统初始化选择语言和时区),就再也没有任何方法去修改了。并且他的数据库标准目前也尚未明确,如果你仔细查看ES2015文档时,你会发现仅仅有几行且含义不明确的定义来介绍本地时区和DST的可用性。例如,DST被定义为以下:ECMAScript 2015 — Daylight Saving Time Adjustment

An implementation dependent algorithm using best available information on time zones to determine the local daylight saving time adjustment DaylightSavingTA(t), measured in milliseconds. An implementation of ECMAScript is expected to make its best effort to determine the local daylight saving time adjustment.\

JavaScript Date对象

在服务器端,为了保持避免多时区影响,服务器必须存储绝对时间,常见会被存储为Unix time(基于UTC)和 ISO-8601(包含Offset信息)两种,例如韩国首尔 2017-03-11 11:30:00,在ISO-8601则展示为2017–03–11T11:30:00+09:00,在Unix Time格式下则被展示为1489199400(单位秒)。Mark: Unix time单位是秒而不是毫秒,这也是前后端格式常见错误之一。

在JavaScript中,涉及到日期和时间的任务都是要使用“Date”对象来处理,它是ECMScript定义的原生对象,底层是C++实现的,API文档MDN Documents。风格受 Java 的 java.utils.Date 类影响。因此继承了不受欢迎的特点,例如数据可变和month字段枚举开始是0。

JavaScript的“Date”对象内部使用绝对数值(例如Unix time)来管理,但是构建和方法例如 parse()、getHour() 等方法是受客户端的本地时区影响的。因此在浏览器创建时间对象会直接受本地时区的影响。

const d1 = new Date(2017, 2, 11, 11, 30);
d1.toString(); //Sat Mar 11 2017 11:30:00 GMT+0800 (中国标准时间)

 new Date 内部会调用 Date.parse() 方法,这个方法支持 RFC2888 协议和 ISO-8601协议。然而如《MDN’s Date.parse() Document》描述,不同浏览器返回的数值也是千差万别的,例如“2015-10-12 12:00:00”时间串在Safari浏览器则会返回 NaN,IE、Chrome、FireFox则返回相同的值。

由于服务器可能会采用Unix time,Date.parse()只接受h毫秒时间,因此需要进行1000的乘法。

const d1 = new Date(1489199400 * 1000);
d1.toString(); // Sat Mar 11 2017 10:30:00 GMT+0800 (中国标准时间)

 如果采用的是ISO-8601呢?正如前文所说,Date.parse()不稳定也不推荐使用。但是自从ECMAScript 5及之后版本支持ISO-8601,你可以使用 ISO-8601格式的日期字符串来初始化(eg:IE 9或者支持ES5的更高版本)。如果你的浏览器不是最新版本,请在字符串后增加Z字符,否则旧版本浏览器有时会创建并不是基于UTC的时间对象,下面是在IE10上例子。

const d1 = new Date('2017-03-11T11:30:00');
const d2 = new Date('2017-03-11T11:30:00Z');
d1.toString(); // "Sat Mar 11 11:30:00 UTC+0900 2017"
d2.toString(); // "Sat Mar 11 20:30:00 UTC+0900 2017"

 时间如何传输给后台呢?直接上代码。

// Unix time 时间格式
const d1 = new Date(2017, 2, 11, 11, 30);
d1.getTime(); // 1489203000000 别忘记毫秒单位

// ISO-8601 时间格式
const d1 = new Date(2017, 2, 11, 11, 30);
d1.toISOString(); // 2017-03-11T03:30:00.000Z
d1.toJSON();       // 2017-03-11T03:30:00.000Z

// UTC GMT 时区
d1.toGMTString() // Sat, 11 Mar 2017 03:30:00 GMT
d1.toLocaleString() // 2017/3/11 上午11:30:00

 修改时区

我们都清楚JavaScript是无法修改时区?但是在应用中,我又期望改变时区,去统一展示时间格式。

在早期,我会通过构建具有特定offset的Date对象来进行展示,例如下面的代码。

假设在韩国首尔看到在纽约的本地时间,首尔的当前时间是 11:30 Mar 11, 2017。

function formatDate(date) {
  return date.getFullYear() + '/' + 
    (date.getMonth() + 1) + '/' + 
    date.getDate() + ' ' + 
    date.getHours() + ':' + 
    date.getMinutes();
}
const seoul = new Date(1489199400000);
const seoulTimeoffset = seoul.getTimeZoneOffset(); // -540 minutes +9:00
// New York 时区是 -5:00
const NewYorkTimeoffset = 5 * 60;
const timeoffset = seoulTimeoffset - NewYorkTimeoffset;

// 840 来源于
const seoul = new Date(1489199400000);
const ny = new Date(1489199400000  (timeoffset * 60 * 1000));

formatDate(seoul);  // 2017/3/11 11:30
formatDate(ny);     // 2017/3/10 21:30

 简单来说就是计算两地时区的offset,然后本地时间减去offset,然后格式化即可。

这种方案实际是隐藏深层次的问题,首先本质你依旧是本地时区,因此你无法通过简单的“setXXX”操作,来获取对应的服务器时间,你依旧需要进行offset的计算,且前文提供过时区的不一致性,例如夏令时的概念,你依旧需要手动去区分正反向和特定时间转换,本质上依旧十分不稳定。

更好的方案则是使用 Moment.js API,它提供非常丰富的关于时间和日期的API,其中就包含大量时区的API,且几乎已经成为标准了。

const seoul = moment(1489199400000).tz('Asia/Seoul');
const ny = moment(1489199400000).tz('America/New_York');

seoul.format(); // 2017-03-11T11:30:00+09:00
ny.format();    // 2017-03-10T21:30:00-05:00

seoul.date(15).format();  // 2017-03-15T11:30:00+09:00
ny.date(15).format();     // 2017-03-15T21:30:00-04:00

 Moment.js 是基于 Luxon 库,Luxon 和 date-fns-tz(产生了 date-fns 库)是国际通用的时间库,这里有个小细节则是Moment.js 虽然是大多数人熟悉和使用的,但是其本身已经退役了,Moment.js团队建议未来使用 Luxon 来进行开发。因此我在这里推荐大家去使用 date-fns 库。

date-fns 本身比较小,只有140个左右方法,风格类似 Lodash 的 dates,也兼容了 CommonJS 和 ES Modules,方便跨端使用。

//Import the function initially
const {format} = require('date-fns');
//today's date
const today =format(new Date(),'dd.MM.yyyy');
console.log(today);

 结语

本文到这里为止,已经介绍有关时区概念、JavaScript对时区的支持、第三方时间库以及其隐藏的问题,虽有疏漏,但是也是在软件开发方向精益求精的一种敦促,也希望能够给大家提供参考,在面临技术决策时,能够作出正确的决策。