搞定前端日期时间

890 阅读8分钟

如果服务端返回时间串比如 2022-04-16T23:05:26.000ZSun Apr 17 2022 14:05:26 GMT+0800,需要转换为北京时间显示,应该怎样处理?如果返回 2022/04/28 16:00:00,且已知该字段都使用 utc 零时区标准储存,按北京时间在浏览器显示,又该怎样处理呢?

前言

image.png
由于世界各国和地区所处经度不同,每15经度划分一个时区,总共24个时区,各地区对应的本地时间和时区相关。并且存在不同的时间标准,例如我国使用 UTC 东八区的时间,但很多国外地区使用夏令时,每个地区的标准也不尽相同,涉及夏令时的时间转换只能 case by case 处理。日期还有不同的格式表示,对前端来说,可能会面对不同时区、日期格式在不同时间标准下的转换处理,稍有不慎就容易出现误差。

常见的时间标准

按标准类别来分主要有以下三种,但对于前端日期时间处理可以归纳为两类,国际标准时间与夏令时。GMT 与 UCT都属于国际标准时间,我们能通过时间库非常方便转换为当地时间;夏令时则与不同国家和地区的政策有关,不能按通用的方式转换为当地时间。

1、GMT 格林尼治标准时间
GMT是指位于英国伦敦的皇家格林尼治天文台的标准时间,本初子午线被定义在通过那里的经线,由于地球每天的自转有细微的不规则,而且正在缓慢减速,不再被作为标准时间使用。现在的主要标准时间使用 UTC(协调世界时间)。

2、UTC 协调世界时间
UTC 是最主要的世界时间标准,由原子钟提供,经过平均太阳时、地轴运动修正 GMT格林尼治标准时间,以「秒」为单位的国际原子时所综合精算而成的时间。一般情况认为UTC和GMT是相等的

3、DST 夏令时
DST 夏令时,是一种在夏季月份牺牲正常的日出时间,而将时间调快的做法。通常使用夏时制的地区,会在接近春季开始的时候,将时间调快一小时,并在秋季调回正常时间。实际上,夏时制会造成在春季转换当日的睡眠时间减少一小时,而在秋季转换当日则会多出一小时的睡眠时间。在与国外服务对接中,可能会涉及夏令时。

常见的日期格式

日期时间表示多种多样,我们需要了解主流日期格式的规范。非 ISO 标准格式的时间处理可能出现偏差。

ISO 8601
是日期和时间表示的国际标准。ISO 8601 描述了大量的日期与时间的格式。为了减少出错可能和复杂性,对支持的格式进行了限制。我们尽量使用 ISO 标准时间格式

规范:

格式说明示例
YYYY四位数2022
MM两位数(一位数是不符合标准的)04
DD两位数(一位数是不符合标准的)17
小时hh两位数(24小时制)08
分钟mm两位数36
ss两位数22
秒的小数部分s一个或多个ss.s 示例 22.45
时区指示符TZDZ 或 +hh:mm 或 -hh:mm
1. UTC(协调世界时)表示,带有一个特殊的 UTC 指示符(“Z”)
2. +hh:mm 或 -hh:mm 为时区偏移量

完整的示例:
YYYY-MM-DDThh:mm:ss.sTZD(例如 2022-03-16T19:20:30.45+08:00)

RFC 2822
是一种用于在HTTP 和电子邮件标题等位置统一表示日期和时间的互联网信息格式,例如 Sun Apr 17 2022 14:05:26 GMT+0800 (中国标准时间)。

规范:

说明示例
星期几其中之一
"Mon" / "Tue" / "Wed" / "Thu" / "Fri" / "Sat" / "Sun"
Sun
其中之一:
"Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" / "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec"
Apr
一位或两位数17
四位数2022
小时两位数15
分钟两位数05
两位数26
时区指示符+四位数 或 -四位数+0800

JS API 对不同时间标准和日期格式的处理

new Date().toString() 
// Sun Apr 17 2022 14:05:26 GMT+0800 (中国标准时间)

通过 Date 的实例调用 toString 方法,返回得到一个英语日期格式的字符串,整个字符串提供了哪些信息?
首先这段字符串使用了 RFC2822 的日期格式标准。
1、Mon Apr 17 2022 14:05:26 是对日期时间的描述
2、GMT+0800 (中国标准时间) 使用GMT格林尼治标准时间,+0800 描述了相对于格林尼治天文台 0时区加了 8小时,即已转换为了东八区(北京时间)展示。

new Date('Mon Apr 17 2022 14:05:26 GMT+0800 (中国标准时间)').toGMTString()
// Sun, 17 Apr 2022 06:05:26 GMT

通过 Date 的实例调用 toGMTString 方法,和上一个例子类似,得到一个英语日期格式的字符串,但是日期时间描述是基于 0时区的 GMT格林尼治标准时间。

new Date('Mon Apr 17 2022 14:05:26 GMT+0800 (中国标准时间)').toUTCString()
// Sun, 17 Apr 2022 06:05:26 GMT

通过 Date 的实例调用 toUTCString 方法,有意思的是和调用 toGMTString 方法返回一模一样,仍然显示GMT输出,一般可以认为UTC 和 GMT是相等的(UTC 对GMT做了误差校准)。

new Date('Mon Apr 17 2022 14:05:26 GMT+0800 (中国标准时间)').toISOString()
// 2022-04-17T06:05:26.000Z

通过 Date 的实例调用 toISOString 方法,得到一个基于 ISO 标准 0时区的日期时间字符串。

日期处理时容易踩的坑

先看一个例子,对于同一个日期不同的格式,浏览器解析出来的时间也可能存在差异,我们应该尽量避免使用 Date 构造解析日期字符串。

new Date('2022-04-17')
// Sun Apr 17 2022 08:00:00 GMT+0800 (中国标准时间)

new Date('2022-4-17')
// Sun Apr 17 2022 00:00:00 GMT+0800 (中国标准时间)

from MDN: 由于浏览器之间的差异与不一致性,强烈不推荐使用Date构造函数来解析日期字符串 (或使用与其等价的Date.parse)。对 RFC 2822 格式的日期仅有约定俗成的支持。 对 ISO 8601 格式的支持中,仅有日期的串 (例如 "1970-01-01") 会被处理为 UTC 而不是本地时间,与其他格式的串的处理不同。

在没指定时区的情况下,2022-04-17(符合 ISO 8601)被当做 UTC 0时区处理,转换成东八区,所以加上了8小时,并补上时区指示符;2022-4-17(符合 RFC2822)被当做本地时区处理,因此不做时区转换,只补上了时区指示符。

滥用 utc 和 local 方法可能得到不符合预期的结果,我们用 moment 的 utc 和 local 做一下测试,特地选择了 0时区 23 点多的时间,换算成北京时间是第二天的早上 7点多,大家可以试试是否和预期一致

const timeStr = '2022-04-16T23:05:26.000Z'
const normalTime = moment(timeStr);
const utcTime = moment.utc(timeStr);

console.log(`情形一: ${normalTime.format('YYYY-MM-DD hh:mm')}`);
console.log(`情形二: ${utcTime.format('YYYY-MM-DD hh:mm')}`);
console.log(`情形三: ${normalTime.local().format('YYYY-MM-DD hh:mm')}`);
console.log(`情形四: ${utcTime.local().format('YYYY-MM-DD hh:mm')}`);

image.png
对国内服务来说,展示为北京时间才符合预期,2022-04-16T23:05:26.000Z 应该显示为 2022-04-17 07:05。通过上面的测试可以看出,timeStr 是 ISO 标准时间,moment 的 format 根据本地化配置(配置部分省略,moment 官方文档可查阅配置方式)自动处理为当地时区的时间,所以情形一与情形三得到相同的结果;情形二得到了 2022-04-16 11:05,因为被指定为 utc 0时区后再 format,如果对其 local 后再 format 同样能获得预期的日期时间(即情形四)。

timeStr = '2022-04-16T23:05:26.000Z' 本身就是一个标准的时间表示(ISO 标准格式),如果是 2022-04-16这样一个日期字符串应该怎样处理,就得根据业务数据定义了。当然,应该尽量避免处理这种非标时间格式。

使用日期时间库

常见处理日期时间的库有 moment、dayjs、date-fns,都能满足日常的需求,简单聊一下他们各自的特点。

moment(停止更新了)

Moment.js is a legacy project, now in maintenance mode. In most cases, you should choose a different library

moment 是一个遗留项目,处于维护模式,体积也比较大,推荐使用其它的库。

dayjs
dayjs 和 moment 的 API 类似,如果有 moment 使用经验,切换的学习成本较低,且更加轻量,推荐使用

date-fns
API 与 moment 风格差异较大,它一个显著的优势就是非常方便 tree-shaking,对于特别注重构建大小的项目可以考虑使用 date-fns。

参考

  1. www.w3.org/
  2. developer.mozilla.org/
  3. github.com/