前言
最近工作内容需要封装时间相关的utils,之前也没有具体研究过这块,所以查看了一些资料补充盲区。
本文将简要介绍前端常用日期处理库:官方停止维护的moment.js,无缝代替moment.js的day.js,逐渐流行的date-fns,最后基于date-fns封装常用日期处理的utils。
2022年了,如果项目中有用moment.js的可以用day.js代替减少体积做优化,新项目可以直接选择date-fns。
本文所有的utils均在github实现
大纲
- 常用日期处理库
- 官方停止维护的moment.js
- 无缝代替moment.js的day.js
- 逐渐流行的date-fns
- 基于date-fns封装utils
一、常用日期处理库
1.1 官方停止维护的moment.js
github:github.com/moment/mome…
moment.js是一个大而全的时间日期库,极大方便了我们在 JavaScript 中计算时间和日期,每周下载量超过 1200 万,已成功用于数百万个项目中。
但是,作为一个诞生于2011年的元老级明星项目,以现在的眼光来看moment.js并非完美无缺,官方总结了两大问题:
(1) 可变对象
moment对象是可变对象(mutable)
,简单点说,任何时间上的加减等计算都改变了其本身。这种设计让代码变得十分不可控,而且很容易带来各种隐蔽且难以调试的 bug。以至于我们在每步修改之前,都要先调用 .clone
克隆一次才能放心操作。
(2)包体积过大
因为Momnet.js将全部的功能和所有支持的语言都打到一个包里
,包的大小也是到了 280.9 kB 这样一个夸张的数字,而且对于Tree shaking无效
。如果要使用时区相关的功能,包体积更是有 467.6 kB 的大小。简单点说,我们可能只需要一个 .format
格式化时间的方法,用户就需要加载数百 kB 的库,这是十分不划算的。
在2020年9月,moment.js官方宣布停止开发,进入维护状态(如下图),后续不会再为其增加新功能,并建议新项目不要使用moment.js,推荐使用更现代的库或JavaScript目前的实验性提案Temporal
。moment团队提供的替代方案包括:Luxon
、Day.js
、date-fns
和js-Joda
。他们还说,希望未来有一天能够完全不需要JavaScript的日期和时间库,而是使用语言本身的功能。所以他们还推荐了尚处于实验性阶段的Temporal
。
所以2022年了,当我们在技术选型的时候,可以果断抛弃moment.js
1.2 无缝代替moment.js的day.js
github:github.com/iamkun/dayj…
day.js基本用法如下:
dayjs().startOf('month').add(1, 'day').set('year', 2018).format('YYYY-MM-DD HH:mm:ss');
(1)和moment.js相同的API
和moment.js相同的API、相同的链式操作。
(2)不可变
上面提到,可变性是使用momentJs时最大的问题之一。在day.js完全消除了这个问题,它支持不变性。
(3)体积小
Day.js 虽然仅有 2kb 大小,但是功能一点都没有阉割,包含了时间处理的全部常用方法。
更多可查看day.js作者自己写的推广文章:juejin.cn/post/684490…
1.3 逐渐流行的date-fns
github:github.com/date-fns/da…
(1)模块化、按需引入
date-fns库包含多个函数,有200多种功能,适用于几乎所有场合。并且是模块化的,可以根据需要单独导入这些函数。适用于webpack
、Browserify
或 Rollup
,还支持 tree-shaking
。
例如,如果要计算2个日期之间的差值,只需要导入formatDistance
和subDays
函数。
import { formatDistance, subDays } from ‘date-fns’ formatDistance(subDays(new Date(), 3), new Date())
(2)不可变
上面提到,可变性是使用 momentJs 时最大的问题之一。在date-fns完全消除了这个问题,它支持不变性。
const startDate = new Date(); const endDate = add(startDate, {years: 2}); console.log(startDate) // 2022-03-13T13:39:07+01:00 console.log(endDate) // 2024-03-13T13:39:07+01:00
(3) 同时支持 Flow 和 TypeScript
二、基于date-fns封装utils
为什么要封装?一般library都是提供了基础实现
1、日期这种需要跟业务的UI规范结合起来
比如我们默认的日期格式是yyyy-MM-dd HH:mm
,而不是yyyy/MM/dd HH:mm:ss
等
比如日期间隔的默认格式是为yyyy/MM/dd - yyyy/MM/dd
,而不是yyyy-MM-dd ~ yyyy-MM-dd
等,提供一个日期间隔的utils,而不是每次自己拼装
...
在封装的时候写好默认格式(其实真正考虑多语言场景是不能写死格式的,因为不同国家日期的表达其实是不一样的,后面解决了这个问题,再写文章补充吧),在使用的时候就可以避免不同的人写出来的格式不一样
2、日期涉及到多语言
比如时长:22小时17分钟1秒
,22h17m1s
,22시간17분1초
比如多久之前:5 天前
、 5 days ago
...
date-fns提供的多语言不一定能百分百契合,在封装的时候处理好
2.1 getTimestamp
获取当前时间或者某个时间的秒级时间戳
import { getUnixTime } from 'date-fns'; /** * @author zxyue25 * @desc 获取当前时间或者某个时间的秒级时间戳; * 如果入参是毫秒秒级时间戳(13位),则去除最后三位1000返回毫秒级(13位)时间戳;主要场景在前端需要入参拿秒级时间戳给后端作为入参 * @param date - Date | Number * @returns 返回格式化后的秒级时间戳 - Number * @example * ``` * getTimestamp() // 1658320260 * getTimestamp(new Date()) // 1658320260 * getTimestamp(new Date().getTime()) // 1658320260 * getTimestamp(Date.parse(new Date())) // 1658320260 * getTimestamp(1658312707) // 1658312 * getTimestamp(1) // 0 * ``` */ export const getTimestamp = (date: number | Date = new Date()): number => getUnixTime(date);
2.2 getMilliTimestamp
获取当前时间或者某个时间的毫秒级时间戳
import { getTime } from 'date-fns'; /** * @author zxyue25 * @desc 获取当前时间或者某个时间的毫秒级时间戳; * 如果入参是秒级时间戳(10位),则乘以1000返回毫秒级(13位)时间戳;主要场景在server返回了秒级时间戳,前端先乘以1000转换成日期展示 * TODO:这里传入了其他数字怎么处理?直接返回还是补齐13位返回?date-fns是直接返回,建议直接返回 * @param date - Date | Number * @returns 返回格式化后的毫秒级时间戳 - Number * @example * ``` * getMilliTimestamp() // 1658320372160 * getMilliTimestamp(new Date()) // 1658320372160 * getMilliTimestamp(new Date().getTime()) // 1658320372160 * getMilliTimestamp(Date.parse(new Date())) // 1658320372000 * getMilliTimestamp(1658312707) // 1658312707000 * getMilliTimestamp(1) // 1 * ``` */ export const getMilliTimestamp = (date?: number | Date): number => { if (!date) { return getTime(new Date()); } else { if (date instanceof Date) { return getTime(date); } else { if (date.toString().length === 10) { return getTime(date * 1000); } else { return date; } } } };
2.3 formatDate
将时间戳转换为指定格式的日期;入参可以是秒级时间戳、毫秒级时间戳;
import { format } from 'date-fns'; /** * @author zxyue25 * @desc 将时间戳转换为指定格式的日期;入参可以是秒级时间戳、毫秒级时间戳; * 如果入参是秒级时间戳(10位),会乘以1000转换;格式默认为'yyyy-MM-dd HH:mm'; * 主要场景:通常情况下时间戳转换日期不传格式,而是用默认格式;后端返回的时间戳一般是秒级时间戳,如果直接用date-fns需要自己乘1000传入 * @param date - Date | Number * @param formatStr - String * @returns 返回格式化后的秒级时间戳 - Number * @example * ``` * formatDate(1658320372161) // 2022-07-20 20:32 * format(1658320372, 'yyyy-MM-dd HH:mm') // 1970-01-20 12:38 * formatDate(1658320372) // 2022-07-20 20:32 * formatDate(new Date()) // 2022-07-21 11:28 * formatDate(1658320372000, 'yyyy/MM/dd HH:mm:ss') // 2022/07/20 20:32:52 * ``` */ export const formatDate = (date: number | Date, formatStr = 'yyyy-MM-dd HH:mm') => { if (typeof date === 'number' && date.toString().length === 10) { return format(date * 1000, formatStr); } else { return format(date, formatStr); } };
2.4 formatDateRange
将两个时间戳或者Date日期转换为指定格式的日期,并用指定连接符连接;
import { formatDate } from './formatDate'; /** * @author zxyue25 * @desc 将两个时间戳或者Date日期转换为指定格式的日期,并用指定连接符连接; * 入参可以是秒级时间戳、毫秒级时间戳或者Date日期;如果入参是秒级时间戳(10位),会乘以1000转换; * 格式默认为`${yyyy/MM/dd} - `${yyyy/MM/dd}';如果想更改格式,可传入第三个参数(日期的格式),第四个参数(连接符的格式) * @param startDate - Date | Number * @param endDate - Date | Number * @param formatStr - String 默认格式'yyyy/MM/dd' * @param joinStr - String 默认连接符'-' * @returns 返回格式化后的日期区间字符串 * @example * ``` * formatDateRange(1658320372161, 1658717927699) // 2022/07/20 - 2022/07/25 * formatDateRange(1658320372, 1658717927) // 2022/07/20 - 2022/07/25 * formatDateRange(1658320372, 1658717927, '', '~') // 2022/07/20 ~ 2022/07/25 * formatDateRange(1658320372, 1658717927, 'yyyy/MM/dd HH:mm', '~') // 2022/07/20 20:32 ~ 2022/07/25 10:58 * ``` */ export const formatDateRange = ( startDate: number | Date, endDate: number | Date, formatStr = 'yyyy/MM/dd', joinStr = '-', ) => `${formatDate(startDate, formatStr || 'yyyy/MM/dd')} ${joinStr} ${formatDate(endDate, formatStr || 'yyyy/MM/dd')}`;
2.5 formatDateDistance
获取指定时间距离当前时间或者指定时间多远
import { formatDistance } from 'date-fns'; import { getMilliTimestamp } from './getMilliTimestamp'; import { LANGUAGE_DATE_FNS_MAP } from './locale'; /** * @author zxyue25 * @desc 获取指定时间距离当前时间或者指定时间多远; * @param date - Date | Number * @param baseDate - Date | Number,默认为当前时间 * @param options - 扩展项,可以配置语言,有两种方式:传入语言类型lang;或者直接传入Locale; * @returns 返回描述“指定时间距离当前时间或者指定时间多远”的字符串,有多语言处理 * @example * ``` * formatDateDistance(1658320372161, 1658717927699, { lang: 'zh-CN' }) // 5 天前 * formatDateDistance(1658320372161, 1658717927699, { lang: 'zh-CN', addSuffix: 'false' }) // 5 天 * formatDateDistance(1658320372, 1658717927) // 5 days ago' * formatDateDistance(new Date('2022-07-12'), new Date('2022-07-17')) // 5 days ago * formatDateDistance(new Date('2022-07-05'), new Date('2022-07-12')) // 7 days ago * formatDateDistance(new Date('2022-06-12'), new Date('2022-07-12')) // about 1 month ago * formatDateDistance(new Date('2021-07-12'), new Date('2022-07-12')) // about 1 year ago * formatDateDistance(1658320372, 1658717927, { locale: ko }) // 5일 전 * ``` */ type OptionType = { locale?: Locale; addSuffix?: boolean; lang?: keyof typeof LANGUAGE_DATE_FNS_MAP; }; export const formatDateDistance = ( date: Date | number = 0, baseDate: Date | number = new Date(), options?: OptionType, ): string => { const initOptions = { addSuffix: options?.addSuffix || true, locale: options?.locale || LANGUAGE_DATE_FNS_MAP[options?.lang || 'en'], ...options, }; if ( (typeof date === 'number' && date.toString().length === 10) || (typeof baseDate === 'number' && baseDate.toString().length === 10) ) { return formatDistance(getMilliTimestamp(date), getMilliTimestamp(baseDate), initOptions); } return formatDistance(date, baseDate, initOptions); };
2.6 formatDateDuration
将指定秒转为‘H小时M分钟S秒’,H、M、S为0时,默认不展示;如果想更改格式可传入第二个扩展参数options
import { formatDuration } from 'date-fns'; import { LANGUAGE_DATE_FNS_MAP } from './locale'; /** * @author zxyue25 * @desc 将指定秒转为‘H小时M分钟S秒’,H、M、S为0时,默认不展示;如果想更改格式可传入第二个扩展参数options * @param second - Number,多少秒 * @param options - 扩展项,可以配置语言,有两种方式:传入语言类型lang;或者直接传入Locale; * @returns 返回描述“H小时M分钟S秒”的字符串,有多语言处理 * @example * ``` * formatDateDuration(71) // 1minute11seconds * formatDateDuration(71, { lang: 'zh-CN' }) // 1分钟11秒 * formatDateDuration(3604, { lang: 'zh-CN' }) // 1小时4秒 * formatDateDuration(80221, { lang: 'zh-CN' }) // 22小时17分钟1秒 * formatDateDuration(80221, { locale: ko }) // 22시간17분1초 * formatDateDuration(80221, { lang: 'zh-CN', delimiter: ',' }) // 22小时,17分钟,1秒 * formatDateDuration(80221, { lang: 'zh-CN', format: ['hours', 'minutes'] }) // 22小时17分钟 * formatDateDuration(80220, { lang: 'zh-CN' }) // 22小时17分钟 * formatDateDuration(80220, { lang: 'zh-CN', zero: true }) // 22小时17分钟0秒 * formatDateDuration(880220, { lang: 'zh-CN' }) // 244小时30分钟20秒 * ``` */ type OptionType = { locale?: Locale; zero?: boolean; delimiter?: string; format?: Array<string>; lang?: keyof typeof LANGUAGE_DATE_FNS_MAP; }; export const formatDateDuration = (second: number, options?: OptionType) => { const hours = Math.floor(second / 3600); const minutes = Math.floor((second % 3600) / 60); const seconds = Math.floor(second % 60); return formatDuration( { hours, minutes, seconds, }, { zero: options?.zero || false, locale: options?.locale || LANGUAGE_DATE_FNS_MAP[options?.lang || 'en'], ...options, }, ).replace(/ /g, ''); };
最后
日期
处理是前端必定会遇到的问题,本文也只是一部分场景的封装,后续工作中遇到其他utils,也会继续封装在github
本文转载自:jjjona0215,版权归原作者所有,本博客仅以学习目的的传播渠道,不作版权和内容观点阐述,转载时根据场景需要有所改动。
最新评论