加一个月,跳到三月三

加一个月,跳到三月三

10 月 28 号 Node 26 转成 LTS,代号 Lithium。官方那条发布说明写得挺实在:除了把 process.release 里的元数据改成 LTS,跟上一个版本没有任何代码差异。

我盯着这个版本看,是因为它默认开着 Temporal。这个日期时间 API 在 TC39 那边磨了很久,中间改过名、砍掉过一版最激进的方案,去年还有人把它归到“大概永远落不了地”那一类提案里。现在服务端开箱就有。

浏览器这边没那么整齐。Chrome 今年 1 月的 144 版本把它放进去了,Firefox 更早一些,Safari 我印象里还在追,具体到哪一版我不敢讲。同样一句日期加减,跑在 Node 里是标准行为,丢进某个用户的浏览器就是 undefined。

三号那天

先讲为什么会想起这件事。

我们有一种“一个月持有期”的产品,前端要给用户显示到期日。逻辑很朴素:拿申购确认日,月份加一。代码是那种谁都写得出来的写法,new Date 出来,setMonth 一加,收工。

一月底确认的那几笔,页面上给出的到期日是三月三号。

原因二月没有三十一号。Date 不会拒绝你,它老老实实算出一个不存在的日子,然后自己往前滚。平年闰年滚出来的还不一样,我当时拿 2026 和 2024 各试了一次,一个三月三、一个三月二。看着只是几天的事,落在用户那边是“我以为周三能赎,结果还得再等”。

这锅该谁背,内部扯过一轮。后端算的没错,产品规则写的没错,测试用例覆盖的申购日是月初到月中那批常用值。最后错在一行看不出任何毛病的代码上,那行还是三年前写进去的,被十几个页面复用。

坑只是表象

网上数 Date 坑位的文章一抓一大把:月份从 0 开始、getYear 的遗产、字符串解析、夏令时。背下来确实能躲开。我以前也是这么理解的,还跟新人说过“封一个 date 工具函数就完事了”。这话现在看是错的。

Date 真正的麻烦在于它只有一种类型,可日期时间的语义有好几种。不带时区的“几月几号”,比如产品成立日。绝对时刻,比如扣款那一瞬间。带时区的墙上时间,比如“北京时间下午三点收盘”。还有一段时长,比如 180 天。这几件事在 Date 里共用同一个对象,出错就出在“我以为它是第一种,其实它是第二种”的地方。

最典型的是这一对写法:

new Date('2026-03-01')
new Date('2026/03/01')

前者按 UTC 零点解析,后者按本地时区。在东八区差八小时,一跨零点,展示出来的日期少一天。这两种写法在 code review 里肉眼看不出区别,只在特定业务时区、特定渲染时刻才现形,所以能活很多年。

Temporal 做的事是把这几类拆开:PlainDate 管“几号”,Instant 管“哪一刻”,ZonedDateTime 管“哪个地方的墙上时间”,Duration 管“多久”。对象一律不可变,加减给你新的。加减月份会归一,月底加一个月不跳月。

Temporal.PlainDate.from('2026-01-31').add({ months: 1 })
// 2026-02-28

就这一条,能省掉一个我见过不止一次的线上问题。

服务端有了,浏览器还没有

服务端先行带来一个别扭局面:同一套日期规则要在前后端算出同一个结果,但两边运行时能力不一样。

我们没做批量替换。试过一次,把某个页面的 Date 调用全换成 Temporal,diff 出来几百行。谁能审到“这里原本依赖了隐式时区”这个级别?审不动。后来定的口径是新代码默认用,存量只在真正出错的那处改。

polyfill 我也摸过一轮,用的包压缩后二十 KB 出头?记不准确了。钱花在另外两处。一处是能力检测,不能只判断 Temporal 这个对象在不在,得看你实际要调的那几个方法在不在,早期有些环境里对象是有的、方法是缺的。另一处是它跟着 Intl 的时区数据库版本走,同一句表达式在不同构建环境能给出不同结果,ICU 版本没对齐的时候最难查,因为它不报错,只是悄悄给你一个别的日期。

顺带说个跟这个话题关系不大的:Node 从 27 开始改成一年一个版本,每个大版本都进 LTS。以后升级策略能写成一句固定条款,不用每年猜哪个版本能长期用。对我这种要维护一堆存量服务的人,这条比 Temporal 还实在。

先动边界,别动库

不建议先动库。dayjs、moment 在存量项目里活得很好,我也不觉得它们会因为这批原生 API 快速退场。原生覆盖到我们需要兼容的那批内核,还得些年头。

该先管住的是字符串这一层。接口返回的日期到底带不带时区、是不是补零、能不能直接塞进 Date 构造函数,写进规范并且 lint 起来。裸的 new Date(字符串) 应该被当成危险写法。

业务日期的加减别放前端。持有期、到期日、可赎回日这类,服务端算完给结论,前端只渲染。为什么非要这样?因为前端算出来的日期错一天,事后没人能证明用户当时看到的是哪个值。

还有一条测试值得加:找几个月底的输入,一月三十一、二月闰不平那种,加一个月、加一年,看各自归一成什么。成本大概五分钟,守的是那种一上线就要给客服解释的问题。

还有一件事没查

写到这里想起另一件事。那些两三年前搭的老页面里,有多少日期是在拿字符串直接比大小的。'2026-11-01' 和 '2026-1-15' 这么比肯定错,而且错得很安静。

一直没排期去扫。我猜不会比三月三号那个案例少,但真扫出来是多少,说不准。

You voted 2. Total votes: 2

添加新评论