日期格式化也有兼容性问题?来捋一捋日期处理库的差异和操作技巧
引言
我们的项目中,有一个日期格式化的功能,用户选择日期之后,比如选择了2025-07-01,前端会处理成零点的时间(2025-07-01 00:00:00
)进行回显,同时提交的数据也是(2025-07-01 00:00:00
)的时间戳。
但是,最近线上出现了一个非常典型的数据:用户提交的数据竟然是(2025-07-01 08:00:00
)的时间戳。
整个团队尝试了各种浏览器都没有复现这个问题,客户那边没有提供更多的有效信息。紧急修复数据之后,我开始对日期格式化功能进行复盘。
花开两朵,先表一枝。
一、第一回合:复盘代码
1.1 功能描述
用户在创建促销活动的时候,可以选择活动上线和下线日期,如选择了2025-07-01 ~ 2025-07-31,那么会默认展示为2025-07-01 00:00:00 ~ 2025-07-31 23:59:59,旨在告知用户,活动是0点开始,23:59:59结束。
1.2 代码分析
我们日期格式化功能的代码如下:
/**
* 设置特定格式的时间戳(将日期时间调整为当天的开始或结束时刻)
*
* @param {string} dateString - 输入的日期字符串,将被解析为moment对象
* @param {string} [type='begin'] - 时间类型,可选值为'begin'(当天开始时间)或'end'(当天结束时间)
* @returns {number} - 返回调整后的时间戳(毫秒数),如果输入无效则返回空字符串
*/
const setFormatSpecificTimeStamp = (dateString, type = 'begin') => {
// 处理空输入情况
if (!dateString) {
return '';
}
// 验证日期有效性
const date = moment(dateString);
if (!date.isValid()) {
return '';
}
// 定义时间格式配置(小时、分钟、秒、毫秒)
const formatter = {
begin: [0, 0, 0, 0], // 当天开始时间(00:00:00.000)
end: [23, 59, 59, 0], // 当天结束时间(23:59:59.000)
};
// 获取对应的时间格式配置,默认使用begin配置
const format = formatter[type] || formatter.begin;
// 设置时间分量
date.set('hour', format[0]);
date.set('minute', format[1]);
date.set('second', format[2]);
date.set('millisecond', format[3]);
// 返回时间戳
return date.valueOf();
};
基本功能:
将输入的日期字符串调整为当天的开始时刻(00:00:00)或结束时刻(23:59:59),并返回对应的时间戳(毫秒数);若输入无效则返回空字符串。
核心逻辑:
- 输入检查:空输入直接返回空字符串。
- 日期验证:使用moment解析日期,无效则返回空字符串。
- 时间配置:
- begin类型设为
[0,0,0,0]
(00:00:00.000)。 - end类型设为
[23,59,59,0]
(23:59:59.000)。
- 时间调整:根据类型设置时分秒毫秒。
- 返回值:返回调整后的时间戳(毫秒数)。
1.3 问题分析
结合日期的特点,我推测可能导致线上问题的原因如下:
- Moment.js 默认使用本地时区解析。
- 直接修改时间部分不会重置时区偏移。
- 夏令时转换导致额外偏差。
- Moment.js 在不同环境下的兼容性问题。
二、第二回合:Moment.js 的兼容性问题
2.1 典型兼容性问题清单
问题类型 |
表现场景 |
影响范围 |
时区解析不一致 |
夏令时转换期间 |
欧美地区用户 |
体积过大 |
生产包体积分析 |
低端安卓设备 |
可变性缺陷 |
日期对象被意外修改 |
状态管理场景 |
性能瓶颈 |
大规模日期操作 |
数据可视化大屏 |
2.2 问题示例
时区问题:
// 美国东部时间夏令时问题
moment('2025-07-01 00:00:00').format();
// 输出: "2025-07-01T00:00:00-04:00" (自动跳转)
三、第三回合:Day.js 替代方案
3.1 Day.js 基本介绍
Day.js 是一个轻量级的日期处理库,它的核心文件只有 2KB 左右,而且提供了丰富的插件来扩展功能。Day.js 的 API 设计与 Moment.js 类似,这使得开发者可以很容易地从 Moment.js 迁移到 Day.js。
3.2 问题修复方案
最终方案:
/**
* 设置特定格式的时间戳
*
* 该函数根据指定的类型(开始/结束)和时区设置,对输入日期进行格式化处理,
* 返回调整后的时间戳(毫秒数)。无效输入将返回NaN。
*
* @param {*} dateInput - 输入的日期值,可以是能被dayjs解析的任何格式
* @param {'begin'|'end'} [type='begin'] - 时间调整类型:
* 'begin':调整为当天的开始时间(00:00:00.000)
* 'end':调整为当天的结束时间(23:59:59.999)
* @param {'local'|'utc'} [timezone='local'] - 时区设置:
* 'local':使用本地时区
* 'utc':使用UTC时区
* @returns {number} 调整后的时间戳(毫秒数),无效输入返回空
*/
const setFormatSpecificTimeStamp = (
dateInput,
type = 'begin',
timezone = 'local',
) => {
// 处理空输入情况
if (!dateInput) return '';
// 日期有效性验证
let date = dayjs(dateInput);
if (!date.isValid()) {
console.error(`Invalid date input: ${dateInput}`);
return '';
}
// 应用时区转换
if (timezone === 'utc') {
date = date.utc();
}
// 定义时间调整策略
const timeAdjusters = {
begin: d => d.startOf('day'),
end: d => d.endOf('day'),
};
// 选择并应用时间调整器
const adjuster = timeAdjusters[type] || timeAdjusters.begin;
// 执行调整并返回时间戳(毫秒部分归零)
return adjuster(date).millisecond(0).valueOf();
};
功能总结:
- 日期标准化:将任意格式的日期统一处理为精确到秒的时间戳。
- 全天范围支持:
type='begin'
→ 返回当天起始时间戳(00:00:00)。type='end'
→ 返回当天结束时间戳(23:59:59)。
- 时区适配:
timezone='local'
→ 基于系统时区。timezone='utc'
→ 基于标准UTC时间。
3.3 小插曲
这里有个小插曲,最初我改完方案,发现提交数据仍然有问题。
输入:
setFormatSpecificTimeStamp('2025-07-31 12:00:00', 'end')
输出:1753977599999。
最后3位数是999,实际应该是000。
这是因为毫秒没有归零,于是增加了millisecond(0)方法。
最后正确的输出:1753977599000。
四、第四回合:日期操作技巧总结
现在第三方库十分成熟,日常对日期操作频率不高,为了节省问题解决的成本,提升解决问题的效率,所以我总结了若干日期操作技巧。
4.1 跨月日期安全操作
// 避免2025-01-31加1月变成2025-03-03
dayjs('2025-01-31').add(1, 'month').endOf('month').format('YYYY-MM-DD')
// → 2025-02-28
4.2 工作日计算
// 导入dayjs及其插件
const dayjs = require('dayjs');
const isSameOrBefore = require('dayjs/plugin/isSameOrBefore');
const weekOfYear = require('dayjs/plugin/weekOfYear');
// 扩展dayjs功能
dayjs.extend(isSameOrBefore);
dayjs.extend(weekOfYear);
/**
* 计算两个日期之间的工作日数量(排除周六和周日)
* @param {string} start - 起始日期字符串(YYYY-MM-DD格式)
* @param {string} end - 结束日期字符串(YYYY-MM-DD格式)
* @returns {number} 两个日期之间的工作日总数
*/
const getWorkdays = (start, end) => {
let count = 0;
let cur = dayjs(start);
// 遍历日期范围内的每一天
while (cur.isSameOrBefore(end)) {
// 检查当前日是否为工作日(非周六/周日)
if (cur.day() !== 0 && cur.day() !== 6) count++;
cur = cur.add(1, 'day');
}
return count;
};
示例:
// 示例:计算2025年7月的工作日数量
const days = getWorkdays('2025-07-01', '2025-07-31');
console.log(days); // 23
4.3 季度处理
// 导入dayjs及其插件
const dayjs = require('dayjs');
const quarterOfYear = require('dayjs/plugin/quarterOfYear');
dayjs.extend(quarterOfYear);
// 获取季度末第一天
dayjs().startOf('quarter').format('YYYY-MM-DD'); // → 2025-07-01
// 获取季度末最后一天
dayjs().endOf('quarter').format('YYYY-MM-DD'); // → 2025-09-30
4.4 设置为当月第一天
const dayjs = require('dayjs');
const date = '2025-07-10';
// 获取输入日期的当月第一天
dayjs(date).startOf('month').format('YYYY-MM-DD');// → 2025-07-01
功能说明:
- 使用dayjs解析目标日期。
- 通过startOf('month')获取该月起始时间。
- 格式化为YYYY-MM-DD字符串。
4.5 设置为当周第一天(周一)
// 导入dayjs及其插件
const dayjs = require('dayjs');
const weekOfYear = require('dayjs/plugin/weekOfYear');
dayjs.extend(weekOfYear);
const date = '2025-07-10';
// 计算给定日期所在周的第一天
dayjs(date).startOf('week').format('YYYY-MM-DD'); // → 2025-07-06
功能说明:
- dayjs(date) - 将日期字符串转换为dayjs对象。
- .startOf('week') - 获取该周的第一天(根据本地化设置,默认周日为一周起点)。
- .format('YYYY-MM-DD') - 格式化为年-月-日字符串。
结语
本文从一个线上日期格式化问题出发,深入探讨了 Moment.js 和 Day.js 这两个日期处理库的差异点、兼容性问题以及特别操作。Moment.js 虽然功能强大,但存在体积过大、时区处理复杂、对象可变等问题。而 Day.js 作为替代方案,体积小、性能优,API 设计与 Moment.js 类似,且对象不可变,避免了一些潜在的问题。
通过本文的学习,我们对日期处理库有了更深入的了解,掌握了如何选择合适的日期处理库来解决实际问题。分享日期处理的三个原则:
- 明确时区:始终显式指定时区上下文
- 不可变性:避免隐式的日期对象修改
- 语义化封装:业务逻辑与日期解耦
这些经验将帮助您构建更健壮的时间相关功能,避免因日期问题导致的线上事故,为业务创造稳定可靠的技术基础。
- 点赞
- 收藏
- 关注作者
评论(0)