如果服务端返回时间串比如 2022-04-16T23:05:26.000Z、Sun Apr 17 2022 14:05:26 GMT+0800,需要转换为北京时间显示,应该怎样处理?如果返回 2022/04/28 16:00:00,且已知该字段都使用 utc 零时区标准储存,按北京时间在浏览器显示,又该怎样处理呢?
前言
由于世界各国和地区所处经度不同,每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 |
| 时区指示符 | TZD | Z 或 +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')}`);
对国内服务来说,展示为北京时间才符合预期,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。