news 2026/9/30 2:29:15

App Ideas 中的 Countdown Timer:用纯原生 JavaScript 打造事件倒计时器从需求到实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
App Ideas 中的 Countdown Timer:用纯原生 JavaScript 打造事件倒计时器从需求到实现
  • 文档
  • 教程

【免费下载链接】app-ideas

A Collection of application ideas which can be used to improve your coding skills.

项目地址:https://gitcode.com/GitHub_Trending/ap/app-ideas
点击查看免费下载

本文以 App Ideas 仓库的 Countdown Timer 项目文档 为骨架,完整讲解一个入门级(Tier 1)倒计时应用的客观目标、约束条件、7 条用户故事与 4 项 Bonus 功能,并给出基于原生 JavaScriptDate、setInterval/clearInterval与localStorage的可运行参考实现。读完你可以独立完成一个支持事件定义、自动递减、到期提醒与多事件持久化的倒计时器,并掌握浏览器时间编程的基本功。

项目定位:为什么它是 Tier 1 入门项目

在仓库根目录的 README.md 中,Countdown Timer 被列在 Tier-1 入门项目清单里,简短描述为 "Event Countdown timer"(README.md 第 51 行)。README 对 Tier-1 开发者画像的定义是:

Developers in the early stages of their learning journey. Those who are typically focused on creating user-facing applications.

这意味着该项目的价值不在于算法复杂度,而在于让你完整走一遍"需求 → 输入校验 → 定时刷新 UI → 状态清理"的用户界面应用开发闭环。它天然贴合日期时间处理、表单校验、定时器和 DOM 渲染这四个高频基础技能点,同时因其"可复制、易扩展"的属性,也常被用于教程写作与作品集积累。

值得一提的是,仓库中还有两个同属计时领域的兄弟项目可供延伸对比:Stopwatch-App.md(正计时 + 圈数)与 Pomodoro-Clock.md(工作/休息番茄钟)。倒计时器与它们共用setInterval等核心机制,但方向相反、语义不同,适合在完成本项目后串联练习。

核心目标

文档给出的目标非常明确:

Countdown Timer 的目标是:提供一种持续递减的显示,展示距用户输入事件的月(months)、天(days)、小时(hours)、分钟(minutes)和秒(seconds)。

应用场景是生日、结婚纪念日、节假日等人们期待的重要时刻。与"现在几点了"的时钟不同,倒计时器的唯一焦点是剩余时间,且必须随时间流逝不断减少,最终在事件到来时归零并触发提醒。

值得注意的一个细节是:文档的 Objective 提到了"月"这一单位,但 7 条用户故事中实际要求的显示粒度是天、小时、分钟、秒。从文档结构可以推断,"月"是目标层面的理想化表述,而用户故事是必须达成的验收标准。实现时建议两者都覆盖:以"月 + 天 + 时 + 分 + 秒"作为主显示,或至少完整实现天/时/分/秒,把月作为扩展项。

两条硬性约束

文档设置了两个约束,它们是本项目的"考试规则":

  1. 只能使用语言内置的日期时间函数进行计算,不得依赖 MomentJS 这类第三方库或包。前提是你的语言自带足够的日期时间操作能力——JavaScript 的Date对象完全满足这一要求。这条约束的目的,是逼迫你亲手处理时间戳运算、单位换算与取模借位,而不是把核心逻辑交给库。
  2. 不得使用 Counting Down To 这类代码生成器网站,必须开发自己的原创实现。这是为了保证代码的原创性与可维护性。

简而言之:这是一个"用原生 API 手搓"的项目。对 JavaScript 而言,你需要的全部武器就是Date、setInterval和clearInterval,它们也正是文档 "Useful links and resources" 一节重点指向的三样资源。

用户故事逐条拆解与实现要点

文档给出了 7 条用户故事,是实现的验收清单。下面逐条给出解读与落地建议。

1. 事件输入表单

用户可以看到一个事件输入框,包含事件名称字段、日期字段、可选时间字段和一个 "Start" 按钮。

界面结构至少需要:text类型名称输入、date类型日期输入、time类型时间输入(可选)、一个启动按钮。推荐配合required属性与 HTML5 校验提示,让浏览器先做一层基础把关。

2. 事件定义与默认时间语义

用户可以通过输入事件名称、预定日期和可选时间(仅当日)来定义事件。如果时间被省略,则假定为事件日期当地时区的午夜(Midnight)。

这是本项目最容易踩坑的一条。当用户不填时间时,事件时刻应为该日期当地时区的 00:00:00。直接new Date("2026-10-01")在多数浏览器中解析为 UTC 午夜,与本地午夜存在时差偏移,这正是文档强调"当地时区"的原因。推荐的可靠做法是分段构造:

function buildEventTime(dateStr, timeStr) { if (timeStr) { // "2026-10-01" + "18:30" → 本地时区的 18:30 return new Date(`${dateStr}T${timeStr}:00`); } // 未填时间 → 本地时区午夜 return new Date(`${dateStr}T00:00:00`); }

3. 名称为空的警告

如果事件名称为空,用户可以看到警告消息。

在点击 Start 后校验eventName.trim() === "",为空则在界面中显示错误文本并中止启动。可配合输入框aria-invalid或红色提示样式增强可访问性。

4. 日期或时间输入错误的警告

如果事件日期或时间输入错误,用户可以看到警告消息。

包括:日期为空、日期格式非法、时间格式非法。input[type="date"]/input[type="time"]在浏览器内会拦截大部分非法输入,但仍需防御空值与极端值,并在校验失败时展示统一风格的错误提示。

5. 精度溢出的警告

如果距事件的时间会溢出倒计时器的显示精度,用户可以看到警告消息。

从文档描述可以推断,这是指倒计时显示能表示的最大剩余时长有限(例如天字段只有两位、月字段只有两位,或整体限制在某个最大范围内)。当目标事件过远、剩余时间超过显示精度时,应给出明确警告而不是静默截断。实现时可以先设定精度上限常量(如MAX_MONTHS = 99),启动前用剩余时间与上限比较,超限即提示。

6. Start 启动倒计时

用户可以点击 "Start" 按钮,看到倒计时开始显示距事件的天、小时、分钟和秒。

点击 Start 后:读取表单 → 校验 → 计算targetTime - Date.now()→ 启动setInterval每秒刷新剩余时间并写入 DOM。

7. 自动递减的借位逻辑

倒计时中的各元素自动递减。例如:当剩余秒数到达 0 时,剩余分钟数减 1,秒数从 59 重新倒数。该递进必须从秒一路延续到天位。

这是倒计时的核心观感要求。若采用"每秒从剩余毫秒重算并取模"的方式(如seconds = Math.floor(diff / 1000) % 60),上述借位行为会被数学上自动满足:diff每秒减少 1000ms,秒位到 0 的下一帧自然进入分钟位减 1、秒位回 59 的状态。这种方式比"手工维护递减状态并逐位借位"更不易出错,也是推荐实现。

核心技术方案

结合文档 "Useful links and resources" 一节给出的三件核心武器,展开其在本项目中的职责:

Date:时刻的锚点

Date负责三件事:取当前时间(Date.now())、按本地时区构造目标时刻(new Date(...))、计算差值(targetTime - Date.now())。差值单位为毫秒,是一切单位换算的原材料。

setInterval:心跳

setInterval(callback, 1000)让回调每秒执行一次,每次读取最新差值并更新 UI。注意两点:一是间隔为 1000ms 时浏览器可能在后台标签页节流,导致 UI 刷新不精确;二是不要在回调里用"计数减 1"来推算时间,而应每次用Date.now()重新计算真实差值,这样即使某帧被节流,恢复后显示依然正确。

const timerId = setInterval(() => { render(getTimeRemaining(targetTime)); }, 1000);

clearInterval:及时止损

事件到达、用户重置或销毁页面时,必须clearInterval(timerId)释放定时器,避免幽灵刷新与内存浪费。事件到达时的处理是:停止刷新 → 全部归零 → 触发提醒。

时间差换算与借位

以毫秒差值为输入,逐级取模换算:

const MS = { sec: 1000, min: 1000 * 60, hour: 1000 * 60 * 60, day: 1000 * 60 * 60 * 24 }; function getTimeRemaining(targetTime) { const diff = targetTime - Date.now(); if (diff <= 0) { return { months: 0, days: 0, hours: 0, minutes: 0, seconds: 0, total: 0 }; } return { total: diff, months: Math.floor(diff / (MS.day * 30.44)), // 以平均月长近似,可按需求取舍 days: Math.floor(diff / MS.day), hours: Math.floor((diff / MS.hour) % 24), minutes: Math.floor((diff / MS.min) % 60), seconds: Math.floor((diff / MS.sec) % 60), }; }

这里% 60、% 24的取模天然实现"秒满 60 进分、分满 60 进时、时满 24 进天"的借位,即用户故事 7 所要求的递减行为。

参考实现:一个符合全部约束的单页应用

以下实现只使用浏览器内置 API(Date、setInterval/clearInterval、localStorage、原生 DOM),完全满足文档的"无第三方库、原创实现"约束。核心结构分为三部分。

HTML 骨架

<main> <h1>Countdown Timer</h1> <form id="event-form"> <label>事件名称 <input id="name" type="text" placeholder="例如:新年倒计时" /> </label> <label>日期 <input id="date" type="date" required /> </label> <label>时间(可选,缺省为当地午夜) <input id="time" type="time" /> </label> <button type="submit">Start</button> </form> <p id="warning" role="alert"></p> <section id="countdown"> <div><span id="days">0</span> 天</div> <div><span id="hours">0</span> 小时</div> <div><span id="minutes">0</span> 分钟</div> <div><span id="seconds">0</span> 秒</div> </section> <ul id="event-list"></ul> </main>

JavaScript 核心逻辑

const MAX_MONTHS = 99; // 显示精度上限,超出即视为溢出 let timerId = null; // 1) 构造目标时刻:未填时间 → 本地午夜(用户故事 2) function buildEventTime(dateStr, timeStr) { return timeStr ? new Date(`${dateStr}T${timeStr}:00`) : new Date(`${dateStr}T00:00:00`); } // 2) 表单校验(用户故事 3、4、5) function validate(name, targetTime) { if (!name.trim()) return "事件名称不能为空。"; // 故事 3 if (isNaN(targetTime.getTime())) return "日期或时间格式不正确。"; // 故事 4 if (targetTime <= new Date()) return "事件时间必须晚于当前时间。"; const months = Math.floor((targetTime - Date.now()) / (MS.day * 30.44)); if (months > MAX_MONTHS) return "剩余时间超出倒计时显示精度。"; // 故事 5 return ""; } // 3) 每秒刷新(用户故事 6、7) function startCountdown(targetTime) { stopCountdown(); timerId = setInterval(() => { const r = getTimeRemaining(targetTime); document.getElementById("days").textContent = r.days; document.getElementById("hours").textContent = r.hours; document.getElementById("minutes").textContent = r.minutes; document.getElementById("seconds").textContent = r.seconds; if (r.total <= 0) { stopCountdown(); alert("事件已到达!"); // Bonus:到期提醒 } }, 1000); } function stopCountdown() { if (timerId) { clearInterval(timerId); timerId = null; } }

事件绑定与持久化

document.getElementById("event-form").addEventListener("submit", (e) => { e.preventDefault(); const name = document.getElementById("name").value; const date = document.getElementById("date").value; const time = document.getElementById("time").value; const targetTime = buildEventTime(date, time); const warning = validate(name, targetTime); document.getElementById("warning").textContent = warning; if (warning) return; // 校验失败不启动 const event = { id: Date.now(), name, targetTime: targetTime.getTime() }; saveEvent(event); // Bonus:跨会话持久化 startCountdown(event.targetTime); });

其中saveEvent利用localStorage实现 Bonus 中的"跨会话持久化":

function saveEvent(event) { const events = JSON.parse(localStorage.getItem("countdown-events") || "[]"); events.push(event); localStorage.setItem("countdown-events", JSON.stringify(events)); }

页面加载时从localStorage读取事件数组并逐个渲染倒计时,即可同时满足 Bonus 中的"支持多个事件"与"为每个事件显示倒计时"两条。数据模型使用数组 + 唯一id,删除事件时可依据id过滤后回写。

运行方式

以上三部分合并为一个index.html即可直接运行,无需构建工具或安装依赖。在浏览器中打开该文件,输入名称、日期与可选时间,点击 Start 即可看到天/时/分/秒逐秒递减;将事件时间设为 1~2 分钟后即可快速验证"秒到 0 分钟减 1、秒从 59 重来"的借位行为与到期提醒。

边界场景与异常处理清单

场景处理策略对应用户故事
名称为空显示警告,不启动故事 3
日期/时间缺失或非法显示警告,不启动故事 4
事件时间早于当前时间显示"必须晚于当前"警告故事 4 的延伸防御
剩余时间超过显示精度(如超过 99 个月)显示溢出警告故事 5
到达事件时刻停止定时器、归零、提醒故事 7 + Bonus
刷新/关闭页面从 localStorage 恢复事件并重建倒计时Bonus

测试与验证思路

由于本项目是纯前端应用,验证重点放在时间逻辑上:

  • 等价类测试:分别构造"事件在 N 秒后""事件恰好在整点""事件已过期"三类输入,检查显示值是否符合预期。
  • 借位测试:把目标时间设为"当前时间 + 61 秒",观察 1 分钟 01 秒 → 59 秒的跨位递减过程,验证用户故事 7。
  • 午夜缺省测试:只填日期不填时间,断言targetTime.getHours() === 0且getMinutes() === 0,验证本地时区午夜语义(用户故事 2)。
  • 持久化测试:定义事件后刷新页面,确认事件列表与倒计时被正确恢复。

延伸:仓库中的同类练习

完成本项目后,可以回到 README.md 的项目索引中,把计时主题做成一组系列练习:

  • Stopwatch App:正计时方向,练习start/stop/restart状态机与圈数记录;
  • Pomodoro Clock:固定时长的工作/休息切换,练习多阶段计时与状态流转;
  • Calendar App(仓库列表中亦可见):若想把事件与日历视图结合,可继续挑战。

三个项目共享Date与setInterval的底层机制,但分别训练"倒计时、正计时、阶段切换"三种不同语义,是性价比很高的组合练习路径。

小结

Countdown Timer 是一个信息量恰到好处的入门项目:它的约束迫使你独立实现时间计算,它的 7 条用户故事覆盖了表单、校验、定时刷新、借位递减与清理,它的 Bonus 功能则把练习延伸到持久化与多事件管理。本文给出的参考实现严格基于原生Date、setInterval/clearInterval与localStorage,完整对应文档中的每一项验收标准。建议你以本文代码为基线,再自行扩展"月"单位显示、事件删除、到期音效等能力,形成你自己的原创版本。

  • 文档
  • 教程

【免费下载链接】app-ideas

A Collection of application ideas which can be used to improve your coding skills.

项目地址:https://gitcode.com/GitHub_Trending/ap/app-ideas
点击查看免费下载
上一篇:M3u8Downloader_H 批量下载实战:几十个视频一次跑完
下一篇:BepInEx 6.0 IL2CPP 排障指南:Unity 插件框架启动报错的 3 个常见问题,10 分钟查明白

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 2:29:15

Puter.js:零 API Key,1 行 JS 接入 500+ AI 模型与云存储

Puter.js&#xff1a;零 API Key&#xff0c;1 行 JS 接入 500 AI 模型与云存储 【免费下载链接】puter &#x1f310; The Internet Computer! Free, Open-Source, and Self-Hostable. 项目地址: https://gitcode.com/GitHub_Trending/pu/puter Puter.js 是一款免费、开…

作者头像 李华
网站建设 2026/9/30 2:28:43

springbootAI智能法律援助小程序16783-计算机课程设计、毕业设计

前言 ✨ 博主介绍&#xff1a;一线全栈工程师&#xff0c;毕设实战引路人。技术栈覆盖Java、Python、C#、PHP、Node.js及UniApp跨端开发&#xff0c;擅长多语言项目落地与架构设计。持续分享毕设源码、开题报告、技术选型心得与职场踩坑经验。用工程化思维写代码&#xff0c;帮…

作者头像 李华
网站建设 2026/9/30 2:28:28

Buzz 本地语音转文字:离线转写一条录音并导出可用字幕

Buzz 本地语音转文字&#xff1a;离线转写一条录音并导出可用字幕 【免费下载链接】buzz Buzz transcribes and translates audio offline on your personal computer. Powered by OpenAIs Whisper. 项目地址: https://gitcode.com/GitHub_Trending/buz/buzz Buzz 是一款…

作者头像 李华
网站建设 2026/9/30 2:28:06

大麦网抢票脚本:Python 自动购票的完整实战指南

大麦网抢票脚本&#xff1a;Python 自动购票的完整实战指南 【免费下载链接】Automatic_ticket_purchase 大麦网抢票脚本 项目地址: https://gitcode.com/GitHub_Trending/au/Automatic_ticket_purchase 热门演唱会的票放出来也就几秒&#xff0c;等你手动刷新、点选、确…

作者头像 李华
网站建设 2026/9/30 2:26:07

GPIO

这次小编带来的是一些嵌入式基础理论概念&#xff0c;若有不正确的地方&#xff0c;欢迎一起探讨。1.GPIO的基础知识1.1GPIO的基本概念1.1.1 GPIO是什么GPIO即General Purpose Input/output&#xff0c;通用输入输出出口。是MCU的一个 内部结构&#xff0c;用来做输入输出的控制…

作者头像 李华
网站建设 2026/9/30 2:24:35

如何参与HBase开源贡献:从能编译的源码到第一个PR的4个实操步骤

如何参与HBase开源贡献&#xff1a;从能编译的源码到第一个PR的4个实操步骤 【免费下载链接】hbase Apache HBase 项目地址: https://gitcode.com/GitHub_Trending/hb/hbase Apache HBase是构建在Hadoop与HDFS之上的高可靠、高性能面向列的分布式存储系统&#xff0c;用…

作者头像 李华