指南 · 专家
版本基线 date-fns 4.x。深入内核与演进:v2→v3→v4 破坏性变更全梳理、v4 时区
in选项与类型推断、@date-fns/tz机制、UTCDate、迁移实战与陷阱。
一、版本演进与节奏
| 版本 | 时间 | 头条 |
|---|---|---|
| v2.0 | 2019 | 引入 tree-shaking 友好的具名导入、formatRelative 等 |
| v3.0 | 2023-12 | ESM 化大重构(dual-package、扁平结构、具名导出、弃 IE) |
| v4.0 | 2024-09 | 首次内置一等时区支持(@date-fns/tz + in 选项) |
| v4.1 | 2024-09 | 给 format 系列补全时区支持 |
官方在 v4 说明里点明:v2 与 v3 间隔约四年,而 v3→v4 不到一年,并承诺今后保持节奏、尽量减少破坏性变更。v4 本身的破坏性变更主要是类型层面。
二、v3 破坏性变更全梳理(从 v2 升级必读)
v3 是一次彻底的现代化重构,核心 BREAKING:
dual-package(ESM + CommonJS):导出在
package.json显式声明,ESM 文件改用.mjs扩展名。扁平结构:函数文件直接是
node_modules/date-fns/add.mjs,locale 是date-fns/locale/enUS.mjs。具名导出取代默认导出:
js// v2 const addDays = require("date-fns/addDays"); // v3:改为解构 const { addDays } = require("date-fns/addDays"); // 或从 "date-fns" 顶层解构constants移出顶层:import { daysInYear } from "date-fns/constants"(改善 Next.js 等的 modularizeImports 兼容)。不再校验参数个数 / 不再显式转换参数类型:交给类型检查器,函数更精简;代价是纯 JS 误用少了运行时拦截。
接受字符串日期:所有接受日期参数的函数现在也接受字符串(自动规范化)。
Interval 函数不再抛错:
start晚于end按负向区间处理(详见进阶篇);intervalToDuration跳过 0 值字段。roundToNearestMinutes:nearestTo< 1 或 > 30 时返回Invalid Date(不再抛错)。统一
Math.trunc舍入:differenceInX新增roundingMethod选项,默认向零截断。弃 IE、弃 Flow:拥抱现代 JS / ESM;TypeScript 类型完全重写。
新增
interval()函数:显式校验区间,模拟 v2 行为。
三、v4 时区支持深入
v4 的头条:「ten years after its release, date-fns finally gets first-class time zone support」。机制有两层:
1) context in 选项
所有相关函数新增 in 选项,指定计算所在时区;若函数返回日期,结果也在该时区:
import { addDays, startOfDay } from "date-fns";
import { tz } from "@date-fns/tz";
startOfDay(addDays(Date.now(), 5, { in: tz("Asia/Singapore") }));
//=> "2024-09-16T00:00:00.000+08:00"跨时区「同一天」判断是最典型的用例——同一 UTC 时刻在不同时区可能跨日:
import { isSameDay } from "date-fns";
const sg = new Date("2024-03-13T22:00:00+08:00");
const la = new Date("2024-03-13T06:00:00-07:00"); // 同一 UTC 时刻
isSameDay(sg, la, { in: tz("Asia/Singapore") }); //=> true
isSameDay(sg, la, { in: tz("America/New_York") }); //=> false2) 混合类型与类型推断
v4 允许函数参数(及 Interval 的 start/end)是不同类型(TZDate、UTCDate、原生 Date、字符串、数字混用),库会归一化后计算,并返回与上下文一致的类型。推断优先级:
- 若传了
in选项 → 用它; - 否则取第一个遇到的对象类型;
- Interval 的
start与end分开判断,start优先。
clamp(Date.now(), {
start: new TZDate(start, "Asia/Singapore"),
end: new UTCDate(),
});
//=> TZDate(首参是数字,start 优先于 end)四、@date-fns/tz 机制
@date-fns/tz(v4 官方时区包)的核心导出:
TZDate:扩展自原生Date、携带时区信息的类,能与所有 date-fns 函数协作并保留该时区;TZDateMini是不含格式化器的轻量版。jsimport { TZDate } from "@date-fns/tz"; import { addDays } from "date-fns"; const d = new TZDate(2024, 2, 13, "Asia/Singapore"); addDays(d, 1).toString(); //=> '... GMT+0800 (Singapore Standard Time)' d.withTimeZone("America/New_York"); // 同一时刻换时区表示tz(timeZone):上下文提供器,专供 date-fns 函数的in选项;内部把输入转时间戳并按指定时区构造TZDate。辅助:
tzOffset(偏移分钟)、tzScan(探测 DST 切换)、tzName(时区名)。
与
date-fns-tz(第三方 marnusw 维护)的关系:是两个不同的包。date-fns-tz在 v4 之前补时区,提供formatInTimeZone/toZonedTime/fromZonedTime,基于 Intl、不打包时区数据。@date-fns/tz是官方在 v4 推出的「一等公民」方案。二者非改名、非别名,按需择一。
五、UTCDate:纯 UTC 计算
需要「就好像本地时区是 UTC」地做计算(避免本地时区干扰),用 @date-fns/utc 的 UTCDate:
import { UTCDate } from "@date-fns/utc";
import { addHours, startOfDay } from "date-fns";
const d = new UTCDate(2024, 0, 1);
startOfDay(d); // 以 UTC 视角取当天起点v3 起函数就能检测这类自定义 Date 扩展,按入参构造器返回同类型结果,因此
UTCDate与全部函数无缝协作。
六、迁移实战与陷阱清单
- Moment → date-fns:格式串先改 token(
YYYY→yyyy、DD→dd);链式.add().format()改成函数组合format(addDays(d,1), '...')(或用date-fns/fp管道);moment.utc()→UTCDate;moment.tz()→TZDate/in: tz(...)。 - v2 → v3:把
require('date-fns/x')默认导入改解构;constants改从date-fns/constants导入;依赖运行时参数校验的代码补上 TS 类型。 - 常见陷阱:
- 构造函数月份 0-based(
new Date(2024,1,11)是二月); - 误用原生
date.setHours()(原地改)当成 date-fns 的setHours(date, h)(返新值); - 命名空间导入
import * as破坏 tree-shaking; - 忘了
parse必须传第三个referenceDate; - 跨时区场景不传
in选项,结果默默用了系统时区。
- 构造函数月份 0-based(