
前些天在翻一个老项目的代码时看到评论区底部还挂着一串“2024-06-12 14:32:58”这样的完整时间戳突然觉得特别违和。现在主流社区的评论、动态流、操作日志早就默认把时间显示成“3分钟前”“昨天”“2小时前”这类相对时间了谁还愿意去读一个精确到秒的时间戳再去心里换算我顺手把项目中所有时间展示都换成了 jQuery Prettydate 来做统一处理整个页面的人味一下子就出来了。jQuery Prettydate 是一个专门把时间戳或 ISO 日期字符串转换成“人性化相对时间”的小插件核心价值就一句话让用户不用做任何心算一眼就能判断这条内容是刚刚发生的、今天早些时候的还是一周前的。适合评论系统、聊天记录、订单日志、后台操作审计、内容动态流这类的场景尤其是那些还以 jQuery 为技术底座的存量项目接入成本几乎为零。1. “3分钟前”背后的产品逻辑时间显示不只是格式化1.1 从一段真实页面谈起为什么我抛弃了“2024-06-12 14:32”在一个老后台管理系统的操作日志页里每条记录都带一个“发生时间”字段。最初开发图省事直接new Date().toLocaleString()输出完整时间。测试的时候没觉得有问题等到真实运营人员用起来反馈就来了他们要在一屏里扫几十条记录每次都要先看到完整时间戳然后自己默默做减法“这个操作是昨天下午的还是前天下午的”效率极低而且很容易看错。后来我把这个页面切到 jQuery Prettydate日志时间变成“35分钟前”“昨天 16:20”“3天前”。运营同事再扫页面不需要任何换算直接根据时间短语的粒度就能快速判断记录的新旧肉眼可见地减少了误读。这里要说明一点相对时间并不是要取代绝对时间而是在“快速浏览场景”里做默认显示。真正需要精确追溯时用户把鼠标悬停到时间上或者点击查看详情仍然能看到原始时间戳。好的时间组件应该是“先给结论再给明细”。1.2 Prettydate 的定位它不是 moment.js也不是 dayjs经常有人问我现在都有 dayjs 了还有必要用这种老插件吗我的回答是看场景。dayjs、Moment.js 这类库本质是“日期处理工具箱”它们的relativeTime功能需要自己配置、自己封装模板。而 jQuery Prettydate 是一个“即插即用的成品”引入文件、调用一个函数或一个 jQuery 方法页面上所有带时间属性的元素就自动变成相对时间了。它不需要你理解什么locale配置、什么fromNow管道就是一个非常纯粹的转换器。更关键的是存量 jQuery 项目里往往已经跑着一堆基于 jQuery 的事件和 ajax 逻辑引入 Prettydate 不需要改变任何既有架构。如果你的项目压根不用 jQuery那当然没必要硬上一个 jQuery 插件直接用原生函数封装几行就够了但如果你已经在用 jQuery这个插件就是最小成本、最大收益的选型。2. 核心原理拆解从 John Resig 那 20 行经典算法说起2.1 经典 prettyDate 函数是怎么算的jQuery Prettydate 的源头可以追溯到 John Resig 在 2008 年写的一篇博客里的 prettyDate 函数。虽然十多年过去插件后续做了不少封装但核心算法一直没变。我把它简化为可读性更好的版本逻辑是下面这样的function prettyDate(time) { var date new Date((time || ).replace(/-/g, /).replace(/[TZ]/g, )), diff (Date.now() - date.getTime()) / 1000, dayDiff Math.floor(diff / 86400); if (isNaN(dayDiff) || dayDiff 0 || dayDiff 31) { return; } return dayDiff 0 ( diff 60 just now || diff 120 1 minute ago || diff 3600 Math.floor(diff / 60) minutes ago || diff 7200 1 hour ago || diff 86400 Math.floor(diff / 3600) hours ago ) || dayDiff 1 Yesterday || dayDiff 7 dayDiff days ago || dayDiff 31 Math.ceil(dayDiff / 7) weeks ago; }这个函数的计算过程非常直白先把 ISO 格式字符串里的-和T、Z做替换确保new Date()在 IE 老版本里也能正确解析这是当年兼容老浏览器的关键一步现代环境已经不那么必要了。算出当前时间与目标时间的差值单位换成秒。再把秒数换算成天数差值dayDiff。按照“秒级 → 分钟级 → 小时级 → 天级 → 周级”的顺序逐层判断命中哪个区间就返回哪个文案。这里的核心是 JS 的和||组合出来的“短路求值”特性只要命中一个条件后面的表达式就不会再执行。理解了这个短路逻辑就很容易自己扩展新的时间阈值。2.2 双阈值设计为什么 60 秒之后紧跟着 120 秒我刚看这段代码时有个疑问既然diff 60是“刚刚”为什么还要单独写一个diff 120的“1 分钟前”再往后才是“N 分钟前”后来想明白了这段逻辑刻意把“1 分钟整”这个边界做了特殊处理。如果不加 120 秒的阈值那么在diff 90秒时Math.floor(90 / 60)会得到 1输出就变成了“1 minutes ago”语法上很难看。用diff 120先把“1 分钟整”兜住直接输出单数形式就不用去写复数判断了。同理3600 秒到 7200 秒之间输出 “1 hour ago”也是为了避免出现 “1 hours ago”。在中文环境里我们不需要处理单复数但这个双阈值思想依然值得借鉴它可以让输出文案的粒度更加平滑而不是在临界点上出现突兀的 1/2 跳变。2.3 超过 31 天不再“人性化”而是直接交给调用方注意dayDiff 31的情况函数直接return了一个 undefined而不是输出“5 weeks ago”之类的话。这其实是刻意的设计决策。判断逻辑是超过一个月的时间差异用户已经很难凭“5 周前”快速感知具体时间点了这时候强行模糊化反而增加理解成本。所以插件在这里“退位”把原始时间留给调用方自己去展示。实际工程里我通常配合模板这样处理如果纯函数返回 undefined那就渲染成YYYY-MM-DD的绝对日期。用户看“2024-06-12”就知道这是两个月前的具体一天比“8 weeks ago”直观得多。3. 动手接入 jQuery Prettydate三种调用方式与验证清单3.1 文件引入与基础依赖我手上的项目还在用 jQuery 3.x所以直接下载了 prettydate 插件文件放在公共静态目录里。引入顺序有讲究先引 jQuery再引 jQuery Prettydatescript src/assets/jquery.min.js/script script src/assets/jquery.prettydate.js/script如果是通过 npm 管理的项目也可以按模块方式引入本质没什么区别。有一点要提醒这个插件既然依赖 jQuery 的全局对象jQuery就必须保证在插件文件加载前jQuery 已经被加载完成。我见过有人把顺序写反结果插件注册失败页面上所有时间都原样显示控制台报$.prettyDate is not a function。排查方式也简单打开控制台看类型错误指向哪个方法就行。3.2 方式一纯函数调用处理动态拼接的时间最简单的用法是直接调$.prettyDate(isoString)传入一个 ISO 8601 格式的时间字符串函数返回对应的相对时间文案。比如$.prettyDate(2024-06-12T14:32:00); // 假设当前是 2024-06-12 14:35 // 返回 3 minutes ago这种做法最适合用在 ajax 动态渲染模板的场景里。比如从后端接口拿到评论列表在拼接 HTML 时直接调用$.ajax({ url: /api/comments, type: GET, dataType: json, success: function(res) { var html ; res.data.forEach(function(item) { var prettyTime $.prettyDate(item.created_at) || formatDate(item.created_at); html div classcomment-item p item.content /p span classtime prettyTime /span /div; }); $(#commentList).html(html); } });这里有个关键细节$.prettyDate()如果解析失败或超过 31 天返回的是 undefined。所以我用|| formatDate(item.created_at)兜底保证页面上永远有可读的时间文本。千万不要直接把 undefined 拼进模板里不然页面上会出现一个 “undefined” 字符串容易让用户以为是 bug。3.3 方式二jQuery 方法调用批量美化既有 DOM如果你不想动后端返回的数据结构也不想改模板拼接逻辑可以直接用选择器选中所有带时间属性的元素一次性完成转换。这也是这个插件最便捷的地方time title2024-06-12T14:32:002024-06-12 14:32/time time title2024-06-11T08:00:002024-06-11 08:00/time$(function() { $(time).prettyDate(); });执行之后这两个time元素里的文本会被自动替换成类似 “3 minutes ago” 和 “Yesterday” 的文案。需要注意的是插件默认从title属性读取原始时间所以 HTML 里必须给元素加上title属性并且保持 ISO 格式。我也见过有的版本支持datetime属性接入前最好看一眼当前版本的源码注释。3.4 方式三初始化已完成渲染的整块容器有时候数据是后端模板引擎直接渲染到页面里的DOM 初始就存在但数量很多。此时不必逐个元素调用可以选中它们的共同容器$(function() { $(#messageList time).prettyDate(); $(#notificationList time).prettyDate(); });这么做的好处是避免对整个页面做全局扫描只处理需要美化的局部区域性能更可控。我在改造后台日志页时就是这么干的几十个time元素一次搞定页面无闪烁。3.5 完工后的验证清单替换完成之后我不会只看“页面上有没有变成相对时间”就收工而是会过一遍下面这几个检查点打开控制台确认没有undefined文本出现在时间里。用一个未来时间测试比如2025-01-01插件应该返回 undefined 走兜底逻辑而不是显示 “in the future”。用一个超过 31 天的历史时间测试确认显示的是绝对日期而不是奇怪的周数。在弱网或 ajax 延迟时刷新页面确认动态渲染的时间也能被正确转换。4. 本地化改造把 “2 hours ago” 变成 “2小时前”还要符合中文习惯4.1 改造输出映射的基本思路英文环境下插件的默认输出是 “30 minutes ago”“Yesterday”这类文案放到中文项目里显然不合适。不过我用的这个版本输出文案是集中在一个映射对象里的可以直接在加载插件之后做一次整体覆盖。我做的第一版改造只是简单地把英文文案换成逐字翻译英文原文直译方案最终采用的方案just now刚刚刚刚1 minute ago1分钟前1分钟前N minutes agoN分钟前N分钟前1 hour ago1小时前1小时前N hours agoN小时前N小时前Yesterday昨天昨天N days agoN天前N天前N weeks agoN周前N周前4.2 中文语境下的两个特殊优化直接翻译能用但离“好用的中文体验”还有点距离。我在实际项目里做了两个针对性优化。第一个是“1天前”的处理。中文里单数和复数都叫“天”没有英文的 day/days 区分所以这里反而简单了不需要 7200 秒到 86400 秒之间的特殊阈值直接统一成“N天前”就行。但我保留了算法里的双阈值分支只是把文案换成中文这样哪天要再输出“昨天”这种更口语化的表达改动成本很低。第二个是“昨天”和“前天”这种更符合中文表达习惯的称呼。英文习惯说 “Yesterday” 和 “2 days ago”中文里则常说 “昨天” 和 “前天”。我在覆盖映射时把dayDiff 1输出“昨天”把dayDiff 2输出“前天”其余天数统一输出“N天前”。这个细节做完之后整个时间文案的中文味道就对了。4.3 和 dayjs 混用的正确姿势有些页面已经在用 dayjs 做日期格式化我不想因为引入 prettydate 就两套逻辑打架。我的处理方式是页面上给用户的“相对时间”显示统一走 prettydate而需要精确到“某月某日某时某分”的地方仍然用 dayjs 的format(YYYY-MM-DD HH:mm)。两边各管一摊互不干扰。如果某个时间展示希望“今天显示时刻昨天显示昨天时刻更早显示完整日期”我会封装一个统一函数function chinesePrettyTime(isoString) { var relative $.prettyDate(isoString); if (relative) { return relative; } var d new Date(isoString); var today new Date(); var startOfToday new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime(); var target d.getTime(); if (target startOfToday) { return 今天 d.getHours() : String(d.getMinutes()).padStart(2, 0); } if (target startOfToday - 86400000) { return 昨天 d.getHours() : String(d.getMinutes()).padStart(2, 0); } return d.getFullYear() - (d.getMonth() 1) - d.getDate(); }这套方案落地之后评论区、日志页的体验是今天的记录显示“今天 14:32”昨天的显示“昨天 09:15”再早的显示“2024-06-01”一眼扫过去信息层次非常清晰。5. 定时刷新与性能再好的相对时间不更新也是“死时间”5.1 为什么必须引入定时刷新页面加载时把时间转成“3分钟前”如果用户停留在页面上 5 分钟不操作那个“3分钟前”就变成假信息了。这在评论区和消息通知场景里会造成误解用户以为这条内容是刚刚发布的实际上是几分钟前的。所以凡是做相对时间的页面必须配套“时间自动走动”的机制。我的做法是启动一个定时器每隔 60 秒重新计算一次页面上所有time元素的相对时间$(function() { function refreshPrettyTimes() { $(time[title], time[datetime]).prettyDate(); } refreshPrettyTimes(); setInterval(refreshPrettyTimes, 60000); });5.2 为什么是 60 秒而不是 10 秒有人可能会问既然是“刚刚”那种秒级变化为什么不用 10 秒刷新一次这里其实是性能和体验的平衡。绝大多数相对时间文案的最小粒度是“分钟”用户刷新看到“60秒前”和“1分钟前”的感知差异极小。但如果每 10 秒全量重刷一遍所有时间节点在评论很多、页面还有图表渲染的场景下会无谓地增加 DOM 操作频率。我一般把刷新间隔设在 60 秒既保证时间不会严重失真又不会对页面性能产生明显影响。5.3 大数据量列表下的性能注意点如果你的页面一次要渲染几百上千条带时间的条目全量扫描所有节点再逐一遍历确实会有开销尤其在低端移动设备上可能出现轻微卡顿。我的优化方案有两个第一控制刷新范围。只刷新当前视口内可见的时间节点或者按容器分批刷新避免无关节点被反复重算。比如无限滚动列表里只对已插入到 DOM 的节点做 prettyDate 处理。第二使用requestAnimationFrame替代setInterval把刷新操作合并到浏览器渲染帧里减少布局抖动。对于同时还有 echarts 图表的运营看板页面这个优化尤为重要因为图表实例本身也在持续重绘DOM 操作过于频繁会让两者互相抢资源导致掉帧。5.4 与 echarts 等重型组件共存的经验热搜词里提到“将原生 JS、jQuery、ajax、echarts 结合制作网页”我很理解这种场景。后台看板页面往往左边的数据表格是 jQuery ajax 渲染的右边是 echarts 图表标题栏还有一排“最后更新时间”。如果让这个“最后更新时间”每 60 秒刷新一次同时 echarts 还在用setInterval轮询更新数据两个定时器叠加在某些低性能机器上就会卡。我的经验是把时间刷新和图表刷新的定时器错开执行。时间刷新固定在第 30 秒触发图表数据刷新在第 0 秒触发或者干脆让图表数据刷新完成后再顺带刷新一次时间节点利用同一个回调减少重复遍历。比如function refreshDashboard() { refreshChartData(); // echarts 更新 $(.dashboard-time).prettyDate(); // 时间一并更新 } setInterval(refreshDashboard, 30000);这样页面同时只有一个主定时器在工作逻辑上也更清晰。6. 踩坑实录时区、动态内容、SEO 和 31 天之后的那些坑6.1 时区陷阱服务器时间戳和浏览器本地时间不一致这个坑我印象最深。项目后端在海外服务器返回的时间字段是带时区偏移的完整 ISO 字符串比如2024-06-12T08:00:00Z。这种字符串没问题new Date()会准确把它转成浏览器本地时间计算差值时是对的。但另一种情况就麻烦了。后端返回的是无时区的2024-06-12T08:00:00我一开始没有多想直接把它丢给 prettyDate。结果页面显示的时间比实际差了 8 小时。原因在于JavaScript 对没有时区标记的 ISO 字符串可能按本地时间解析也可能按 UTC 解析具体行为取决于浏览器实现和字符串格式。如果服务器存的是 UTC 时间但字符串没有带Z解析出来的时间戳就错了。我的对策是在后端接口统一返回带时区偏移的时间字符串如果后端改不了就在前端解析前先做一次标准化处理function normalizeIsoString(str) { if (typeof str ! string) return str; // 没有时区标记的按 UTC 处理补上 Z if (!/[Zz]|[-]\d{2}:\d{2}$/.test(str)) { return str Z; } return str; }这个看似不起眼的处理能让时间差计算在全球任何时区的浏览器上保持一致。6.2 动态加载内容的初始化时机ajax 回调里必须再调一次用插件的时候很多人只记得在$(function(){})里初始化一次却忘了 ajax 动态插入的新节点并不会自动被处理。页面初次加载后用户点“加载更多”拉到的新评论时间还是原始时间戳非常突兀。我踩过的坑就是这个。第一次改造时只在页面初始化时调了 prettyDate结果翻页加载更多数据后新渲染的时间全都没被转换。排查时才意识到ajaxsuccess回调里插入 DOM 之后需要对新节点再执行一次初始化success: function(res) { var html ; // ... 拼接 html $(#commentList).append(html); // 新节点插入后立即处理 $(#commentList time).prettyDate(); }更稳妥的做法是只对新容器处理比如给每次加载的返回数据包一层固定类名然后精确选中这个容器。这既保证了效率也不会影响到已经处理过的旧节点。6.3 SEO 与无障碍不能让爬虫只看到“3分钟前”这也是相对时间最容易忽略的问题。搜索引擎爬虫不会等你的脚本执行完再抓取内容如果页面上的时间全部被 JavaScript 替换成了 “3分钟前”爬虫抓到的就是一个没有精确日期的时间短语这对 SEO 是不友好的。同理屏幕阅读器用户听到的也只是模糊时间无法获知具体日期。我的处理原则是原始时间永远保留在机器可读属性里。jQuery Prettydate 的设计本身就支持这一点只要原始时间放在title或datetime属性里展示文本即使被替换机器仍然能读到底层数据。为了双保险我还会设置time元素的datetime属性为完整的 ISO 时间这样爬虫和辅助技术都能拿到精确值time title2024-06-12T14:32:00 datetime2024-06-12T14:32:003 minutes ago/time6.4 31 天之后显示什么给“老龄化时间”一个体面的退路前面提到超过 31 天插件会返回 undefined。很多人在这一步犯难不知道显示什么好。我见过最粗暴的做法是直接把原始时间戳显示出来比如1718183520000那体验比英文的 weeks ago 还糟。我的默认策略是三级退路一个月内显示相对时间超过一个月但仍是今年显示“6月12日”去年及更早显示“2024-06-12”。这样信息和阅读成本是逐步递增的符合用户对时间粒度的预期。function smartTimeDisplay(isoString) { var relative $.prettyDate(isoString); if (relative) return relative; var d new Date(isoString); var now new Date(); if (d.getFullYear() now.getFullYear()) { return (d.getMonth() 1) 月 d.getDate() 日; } return d.getFullYear() - (d.getMonth() 1) - d.getDate(); }把这个函数作为通用时间展示出口在项目里统一调用后面再遇到“超过一个月显示什么”的问题就不用重复讨论了。6.5 老项目引入新插件的边界尽量不污染全局如果你的项目里已经有大量针对time元素的事件绑定直接全量执行$(time).prettyDate()可能改变多个页面的行为。我最后一次改造时特意给需要的元素加了.js-pretty-date类然后只处理带这个类的节点避免跟项目里其他逻辑互相干扰。这种做法虽然多了一步标记但长期维护起来省心得多——你永远不会突然发现某个本来显示完整日期的地方变成了一串英文相对时间。7. 写在最后的经验这类插件的价值边界和我的使用习惯用 jQuery Prettydate 做了几个项目之后我的体会是它不是什么高深的技术但解决的问题非常具体。时间显示这个细节恰恰是产品“有没有用心”的最直观体现。一个把时间显示成“2024-06-12 14:32:58”的评论区和一个显示成“5分钟前”的评论区给用户的温度感是完全不同的。而且这类老插件的价值不在于“新”而在于“稳”。核心算法十几年没什么变化说明它的设计已经足够成熟。即便未来新项目不可能再用 jQuery我也会借鉴它这套阈值判断思路自己封装一个原生 JS 版本几行代码就能复刻。最后再分享一个小技巧如果你在同一页面同时处理多个时区的用户千万别只依赖浏览器本地时间。最稳妥的方案是拿到时间字符串后先在公共函数里统一转成时间戳再参与差值和文案计算。这样不管用户浏览器设置在哪个时区相对时间的计算结果都不会乱。