前端国际化跨时区问题兼容适配本地时间解决方案

3,644 阅读5分钟

前言

这里有一些时间,你能判断它们的具体时间么?

  1. 2022-04-15T00:00:00.000+0800
  2. 2022-04-15T07:00:00.000+0700
  3. 2022-04-16 15:50:56
  4. 2022-04-16
  5. 1650038400000

它们其实分别对应东八区时间、东七区时间、无时区时间、日期、时间戳

如果读者有一定的项目开发经验,就一定会成为数据库里存储的时间都应该是时间戳这一观点的拥趸

那么回归正题,我们要把这些傻了吧唧的时间全都适配成用户认知中的时间

1 认识时间

首先我们应该知道,对于与请求无关的时间,一般情况下由本地生成,大部分情况无需修改。那么,我们做时区适配的时候自然是着手于请求。

这里先说说一些时间的概念

用户认知时间

什么是用户认知中的时间?我可以说99%以上的人在说起时间的时候都会下意识地使用当地时间;

也就是说,我一个中国人,说今天中午十二点,那肯定指的是

"2022-04-16T12:00:00.000+0800"

API请求时间

API请求时间指的其实是AJAX请求中payload、searchParam的时间,一般来讲,用于查询的时间格式是时间戳或者Date

不过我们不需要知道这一块出来的时间是怎么样的,只需要知道我们的基本目标是:

  • 所有API请求时间,在同一时间点切换各个时区的时候应该表现成同样的值

API返回时间

接口中返回的时间取决于数据的存储方式以及服务器时间,也就是说其中一方面可能混杂着前言【1、2、3、4、5】甚至更多奇葩离谱的时间。但是没关系,我们假定服务器时间是东八区,然后再对各个时间做额外的处理。

而同时结合上边的用户认知时间我们可以得出:

  • 所有API返回时间都应该被格式化成正确的本地时间

那么我们可以得出结论:

  • 对于所有API请求时间,在同一时间点切换各个时区的时候应该表现成同样的值
  • 对于所有API返回时间,它们都应该被格式化成正确的本地时间

2 方案实现

项目使用的是axios,做请求拦截和返回拦截是比较轻松的:

image.png

image.png

我们在请求拦截器和返回拦截器中注册好实现用的方法。

2.1 拦截器实现

我们放出源代码,这是请求拦截器,响应拦截器用的是一个原理:

const getBaseUrl = (url) => {
    const _url = ~url.indexOf("://") ? new URL(url) : new URL(`http://127.0.0.1:8080/${url}`);
    const baseURL = _url.pathname;
    const nnnRule = /\/[1-9]\d*\//;
    const testListRule = /web\/test\/get.*List\//;
    return baseURL
        .replace(nnnRule, "/{nnn}/")
        .replace(testListRule, "web/test/get{O.o}List/");
};

const timeZoneRequestInterceptor = (config) => {
    try {
        const {url, data} = config;
        const baseURL = getBaseUrl(url);
        const {"request": solution = null} = urlMap[baseURL] || {};
        if (!solution) {
            return;
        }
        const {formatTransformer = "", postDataTransformers = "", searchTransformers = ""} = solution;
        if (postDataTransformers) {
            const postTrans = [...postDataTransformers];
            while (postTrans.length !== 0) {
                const next = postTrans.shift();
                config.data = formatTransformer ?
                    formatTransformer(data, next) :
                    next(data);
            }
        }
        if (searchTransformers) {
            const searchTrans = [...searchTransformers];
            while (searchTrans.length !== 0) {
                const next = searchTrans.shift();
                config.url = urlTransformer(url, next);
            }
        }
    } catch (error) {
        console.error("timeZoneRequestInterceptor Error", error);
        return;
    }
};

2.1.1 getBaseUrl

那么我们可以看到首先有一个getBaseUrl,这个函数的作用是提取url成通用pathname

其中replace的使用是为了保证/web2/164/test/web2/165/test都可以被转换为web2/{nnn}/test方便映射。按照这个思路,对于更多类似的url都可以用相似的方法转换。

2.1.2 formatTransformer与postDataTransformers的协调

responseDataTransformers同理

image.png

新建一个队列防止弱引用陷阱,然后不断将config.data进行转换

2.1.3 searchTransformers

image.png

由于searchTransformers实际转换的是url,我们这里实质上是对url的重新拼装,使用固定的urlTransformer,urlTransformer会把searchParams序列化后传递给我们的函数,然后函数执行完毕进行反序列化回归url

image.png

2.2 注册与使用时区转换器

上文其实可以看到变量urlMap,该变量即为注册的地方。

image.png

我们可以用一个简单的结构即完成请求在时区转换器中的注册
在实际使用中,对于各个请求之间的共同点,可以编写通用的函数进行转换,使用的时候只需要添加到函数队列中即可。

image.png

image.png

image.png

2.3 封装时间转换函数

const CURRENT_TIME_ZONE_NUM = -8;
const LOCAL_TIME_ZONE_NUM = new Date().getTimezoneOffset() / 60;
const TRANS_NUM = LOCAL_TIME_ZONE_NUM - CURRENT_TIME_ZONE_NUM;
const addTimeZone = 3;

Date.prototype.toLocalTimeByZoneNum = function(zoneNum) {
    if (isNaN(zoneNum)) {
        zoneNum = 0;
    }
    const timeStamp = this.getTime() + zoneNum * 60 * 60 * 1000;
    return new Date(timeStamp);
};
Date.prototype.toRequest = function() {
    return this.toLocalTimeByZoneNum(TRANS_NUM);
};
Date.prototype.toResponse = function() {
    return this.toLocalTimeByZoneNum(-TRANS_NUM);
};

image.png

我实现时间转换的方式是对于不同时区做区分,并且设定服务器时间为东八,在Date原型对象挂载方便转换的函数。

然后就可以做各种类型时间的处理,现在对于遇到的三个场景的时间转换做个示例:

2.3.1 new Date()生成的当前时间

上文说过,对于请求,时间转换的唯一目标就是任何情况下在不同时区发出的数据都应该是相同的,对于一个原本在东八区的应用,后台的处理也必定是基于东八,所以这里对于YYYY-MM-DDTHH:mm:ss.SSSZZ格式的请求时间做了转东八处理。

const dateChange = (timeStr) => moment(new Date(timeStr).toRequest()).format("YYYY-MM-DDTHH:mm:ss.SSS+0800");

image.png

2.3.2 对于当天0点、23:59:59这种时间

当天0点、当天24点,本质问题不是时间,而是时间点。开发者其实是想查询某天的定点范围(例如「昨天」),而不是查0~24点。
这种情况下,给出来的东七时间一般是

"2022-04-05T00:00:00.000+0700"
"2022-04-05T23:59:59.000+0700"

而理想时间应该是

"2022-04-05T00:00:00.000+0800"
"2022-04-05T23:59:59.000+0800"

所以我们只需要修改时区即可:

const timeZoneChange = (timeStr) => {
    if (LOCAL_TIME_ZONE_NUM < addTimeZone) {
        // 注意跨过一天的变化
        return moment(timeStr).format("YYYY-MM-DDTHH:mm:ss.SSS+0800");
    }
    return moment(timeStr).add(1, "days").format("YYYY-MM-DDTHH:mm:ss.SSS+0800");
};

image.png

image.png

测试的时候记得修改时区哦

image.png

3 方案总结

image.png

之所以做成函数队列以及专门制作的format转换器,一方面是考虑到易用性以及transform函数的编写难度,一方面是发现大部分时间在payload或者searchParams中都是存在共性的,通过队列的形式可以增强transformer函数的复用性。

而相比转换器的实现,可能更麻烦的其实是时间的转换,以及时间转换目标的思考。要怎么做,怎么整,怎么验收?

最终的思考是,我们的目标是让后台仍认为我们在东八区,这样后台无需调整,同时让用户在自己所在的时区内。
当确定了这一点,我才最终完成了时区适配,接口适配工作实际上在得出目标之后是直接做了重构。

image.png

image.png

结尾

当人的情绪和心意高度协调,就会有希望的事情发生

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:cloud.tencent.com/developer/s…