资讯详情

资讯详情

页面倒计时用 localStorage 缓存:setInterval 刷新与恢复的完整实现

1. 活动页倒计时刷新归零问题到底出在哪页面倒计时在活动页、秒杀页里几乎是标配但很多人第一次写都会踩同一个坑用setInterval每秒把变量减一页面一刷新变量回到初始值倒计时直接重置。用户抢购到一半刷新页面发现剩余时间又变回 15 秒体验非常割裂。这个问题的本质是倒计时的状态存在了内存里而内存会随页面刷新被清空。setInterval只是驱动渲染的引擎它不负责保存状态。真正需要持久化的是「截止时间戳」这个数据而不是「剩余秒数」这个瞬时值。我见过两种常见写法都有问题。第一种是纯内存变量let seconds 15; setInterval(() { seconds--; document.getElementById(time).innerText seconds; }, 1000);刷新即归零。第二种是照搬网上片段把剩余秒数存进localStorage每秒读一次写一次localStorage.setItem(gooseTime, 5); function run() { var s document.getElementById(goose_time); s.innerHTML localStorage.getItem(gooseTime); if (!localStorage.getItem(gooseTime) || s.innerHTML 0) { clearInterval(timeId); localStorage.removeItem(gooseTime); return false; } s.innerHTML s.innerHTML * 1 - 1; localStorage.setItem(gooseTime, s.innerHTML); } var timeId setInterval(run();, 1000);这段代码能跑但隐患不少。它存的是「剩余秒数」每秒读写一次localStorage性能浪费setInterval(run();, 1000)用字符串传参会走eval路径严格模式下直接报错更关键的是它没有记录「这个倒计时是什么时候开始的」一旦用户手动改系统时间或者标签页被浏览器挂起倒计时就会跳变。正确的思路是只存截止时间戳渲染时用「截止时间 - 当前时间」实时计算剩余量。这样刷新恢复、跨标签页同步、时间校准三件事都能自然解决。下面我把这套方案拆成可复制的工具函数和配置一步步带你落地。本篇适合正在做活动页、秒杀页、限时优惠弹窗的前端同学也适合想搞清楚localStorage缓存与setInterval配合方式的人。核心检索词就是「页面倒计时 localStorage 缓存 setInterval 刷新恢复」你跟着做就能得到不跳变、不重置的倒计时。2. 用 TaoToken 快速搭一个可调试的倒计时实验环境写倒计时这种逻辑最怕的是「改一行、刷一次、看结果」的循环太慢。我习惯先搭一个能实时对话调试的环境把边界情况问清楚再动手。这里用 TaoToken 来做辅助它提供统一的模型对话入口适合边写边验证思路。TaoToken 是什么简单说它是一个聚合式的 AI 模型调用平台你可以在一个控制台里切换不同模型来对话、写代码、做代码审查。对前端同学来说最实用的场景是把倒计时的边界问题比如标签页挂起后setInterval会不会累积、Date.now()和performance.now()该用哪个直接丢给模型让它给出对比和示例比翻文档快。适合谁用三类人一是刚接触localStorage和定时器、需要有人解释每一步为什么这么写的新手二是做活动页需要快速验证倒计时逻辑的老手三是想把倒计时封装成通用工具函数、需要模型帮忙做代码审查的团队。接入前你需要准备三样东西我把它叫「三件套」Base URLhttps://taotoken.net/apiAPI Key在控制台的 API Keys 页面创建形如sk-开头的一串字符Model ID在模型对话页面能看到当前可用的模型标识比如claude-sonnet-4-5这类拿到三件套后你可以先在模型对话页面直接开聊把下面这段提示词丢进去我在写一个活动页倒计时用 localStorage 存截止时间戳setInterval 每秒渲染。 请帮我分析1) 标签页被浏览器挂起后 setInterval 的行为 2) 用 Date.now() 还是 performance.now() 做时间基准更合适 3) 跨标签页同步倒计时有哪些方案。给出可运行示例。模型会给你一份对比分析你拿着结论再去写代码方向就不会偏。如果你更习惯在编辑器里调试可以走 API 接入把 Base URL 和 Key 配到你的请求工具里用 curl 先验证连通性curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d { model: claude-sonnet-4-5, messages: [{role: user, content: 解释 localStorage 存截止时间戳做倒计时的原理}] }返回里能看到choices[0].message.content说明链路通了。这一步的意义不是让模型替你写代码而是把「为什么存时间戳而不是存秒数」这个核心问题先想透。想清楚之后下面的配置和函数你抄过去就能用。如果你打算长期做这类活动页开发反复调试倒计时、优惠券过期、限时弹窗这些逻辑可以考虑 Coding Plan把模型对话和代码生成整合进日常流程省去每次重新配环境的麻烦。3. 可复制的倒计时工具函数与缓存读写配置这一节是核心我给你一份可以直接落地的配置和函数。先明确设计原则缓存里只存一个字段——截止时间戳毫秒渲染时实时算差值。这样无论刷新多少次、开多少个标签页大家读到的都是同一个截止时间天然一致。先看缓存读写的封装。我把它单独抽出来方便你替换成sessionStorage或加命名空间前缀// countdown-store.js const STORAGE_KEY activity_countdown_deadline; /** * 写入截止时间戳 * param {number} deadline 毫秒时间戳 */ export function setDeadline(deadline) { localStorage.setItem(STORAGE_KEY, String(deadline)); } /** * 读取截止时间戳不存在返回 null * returns {number|null} */ export function getDeadline() { const raw localStorage.getItem(STORAGE_KEY); if (raw null) return null; const num Number(raw); return Number.isFinite(num) ? num : null; } /** * 清除截止时间戳 */ export function clearDeadline() { localStorage.removeItem(STORAGE_KEY); }注意这里做了Number.isFinite校验防止有人手动往localStorage里塞了非数字字符串导致NaN参与计算。这是很多人忽略的防御点。接下来是倒计时工具函数。它接收一个 DOM 元素、一个持续时长秒内部负责「首次进入时初始化截止时间、刷新时恢复、到点后清理」// countdown.js import { setDeadline, getDeadline, clearDeadline } from ./countdown-store.js; /** * 启动倒计时 * param {HTMLElement} el 显示剩余秒数的元素 * param {number} durationSec 倒计时总时长秒 * param {Function} [onEnd] 结束回调 * returns {Function} 停止函数 */ export function startCountdown(el, durationSec, onEnd) { let deadline getDeadline(); // 缓存不存在或已过期重新初始化 if (deadline null || deadline Date.now()) { deadline Date.now() durationSec * 1000; setDeadline(deadline); } let timerId null; function render() { const remainMs deadline - Date.now(); if (remainMs 0) { el.textContent 0; clearInterval(timerId); clearDeadline(); if (typeof onEnd function) onEnd(); return; } // 向上取整避免显示 0 秒但实际还有几百毫秒 el.textContent String(Math.ceil(remainMs / 1000)); } render(); // 立即渲染一次避免首帧空白 timerId setInterval(render, 1000); // 返回停止函数组件卸载时调用 return function stop() { clearInterval(timerId); }; }这份代码有几个关键点值得展开。第一render里用Math.ceil(remainMs / 1000)而不是Math.floor因为剩余 0.3 秒时应该显示 1 而不是 0否则用户会觉得倒计时提前结束了。第二首次进入时先render()一次再启动setInterval避免页面出现一秒的空白。第三setInterval的间隔是 1000ms但渲染值来自实时计算所以即使定时器有轻微漂移显示的数字也不会累积误差。如果你用的是 Vue 或 React把startCountdown包在生命周期里即可。React 示例import { useEffect, useRef } from react; import { startCountdown } from ./countdown.js; export default function Countdown({ duration 15, onEnd }) { const ref useRef(null); useEffect(() { const stop startCountdown(ref.current, duration, onEnd); return stop; // 卸载时清除定时器 }, [duration]); return span ref{ref}--/span; }Vue 3 组合式 API 类似在onMounted里启动、onUnmounted里停止。这里要提醒一句组件卸载一定要清定时器否则路由切走后定时器还在跑控制台会报「操作已卸载 DOM」的警告。配置层面还有一个容易忽略的点localStorage是按域名隔离的。如果你的活动页在activity.example.com而主站在www.example.com两者缓存不互通。跨子域共享需要额外方案这个在第五节排障里会讲。4. 验证请求与成功结果刷新恢复、跨标签页同步、时间校准代码写完不算完得验证三类动作确保倒计时真的不跳变、不重置。我按顺序给你演示。第一类刷新恢复。打开页面倒计时从 15 开始走。走到 10 的时候按 F5 刷新。正确结果是刷新后直接从 10 继续而不是回到 15。你可以在控制台执行localStorage.getItem(activity_countdown_deadline)会看到一个毫秒时间戳比如1735689600000。刷新前后这个值不变说明缓存生效。如果刷新后归零检查是不是每次进入都无条件调用了setDeadline把恢复逻辑覆盖了。第二类跨标签页同步。把同一个活动页在两个标签页里打开。标签页 A 走到 8 秒时切到标签页 BB 应该也显示 8 秒左右而不是各自独立计时。因为两个标签页读的是同一个localStorage截止时间戳渲染时都用deadline - Date.now()所以天然同步。你可以再进一步监听storage事件做实时联动window.addEventListener(storage, (e) { if (e.key activity_countdown_deadline) { console.log(截止时间被其他标签页修改, e.newValue); // 这里可以触发重新渲染 } });注意storage事件只在「其他标签页修改」时触发当前标签页自己改不会触发这是规范行为别误以为是 bug。第三类时间校准。这是最容易被忽略的。假设用户在倒计时进行中手动把系统时间往后调了 1 小时会发生什么因为我们的基准是Date.now()系统时间一变deadline - Date.now()立刻变成负数倒计时直接结束。这在某些场景下是合理的用户作弊但在另一些场景下会造成误伤。更稳的做法是用「服务端时间」做基准。页面加载时请求一次服务端时间算出本地时间与服务端的偏移量之后所有计算都用Date.now() offsetlet serverOffset 0; async function syncServerTime() { const start Date.now(); const res await fetch(/api/time); const { timestamp } await res.json(); const end Date.now(); // 粗略补偿网络往返的一半 serverOffset timestamp - (start end) / 2; } function now() { return Date.now() serverOffset; }然后把countdown.js里的Date.now()全部替换成now()。这样即使本地时间被改倒计时依然以服务端为准。验证方法调完系统时间后刷新页面倒计时应该继续正常走而不是瞬间归零。三类验证做完你可以用一个表格记录结果验证动作预期结果实际结果是否通过走到 10 秒刷新从 10 继续从 10 继续是双标签页对比两边秒数一致相差 0-1 秒是改系统时间后刷新继续正常走继续正常走是如果三类都通过这套倒计时就可以上活动页了。实测下来这套方案在秒杀场景里表现稳定用户刷新、切标签、锁屏再回来倒计时都不会乱。5. 本篇常见错误排查401、local proxy failed、reading choices、OAuth这一节把你在接入和调试过程中可能撞上的报错集中列一下对照着查。报错一401 Unauthorized。如果你在用 API 方式调模型辅助调试返回 401 通常是 Key 没带对或过期了。检查请求头是不是Authorization: Bearer sk-xxx注意Bearer和 Key 之间有一个空格。另外确认 Key 是从控制台的 API Keys 页面复制的完整串别漏字符。如果 Key 没问题还是 401去控制台看下这个 Key 是否被禁用或额度耗尽。报错二local proxy failed。这个报错一般出现在你本地配了请求转发工具但工具没启动或端口对不上。先确认你的请求工具在运行再检查 Base URL 是不是写成了https://taotoken.net/api末尾不要多加斜杠。如果你在settings.json或环境变量里配了代理地址把它临时清掉直连试一次能通就说明是转发配置的问题。报错三reading choices。这个报错是典型的「响应结构不符合预期」。你代码里写了response.choices[0]但实际返回的response里没有choices字段。原因通常是请求根本没成功返回的是一个错误对象比如{ error: {...} }你却直接去读choices。修复方法是先判断const data await res.json(); if (!res.ok) { console.error(请求失败, data.error?.message || data); return; } const content data.choices?.[0]?.message?.content;用可选链?.兜底就不会再抛reading choices了。报错四OAuth 相关。如果你在配 Claude Code 这类工具时看到 OAuth 报错通常是认证流程没走完或者 token 过期。这类工具一般需要你在浏览器里完成一次授权拿到回调后再写回本地配置。检查配置文件里的 token 字段是否为空重新走一遍授权流程即可。注意不要把 token 提交到 Git 仓库用.gitignore排除掉。配置三件套再强调一次。无论你用 CC Switch、Cline MCP 还是 Codex 的auth.json只要涉及模型接入都要写全这三样Base URLhttps://taotoken.net/apiAPI Key控制台创建的那串sk-开头的字符Model ID模型对话页面显示的标识以 Codex 的auth.json为例路径通常在~/.codex/auth.json内容形如{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-5 }Cline MCP 的配置则在settings.json里字段名可能是baseUrl、apiKey、model按工具文档填。CC Switch 类似切换配置时确认这三项都指向同一套别出现 Base URL 是 A、Key 是 B 的混搭那样必然 401。倒计时本身的排查清单。除了接入报错倒计时逻辑也有几个高频坑刷新后归零检查是不是每次进入都无条件setDeadline应该先getDeadline判断。显示数字跳变检查是不是用了Math.floor改成Math.ceil。到点后缓存没清检查remainMs 0分支里有没有clearDeadline()。多标签页不同步检查是不是各标签页各存各的应该共用同一个 key。定时器泄漏组件卸载时有没有调用返回的stop函数。把这些对照一遍基本能覆盖 90% 的倒计时问题。6. 把倒计时封装成可复用模块接入你的活动页最后说下怎么把这套东西沉淀下来。我建议把countdown-store.js和countdown.js放在项目的utils目录对外只暴露startCountdown一个函数。活动页里这样用import { startCountdown } from /utils/countdown.js; const el document.getElementById(countdown); const stop startCountdown(el, 15, () { console.log(倒计时结束可以触发抢购按钮); document.getElementById(buy-btn).disabled false; }); // 页面离开时 window.addEventListener(beforeunload, stop);如果你需要多个倒计时共存比如页面上有秒杀倒计时和优惠券倒计时给每个倒计时传不同的STORAGE_KEY即可把 key 作为参数暴露出去export function startCountdown(el, durationSec, options {}) { const { key activity_countdown_deadline, onEnd } options; // ...内部用 key 替代常量 }这样一套代码能覆盖多个业务场景。调试阶段如果想让模型帮你审查这段封装可以把完整代码贴到模型对话里让它检查边界情况如果打算长期迭代活动页用 Coding Plan 把这类工具函数的生成和审查流程固定下来每次新活动直接复用不用重写。倒计时看着简单但「刷新不重置、多标签同步、时间可校准」这三点做到位活动页的体验就稳了。核心就一句话存截止时间戳不存剩余秒数。记住这句剩下的都是细节。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →