wp-calypso 组件库 TimeSince 详解:用 `@automattic/components` 实现自动刷新的友好相对时间显示

原创2026-10-07 23:57:481,898 阅读
文章标签:前端CMS

wp-calypso 组件库 TimeSince 详解:用 @automattic/components 实现自动刷新的友好相对时间显示

导读

TimeSince 是 wp-calypso(WordPress.com 的 JavaScript 与 API 前端仓库)@automattic/components 组件库中的一个轻量级时间组件,用于把任意日期转换成「相对于当前时刻」的人类友好格式,并随着时间推移自动刷新。本文以 组件 README 为骨架,结合其核心实现、单元测试 与真实业务使用场景,完整讲解它的显示规则、Props 参数、三种 dateFormat 取值、底层实现原理与边界处理,帮助你在自己的 Calypso 页面或独立应用里快速落地「刚刚 / 5 分钟前 / 2 天前」这类体验良好的时间展示。

一、组件定位与设计目标

README 开篇给出了组件的两条核心承诺:

  1. 以人类友好的格式显示相对于现在的日期(Shows a date relative to now in a human friendly format);
  2. 随时间推进自动更新显示(Updates the display as time progresses)。

这两点决定了它与普通 <time> 静态标签的本质区别:TimeSince 是一个"会动"的时间组件,适合用在会话列表、活动日志、订阅详情等需要持续感知时效性的场景。

在仓库中它经由 packages/components/src/index.ts#L36 统一导出(export { default as TimeSince } from './time-since';),因此业务代码只需一行导入即可使用:

import { TimeSince } from '@automattic/components';

二、显示规则:从"刚刚"到"绝对日期"的分段策略

README 给出了三种典型输出形态:

时间跨度 输出示例
一分钟以内 Just now
一周以内 20h ago、5d ago
超过一周 Oct 30, 2017

组件源码中的分段逻辑把它精确实现为五个档位:

  1. 小于 60 秒 → 显示 just now(通过 translate( 'just now' ) 翻译,因此支持 i18n);
  2. 小于 60 分钟 → 显示 Xm ago(如 5m ago);
  3. 小于 24 小时 → 显示 Xh ago(如 3h ago);
  4. 小于 7 天 → 显示 Xd ago(如 2d ago);
  5. 大于等于 7 天 → 回退为绝对日期格式(默认 ll 中格式,如 Jun 12, 2024)。

源码注释中标注的是基于 Intl.RelativeTimeFormat / Intl.DateTimeFormat 的思路,实际实现则以分钟/小时/天为单位的整数向下取整计算(Math.floor),并借助 i18n-calypso 的 useTranslate 完成字符串本地化:

const secondsAgo = Math.floor( millisAgo / 1000 );
const minutesAgo = Math.floor( secondsAgo / 60 );
const hoursAgo   = Math.floor( minutesAgo / 60 );
const daysAgo    = Math.floor( hoursAgo / 24 );

三、Props 完整说明

README 仅列出两个参数,而组件类型定义实际暴露了三个,如下表所示:

Prop 类型 必填 默认值 说明
date string 是 — ISO 8601 日期字符串,或任何能被 new Date() 解析的字符串;运行时传入 Date 对象同样可用
dateFormat string 否 'll' 覆盖超过 1 周(含未来日期)时的格式化方式,取值 ll / lll / llll
className string 否 — 应用到 <time> 元素上的 CSS 类名

需要留意一个细节:README 将 date 描述为 Date,但当前源码的类型签名是 string(并注明「任何可被 new Date() 解析的字符串」)。组件内部通过 new Date( date ) 统一转换,所以两种形态都能正常工作,示例文件 docs/example.jsx 中就是直接传入 new Date( ... ) 对象。

3.1 dateFormat 的三种取值

这是 README 提到但未展开的参数,其语义在源码注释与 formatDate 实现中非常清晰:

  • 'll'(默认):本地化中等日期格式。底层对应 Intl.DateTimeFormat 的 dateStyle: 'medium',即英文环境输出 Jun 12, 2024,其他语言按各自惯例输出(如 2024年6月12日)。
  • 'lll':固定输出 d MMM y(如 12 Jun 2024),不受 locale 影响。实现上是把 dateStyle/timeStyle 置空,改用 day: 'numeric', month: 'short', year: 'numeric' 三个显式选项。该取值由 PR #103586 引入,在 CHANGELOG 中有记录。
  • 'llll':完整日期加时间。对应 dateStyle: 'full', timeStyle: 'medium',英文环境输出类似 Wednesday, June 12, 2024 at 2:00:00 PM。这一格式不用于主文本,而是被组件用作悬停提示(title 属性)。

四、自动刷新机制与实现原理

4.1 每 10 秒刷新一次

useRelativeTime hook 是整个组件的核心:它先用 useState 保存一个 now 快照,再通过 useEffect 注册 setInterval,每 10 秒把 now 更新一次:

useEffect( () => {
	const intervalId = setInterval( () => setNow( new Date() ), 10000 );
	return () => clearInterval( intervalId );
}, [] );

组件卸载时 clearInterval 会被自动调用,不会造成定时器泄漏。10 秒的刷新周期与活动日志中的轮询节奏(EVERY_TEN_SECONDS)保持一致,既保证显示及时,又避免高频重渲染。

4.2 重算与记忆化

now、date、dateFormat、translate、localeSlug 任一变化时,useMemo 会重新计算展示文本;由于相对时间文本本身是纯函数结果,这种记忆化让组件在 10 秒间隔之外的重新渲染中几乎零开销。

4.3 语义化的 <time> 输出

组件最终渲染的是一个语义化 <time> 元素(index.tsx#L130-L134):

<time className={ className } dateTime={ date } title={ fullDate }>
	{ humanDate }
</time>
  • dateTime 保留原始 ISO 日期,供机器读取与无障碍工具解析;
  • title 附上 llll 格式的完整日期时间,用户悬停即可看到精确时刻;
  • 自定义 className 可无缝接入现有样式体系,例如威胁告警组件中的 activity-log__threat-alert-time-since。

五、边界情况:无效日期与未来日期

READMEE 没有覆盖边界行为,但源码与测试用例给出了明确约定:

  • 无效日期(如 "not-a-date"):isNaN( dateObj.getTime() ) 为真,useRelativeTime 与 formatDate 双双返回空字符串,页面上不显示任何内容;
  • 未来日期(millisAgo < 0):不按相对时间处理,直接输出格式化日期,避免出现负的 -1m ago;
  • 超过 7 天的旧日期:同样进入 formatDate 分支,输出 ll(或用户指定的 dateFormat)格式的绝对日期。

六、完整使用示例

组件示例文件演示了四种典型时间跨度的用法,可直接复制:

import TimeSince from '..';

const TimeSinceExample = () => {
	return (
		<div>
			<div>
				{ /* 30 秒前 → just now */ }
				<TimeSince date={ new Date( Date.now() - 30 * 1000 ) } />
			</div>
			<div>
				{ /* 5 分钟前 → 5m ago */ }
				<TimeSince date={ new Date( Date.now() - 5 * 60 * 1000 ) } />
			</div>
			<div>
				{ /* 3 天前 → 3d ago */ }
				<TimeSince date={ new Date( Date.now() - 3 * 24 * 60 * 60 * 1000 ) } />
			</div>
			<div>
				{ /* 5 个月前 → 绝对日期 */ }
				<TimeSince date={ new Date( Date.now() - 5 * 30 * 24 * 60 * 60 * 1000 ) } />
			</div>
		</div>
	);
};

TimeSinceExample.displayName = 'TimeSinceExample';

export default TimeSinceExample;

若要覆盖 7 天以上日期的展示格式,可传入 dateFormat:

<TimeSince date={ someISOString } dateFormat="lll" />   // 固定输出 12 Jun 2024
<TimeSince date={ someISOString } className="my-time" /> // 附加自定义样式类

七、在 WordPress.com 真实业务中的应用

TimeSince 并非孤立组件,它在 wp-calypso 的多处业务界面中被实际使用,可作为接入参考:

这些场景的共同点是:时间既有"新鲜度"语义(需要相对时间),又有"精确性"需求(需要完整日期),正好对应 TimeSince 的「相对展示 + title 完整日期」双通道设计。

八、测试验证:行为如何被锁定

测试文件使用 jest.useFakeTimers() 与 jest.setSystemTime 把系统时间冻结在 2024-06-12T12:00:00Z,再通过 @testing-library/react 断言各种时间跨度下的输出:

  • 30 秒前 → just now;
  • 5 分钟前 → 5m ago;
  • 3 小时前 → 3h ago;
  • 2 天前 → 2d ago;
  • 10 天前 → Intl.DateTimeFormat 的 medium 日期格式;
  • 无效日期 → 渲染空字符串;
  • 未来 1 小时 → 渲染格式化日期;
  • className 与 title 属性被正确应用;
  • dateFormat="lll" → 输出 d MMM y 形式。

测试对 i18n-calypso 的 useTranslate 做了 mock,把 %(minutes)dm ago 等翻译模板展开为 5m ago 形式,这与生产代码中通过 translate 携带 args 与 comment(如 example for a resulting string: 2m ago)注入翻译模板的实现方式一一对应。

九、小结与使用建议

TimeSince 是一个 API 极简但设计完整的时间组件:一分钟内显示「刚刚」,一周内显示「X 分钟/小时/天前」,更早的日期回退为本地化绝对日期,且每 10 秒自动刷新。实际使用时建议:

  • 传入 ISO 8601 字符串(如 threat.first_detected 这类 API 返回的日期值),保持类型与源码签名一致;
  • 需要统一的多语言排期展示时用默认 ll,需要与 locale 无关的紧凑日期(如票据编号式排版)时用 lll;
  • 复用 className 融入既有样式,并善用内置的 title 完整时间提示;
  • 若对刷新频率有特殊要求(如实时性要求更高的聊天界面),可参考 10 秒 setInterval 的实现按需调整周期,但注意权衡重渲染成本。

对于希望深度定制的开发者,可以从 README 出发,对照 核心实现 与 测试用例,快速掌握其全部行为边界。

登录后查看全文
wp-calypso