前端时间管理实践:从时间标准化到工程化封装

前言

最近的实习中,遇到了时区统一和封装管理的难题,途中查阅了很多资料,希望可以借此文章进行复盘。

常见的时间格式

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 对象、dayjsdate-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 :指的是日期,范围从 0131,表示一个月中的具体某一天
  • HH :表示小时,按照 24 小时制,范围从 0023;可以替换成 hh,表示 12 小时制
  • mm :代表分钟,范围从 0059,用于精确表示一个小时内的分钟数
  • ss :表示秒,范围从 0059,进一步精确到分钟内的秒数

常见的时间工具链

在前端,常见的时间处理方案有:Javascript的 原生 Date 对象及其方法,dayjsdate-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-DDYYYY/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;
  }
}

在这里,将品牌类型与时间服务类结合,从而进行强制类型约束。

相关推荐
袈裟和尚2 分钟前
如何在安卓平板上下载安装Google Chrome【轻松安装】
前端·chrome·电脑
曹牧6 分钟前
HTML字符实体和转义字符串
前端·html
小希爸爸11 分钟前
2、中医基础入门和养生
前端·后端
局外人LZ15 分钟前
前端项目搭建集锦:vite、vue、react、antd、vant、ts、sass、eslint、prettier、浏览器扩展,开箱即用,附带项目搭建教程
前端·vue.js·react.js
G_GreenHand29 分钟前
Dhtmlx Gantt教程
前端
鹿九巫30 分钟前
【CSS】层叠,优先级与继承(四):层叠,优先级与继承的关系
前端·css
卓怡学长33 分钟前
w304基于HTML5的民谣网站的设计与实现
java·前端·数据库·spring boot·spring·html5
宝拉不想努力了35 分钟前
vue element使用el-table时,切换tab,table表格列项发生错位问题
前端·vue.js·elementui
YONG823_API40 分钟前
深度探究获取淘宝商品数据的途径|API接口|批量自动化采集商品数据
java·前端·自动化
鱼樱前端40 分钟前
前端必知必会:JavaScript 对象与数组克隆的 7 种姿势,从浅入深一网打尽!
前端·javascript