在 Web 开发中处理时区问题
说实话,处理日期和时间是人类最棘手的问题之一,编程也不例外。如果您的应用需要处理来自世界各地用户的事件,您需要添加时区,甚至可能需要重复执行以保存可能多次发生的事件。本文将介绍一些处理此类应用的方法:
- 如何在数据库中存储日期。
- 如何处理复发。
- 在哪里将时间转换为用户的当地时间。
- 可以帮助完成这些任务的图书馆。
让我们开始吧。
如何在数据库中存储日期
当用户位于不同位置时,在数据库中保存日期的最常见方法是使用UTC(协调世界时)保存时间,这是时钟和时间调节的主要时间标准,但这并不总是最好的解决方案,您必须检查您的具体用例;例如:
- 用户从哪里保存日期?
- 所有用户都需要保存日期还是只需要一个管理员?
- 事件发生在哪里?
例如,最近我必须为我国的一个教堂制作一个电视节目表插件,考虑到活动只在一个地方发生,以 UTC 格式保存日期时间是一种过度设计,因为这不是真正需要的,所以我将它保存在教堂当地时区。
但另一方面,在我的工作中,我遇到过用户可以保存和编辑世界各地的事件的情况,在这种情况下,以 UTC 保存更方便
如何管理日期重复
当我第一次遇到 Web 开发问题时,我总是寻找我使用的应用程序,因为它们为我提供了用户体验、界面,有时还提供 API(如果应用程序准备好与第三方应用程序集成)。所以我立即打开浏览器并查找 Google 日历。
他们有一个非常直观的接口来保存递归,并且在 API 文档中提到了RRule。RRule是处理递归的标准,在大多数编程语言中都有多种实现,在 JavaScript 中,rrule.js就是答案。
以下是每周举办一次活动的示例代码,直到 2021 年 9 月 30 日
// To create the rrule
const rule = new RRule({
freq: RRule.WEEKLY,
dtstart: new Date(Date.UTC(2021, 8, 18, 8, 17, 0)),
until: new Date(Date.UTC(2021, 8, 30, 8, 17, 0)),
count: 30,
interval: 1
});
// to get the RRule in string
rule.toString();
// DTSTART:20210918T081700Z
// RRULE:FREQ=WEEKLY;UNTIL=20210930T081700Z;COUNT=30;INTERVAL=1;WKST=MO
// to get the ocurrence
rule.all();
您可以将 RRule 字符串保存在数据库的字段中。但我认为最好将 RRule 的每个属性保存为单独的字段(frequency
,interval
等),以便更好地从数据库中查询事件。
在哪里将时间转换为用户的当地时间?
时间转换是一个可视化的方面,即使你正在向移动和 Web 应用提供 API,最好也不要让后端代码参与这些转换,而让前端来处理。你可以使用Intl
API 直接从 Web 浏览器检测用户的本地时区。
Intl.DateTimeFormat().resolvedOptions().timeZone
它具有非常可接受的浏览器支持,您可以在MDN中阅读有关它的更多信息。
另一个选项是要求用户指定他们的时区,并预先选择当前时区。
一旦我们获得用户时区,就可以将其从 UTC 或您保存在数据库中的时区转换为用户时区,我们在 javascript 中有一些不错的选择:luxon、date-fns,还建议将这些库中的功能包装在一个中心位置,以防您出于某种原因需要更改,如果您在另一个应用程序中遇到类似情况,测试和移动都会更容易。
为了说明这一点,这里是我为管理时区转换而做的包装器的一个例子,以便让您了解:
import { DateTime } from "luxon";
export const ISO_TIME_FORMAT = "HH:mm";
export function useTime(zone, serverTimezone = "UTC") {
const timeZone = zone;
...
/**
* Transform a JS Date in users' timezone to ISO date in UTC
* @param {Date} date
* @returns {Object}
*/
const getIsoUtcDateTime = (date) => { ... };
/**
* Transform DB date and time in ISO to a JS Date in users' timezone
* @param {String} isoDate
* @param {String} isoTime
* @returns {Object}
*/
const getLocalDateTimeFromISO = (isoDate, isoTime) => { ... };
return {
...
getIsoUtcDateTime,
getLocalDateTimeFromISO
}
我将省略实现细节,因为我只想向您展示包装器带来的一些好处。这里useTime
定义了一个主函数,它只接受一次用户和数据库时区参数,它返回的函数将使用这些时区进行转换。
要使用我们的包装器,假设日期和时间保存为 ISO 字符串"yyyy-MM-dd"
和"HH:mm"
格式,我们可以按如下方式进行:
import { useTime } from "./useTime";
import constants from "./constants";
const { getLocalDateTimeFromISO } = useTime(user.timezone, constants.SERVER_TIMEZONE);
// ... code to fetch events would go here
// transform iso dates to users' timezone
const eventsInLocal = events.map((event) => {
const { date, time } = getLocalDateTimeFromISO(event.date, event.time);
event.date = date;
event.time = time;
return event;
}
测试
为了测试开发中的行为,如果我们从浏览器获取时区,您可以通过单击检查器中顶部栏末尾的三个点 > 更多工具 > 传感器,在检查器中模拟浏览器中的不同时区。
这将在浏览器检查器底部打开一个部分,其中包含一个用于覆盖当前位置和时区的选项:
现在,我们的浏览器时区已设置完毕Asia/Tokio
,其new Date()
行为将与东京时一样(Arigato)
结论
每当我们克服一个又一个艰难的挑战,我们的技能就会得到提升。如果我能用一个数字来衡量处理日期的点数对你的技能的影响,我想象不出一个数字,但那会是一个很高的数字😂。值得庆幸的是,我们有一些人为我们铺平了道路,为我们制定了像UTC和RRule这样的标准。
感谢您的阅读,我希望这篇文章可以为您节省一些时间,如果您有任何疑问,欢迎评论,或者如果您喜欢Twitter以及我的Github,我会在那里做一些实验和项目。
祝你有美好的一天。