前言
最近的实习中,遇到了时区统一和封装管理的难题,途中查阅了很多资料,希望可以借此文章进行复盘。
常见的时间格式
1、Unix 时间戳
在 20 世纪 60 年代末到 70 年代初,贝尔实验室的研究人员开发了 Unix 操作系统。为了方便记录和处理系统中的时间信息,他们需要确定一个统一的起始时间。最终选择了 1970 年 1 月 1 日 00:00:00 UTC 作为起始点,这一选择随后被广泛应用于 Unix 系统及其衍生系统中。
在前端中,常常会使用毫秒时间戳 ,它代表着从 1970 年 1 月 1 日 00:00:00 UTC 到当前时间所经过的毫秒数 。在 Javascript
中,可以通过实例化时间对象 new Date()
来获取当前的毫秒时间戳:
JavaScript
const currentTimestamp = Date.now();
console.log(currentTimestamp); // 输出:1642471500000
由于业务的需要,我们有时候需要用到微秒时间戳 ,即从 1970 年 1 月 1 日 00:00:00 UTC 到当前时间所经过的微秒数 。值得注意的是,JavaScript
的原生 Date
对象、dayjs
和 date-fns
等第三方库都无法直接处理微秒,需要我们手动处理高精度部分(比如时间差计算和时间格式化)。
2、UTC 时间和本地时间
虽然从逻辑和概念上,时间戳易于计算,但是从表达上,时间戳的格式较为抽象、不直观,人类难以直接理解和解读其代表的具体时间。因此,我们还需要采用一种统一的标准时间格式,直接表示为 "年 - 月 - 日 时:分: 秒",例如 "2023-06-15 12:30:00"。
UTC 时间 全称为 "Coordinated Universal Time" , 中文翻译为世界标准时间 、国际协调时间。UTC 时间不依赖于任何特定地区或时区 ,能够确保全球范围内时间的一致性和准确性。例如 2020-10-01T12:00:00Z
,中间的T
是分隔符,用于分隔日期和时间部分,末尾的 Z
表示 UTC。
与 UTC 时间概念相对的,是本地时间(Local Time) ,它表示用户所在时区的实际时间。例如北京时间(UTC+8)的 2020-10-01T20:00:00
,因为全球划分为24个时区(每15°经度1个时区),而北京时间属于东8区,东8区对应的时区偏移量是 UTC+8 时区偏移量,北京本地时间=UTC时间+8小时。
在 Javascript
中,原生 Date
对象本身就支持 UTC 时间 与本地时间的处理,比如:
JavaScript
const localDate = new Date(); // 本地时间
const utcHours = localDate.getUTCHours(); // 获取 UTC 小时
localDate.setUTCHours(12); // 设置 UTC 小时为 12
不仅如此,可以使用第三方库来进行更方便的处理。以 date-fns 库 为例:
JavaScript
import { format } from 'date-fns';
import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz';
// 创建 UTC 时间
const utcDate = new Date('2023-01-01T12:00:00Z');
// 转换为本地时间字符串
const localString = format(utcDate, 'yyyy-MM-dd HH:mm:ss'); // 依赖本地时区
// 明确指定 UTC 格式化
import { formatUTC } from '@date-fns/utc';
const utcString = formatUTC(utcDate, 'yyyy-MM-dd HH:mm:ss'); // 2023-01-01 12:00:00
上面代码中的 'yyyy-MM-dd HH:mm:ss',是 date-fns 库中常用的日期格式化字符串:
yyyy
:代表年份,以四位数字的形式呈现MM
:表示月份,采用两位数字格式dd
:指的是日期,范围从01
到31
,表示一个月中的具体某一天HH
:表示小时,按照 24 小时制,范围从00
到23
;可以替换成hh
,表示 12 小时制mm
:代表分钟,范围从00
到59
,用于精确表示一个小时内的分钟数ss
:表示秒,范围从00
到59
,进一步精确到分钟内的秒数
常见的时间工具链
在前端,常见的时间处理方案有:Javascript
的 原生 Date 对象及其方法,dayjs
和 date-fns
等第三方库。关于这方面,网络上详细的教程很多,这里主要讲解在使用Javascript
的 原生 Date 对象及其方法 时,一些常见的陷阱和错误。
1、getMonth()
的数值陷阱
JavaScript
const date = new Date("2020-03-15");
console.log(date.getMonth()); // 输出 2(代表3月!)
在Javascript
的 原生 Date 对象 中,getMonth()
返回 0~11(0代表1月),与常识不符,极易忘记 +1,因此直接拼接月份会导致显示错误。
2、YYYY-MM-DD
与 YYYY/MM/DD
的隐藏差异
JavaScript
new Date("2024-03-15"); // 按 UTC 解析
new Date("2024/03/15"); // 按本地时区解析
不同浏览器对短横线-
和斜杠/
的解析规则不同:
-
分隔的字符串(如2020-03-15
)可能被当作 UTC 时间/
分隔的字符串(如2020/03/15
)可能被当作 本地时间
因此,当需要 Javascript
的 原生 Date
对象时,应该要统一使用带时区的 ISO 格式,避免一些难以捕捉的bug。
时间封装的工程化实践
由于当前的项目类似于APM监控系统,需要同时处理纳秒、微秒和毫秒 等不同单位的时间戳。但是
Javascript
的原生Date
对象,date-fns
等第三方库大部分只支持毫秒级时间戳,无法很好地满足业务需求,因此需要针对时间的处理进行统一化封装。
这里主要采用的方案是通过 品牌类型(Branded Types) 区分不同精度的时间戳(毫秒、微秒、纳秒),以及 封装高阶时间工具类 从而有效避免类型混淆和误操作。
1、定义品牌类型
TypeScript
// 定义品牌类型的基础符号
declare const TimeStampBrand: unique symbol;
// 毫秒时间戳(number 类型,13位数字)
type MillisecondTimestamp = number & {
[TimeStampBrand]: "millisecond";
};
// 微秒时间戳(BigInt 类型,16位数字)
type MicrosecondTimestamp = bigint & {
[TimeStampBrand]: "microsecond";
};
// 纳秒时间戳(BigInt 类型,19位数字)
type NanosecondTimestamp = bigint & {
[TimeStampBrand]: "nanosecond";
};
在这里,主要通过 TypeScript 的 类型品牌(Type Branding) 为不同精度的时间戳赋予唯一类型标识。
2、时间戳的转换与校验
TypeScript
// ---------- 类型守卫(Type Guard)----------
// 校验是否为合法的毫秒时间戳
const isMillisecondTimestamp = (
value: number
): value is MillisecondTimestamp => {
return value.toString().length <= 13; // 13位数字(最大 9999-12-31T23:59:59.999Z)
};
// 校验是否为合法的微秒时间戳
const isMicrosecondTimestamp = (
value: bigint
): value is MicrosecondTimestamp => {
return value.toString().length <= 16; // 微秒精度范围
};
// ---------- 精度转换函数 ----------
// 微秒转毫秒(可能丢失精度)
const microToMilli = (
micro: MicrosecondTimestamp
): MillisecondTimestamp => {
return Number(micro / BigInt(1000)) as MillisecondTimestamp;
};
// 毫秒转微秒(补充零)
const milliToMicro = (
milli: MillisecondTimestamp
): MicrosecondTimestamp => {
return BigInt(milli) * BigInt(1000) as MicrosecondTimestamp;
};
在这里,主要通过 类型守卫 和 转换函数 来确保类型安全。
3、封装高阶时间工具类
TypeScript
class TimeService {
// 格式化时间戳(自动识别精度)
formatTimestamp(
timestamp:
| MillisecondTimestamp
| MicrosecondTimestamp
| NanosecondTimestamp,
pattern: string
): string {
// 统一转换为毫秒处理
const milli = typeof timestamp === "bigint"
? Number(timestamp / BigInt(1000))
: timestamp;
return format(new Date(milli), pattern, { timeZone: this.timeZone });
}
// 解析字符串为时间戳(明确指定精度)
parseToMillisecond(str: string): MillisecondTimestamp {
const date = this.parseLocal(str, "yyyy-MM-dd HH:mm:ss");
return date.getTime() as MillisecondTimestamp;
}
}
在这里,将品牌类型与时间服务类结合,从而进行强制类型约束。