我正在参加「掘金·启航计划」
有分享才有进步,欢迎大家批评指正
最近在开发一个国际站项目,涉及到的任务流转可能会跨多个国家和地区,这就涉及到时间差的问题了。本文就来梳理一下我在实际开发中涉及到的前端时区的逻辑处理方案。
实际业务场景
在 Asia/shanghai 时区下创建定时流转任务时,产品会有要求:设置为迪拜时间的 2023-06-24 23:00:00
点触发任务流程,这就牵扯到时区转换的问题;同时,在 Asia/shanghai 时区下创建的任务,在世界其他各个时区下看到的应该是不同的时间,但是这里需要格式化为同一个时间(创建地的)格式,并标明 UTC +8
。
我们来举个近一点的例子: 拿东京举例,是
UTC+9
时区,比我们快了一个时区。如果在 Asia/shanghai 时区下选择了设定在东京的2023-06-24 00:00:00
下触发,那么这个时间以东京时间格式化为时间戳后,在上海本地格式化,应该是2023-06-23 23:00:00
基于上面的业务场景,我这里给出了几种处理方案:
方案一、后端调整时区
可以直接把选择的时间字符串 2023-06-24 00:00:00
和需要设置的当地时间的时区 Asia/Tokyo
给到后端,后端来根据时区将时间字符串格式化为相对于东京的时间戳。
以 Go 为例,time 包下就有按照时区格式化的函数:
layout := "2006-01-02 15:04:05"value := "2023-06-24 00:00:00"location := time.LoadLocation("Asia/Tokyo")t := time.ParseInLocation(layout, value, location)fmt.Println("Parsed time:", t.unix())layout := "2006-01-02 15:04:05" value := "2023-06-24 00:00:00" location := time.LoadLocation("Asia/Tokyo") t := time.ParseInLocation(layout, value, location) fmt.Println("Parsed time:", t.unix())layout := "2006-01-02 15:04:05" value := "2023-06-24 00:00:00" location := time.LoadLocation("Asia/Tokyo") t := time.ParseInLocation(layout, value, location) fmt.Println("Parsed time:", t.unix())
这样就能在数据库里存下所选时间的时区和绝对的时间戳了。
这时你会问,那么我前端如何获取到 Asia/Tokyo
这个正确的时区格式给到后端呢?
- 使用 moment-timezone 库
import moment from 'moment-timezone';moment.tz.names(); // ["Africa/Abidjan", "Africa/Accra", "Africa/Addis_Ababa", ...]import moment from 'moment-timezone'; moment.tz.names(); // ["Africa/Abidjan", "Africa/Accra", "Africa/Addis_Ababa", ...]import moment from 'moment-timezone'; moment.tz.names(); // ["Africa/Abidjan", "Africa/Accra", "Africa/Addis_Ababa", ...]
选择完时区后,可以通过 const zone = moment.tz.zone('Asia/Tokyo');
来获取时区对象,对象里便有了 offset 等信息。
实际在项目落地中,moment库比较大,一般使用 dayjs 代替,但是 dayjs 中没有相关的时区 API,所以这个时区列表,我在本地使用 moment 获取到以后做了本地的特殊格式化,并作为静态文件挂载在 CDN 上了,数组结构如下:
[{value: 'Japan Standard Time',abbr: 'JST',offset: 9,isdst: false,text: '(UTC+09:00) Osaka, Sapporo, Tokyo',UTC: ['Asia/Dili', 'Asia/Jayapura', 'Asia/Tokyo', 'Etc/GMT-9', 'Pacific/Palau']},{value: 'China Standard Time',abbr: 'CST',offset: 8,isdst: false,text: '(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi',UTC: ['Asia/Hong_Kong', 'Asia/Macau', 'Asia/Shanghai']},// ...][ { value: 'Japan Standard Time', abbr: 'JST', offset: 9, isdst: false, text: '(UTC+09:00) Osaka, Sapporo, Tokyo', UTC: ['Asia/Dili', 'Asia/Jayapura', 'Asia/Tokyo', 'Etc/GMT-9', 'Pacific/Palau'] }, { value: 'China Standard Time', abbr: 'CST', offset: 8, isdst: false, text: '(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi', UTC: ['Asia/Hong_Kong', 'Asia/Macau', 'Asia/Shanghai'] }, // ... ][ { value: 'Japan Standard Time', abbr: 'JST', offset: 9, isdst: false, text: '(UTC+09:00) Osaka, Sapporo, Tokyo', UTC: ['Asia/Dili', 'Asia/Jayapura', 'Asia/Tokyo', 'Etc/GMT-9', 'Pacific/Palau'] }, { value: 'China Standard Time', abbr: 'CST', offset: 8, isdst: false, text: '(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi', UTC: ['Asia/Hong_Kong', 'Asia/Macau', 'Asia/Shanghai'] }, // ... ]
上面的 offset 是相对于 UTC 标准时区的偏差,该值可以从 moment 的 zone 对象中获取。
Zone 对象里 offsets 常见的有 PST 时区 和 PDT 时区,前者为太平洋标准时间(英文:Pacific Standard Time,縮寫:PST),UTC-8;后者为夏令时间(夏季)称为太平洋夏令时間(英文:Pacific Daylight Time,縮寫:PDT),UTC-7;使用其中一个,根据偏差做加减即可。
- 使用原生的 Intl 对象
如果不需要交互选择时区列表,只是在页面上根据当地时区格式化时间戳,可以使用js原生的 Intl 对象:
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;console.log(timeZone); // Asia/Shanghaiconst timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; console.log(timeZone); // Asia/Shanghaiconst timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; console.log(timeZone); // Asia/Shanghai
而且该方法的兼容性目前已经比较好了:
这样在中国的页面上这样显示:
(Asia/Shanghai UTC +8) 2023-06-23 23:00:00(Asia/Shanghai UTC +8) 2023-06-23 23:00:00(Asia/Shanghai UTC +8) 2023-06-23 23:00:00
在日本的页面这样显示:
(Asia/Tokyo UTC +9) 2023-06-24 00:00:00(Asia/Tokyo UTC +9) 2023-06-24 00:00:00(Asia/Tokyo UTC +9) 2023-06-24 00:00:00
方案二、前端格式化为时间戳
这种方案不依赖后端,前端选择时区,前端转换为绝对值时间戳给到后端。获取时区列表的方式参见方案一。这里我们使用手动时区转换的思路,获取到应该转换为上海时间的时间字符串,即,将 2023-06-24 00:00:00
转换为 2023-06-23 23:00:00
。
在获取到时区 timeZoneOffset
(这里是 9) 和 时间字符串 time
(这里是 2023-06-24 00:00:00)后,前端按照当地时间模拟时区变化来处理:
// 模拟转换为标准 UTC 时间,这里减去9个小时即可const UTC = dayjs(time).add(-timeZoneOffset, 'hour');// 获取浏览器当地时间的小时差,offsetHours 如果在上海,就是 8const date = new Date();const offsetHours = -(date.getTimezoneOffset() / 60);// 将上面的 UTC 加上浏览器本地的时差const result = dayjs(UTC).add(offsetHours, 'hour');// 模拟转换为标准 UTC 时间,这里减去9个小时即可 const UTC = dayjs(time).add(-timeZoneOffset, 'hour'); // 获取浏览器当地时间的小时差,offsetHours 如果在上海,就是 8 const date = new Date(); const offsetHours = -(date.getTimezoneOffset() / 60); // 将上面的 UTC 加上浏览器本地的时差 const result = dayjs(UTC).add(offsetHours, 'hour');// 模拟转换为标准 UTC 时间,这里减去9个小时即可 const UTC = dayjs(time).add(-timeZoneOffset, 'hour'); // 获取浏览器当地时间的小时差,offsetHours 如果在上海,就是 8 const date = new Date(); const offsetHours = -(date.getTimezoneOffset() / 60); // 将上面的 UTC 加上浏览器本地的时差 const result = dayjs(UTC).add(offsetHours, 'hour');
通过上述操作,就将东京时间的 2023-06-24 00:00:00
转换为上海时区的 2023-06-23 23:00:00
,此时在将这个 result 转换为时间戳即可:
result.unix() // 1687532400result.unix() // 1687532400result.unix() // 1687532400
方案三、dayjs格式化为时间戳
该方案为方案二的变种,只是不用原生操作时区差来计算,而是使用 dayjs 内置的方法替代之:
const d = dayjs.tz('2023-06-24 00:00:00', 'Asia/Tokyo')console.log(d.format()) // 2023-06-24T00:00:00+09:00console.log(d.unix()) // 1687532400const d = dayjs.tz('2023-06-24 00:00:00', 'Asia/Tokyo') console.log(d.format()) // 2023-06-24T00:00:00+09:00 console.log(d.unix()) // 1687532400const d = dayjs.tz('2023-06-24 00:00:00', 'Asia/Tokyo') console.log(d.format()) // 2023-06-24T00:00:00+09:00 console.log(d.unix()) // 1687532400
该方法的前提是引入时区插件:
import timezone from 'dayjs/plugin/timezone';dayjs.extend(timezone);import timezone from 'dayjs/plugin/timezone'; dayjs.extend(timezone);import timezone from 'dayjs/plugin/timezone'; dayjs.extend(timezone);
总结
个人使用的是方案三和方案一的结合,获取到时区列表并缓存CDN后,前端选择时区并转换为绝对值时间戳,后端进行二次校验并在列表显示的地方格式化为字符串返回。
需要注意的是,网上获取到的时区偏移可能会有延迟(有些城市会不停调整时区),比如柏林的时区,我在列表里的是 +1,但是网上搜到的最新消息是 GMT+2 的,但是今年10月份又会调回 +1。我这里就还以moment获取的数据为准了。