番茄钟计时器
25 分钟专注 + 5 分钟休息的循环。setInterval 驱动倒计时,localStorage 把今日完成个数和设置都留在本地,关掉浏览器再开也能接着数。
把专注和休息交给一个会自己记得的小钟
这是我自己写完之后用得最多的一个项目——每天开两个番茄钟写代码,比任何 to-do 应用都管用。技术上它把之前几个案例的能力揉在一起:定时器、状态机、本地存储、声音提示。难点不在某个 API,而在于让「切到后台」「关闭页面」「中途暂停」这些边界都不要把数据搞乱。
分五步做一个能用的番茄钟
计时器易写难用,难就难在「时间准不准」和「关了再开能不能恢复」。
状态与常量
定三个常量:WORK = 25 * 60、BREAK = 5 * 60、LONG_BREAK = 15 * 60。状态变量:mode('work' / 'break')、remaining(剩余秒)、isRunning、completedCount(今日完成数)。开始 / 暂停 / 重置三个按钮。
用时间戳避免计时漂移
别用 setInterval(() => remaining--, 1000)——定时器不精确,长时间运行会偏差几秒。正确做法:开始时记 endTime = Date.now() + remaining * 1000,setInterval 里只负责每秒渲染 remaining = Math.round((endTime - Date.now()) / 1000)。这样即使浏览器卡顿几秒,时间也始终准确。
阶段切换与提示音
当 remaining <= 0:如果当前是 work,completedCount++,切到 break;如果是 break,切回 work;每 4 个 work 后切到 long_break。用 new Audio('bell.mp3').play() 播一声提示。同样要存进 localStorage 防丢失。
localStorage 持久化
每秒把 { mode, remaining, endTime, isRunning, completedCount, date } 序列化存进 localStorage。页面加载时先读,如果 date 不是今天就把 completedCount 重置为 0。这样关浏览器再开、刷新页面都能恢复到离开前的状态。
暂停、恢复与重置
暂停:clearInterval + 把 remaining 写成当前剩余(基于 endTime 重算)。恢复:重新计算 endTime = Date.now() + remaining * 1000 再起 interval。重置:remaining 恢复到当前 mode 的初始时长,isRunning 置 false。这三步逻辑写顺,番茄钟就经得起日常使用。
关键代码片段
计时器主循环 + 持久化恢复,两段是这个项目的灵魂。
const WORK = 25 * 60, BREAK = 5 * 60; let state = { mode: 'work', remaining: WORK, endTime: null, isRunning: false, completed: 0 }; let timer = null; function tick() { state.remaining = Math.max(0, Math.round((state.endTime - Date.now()) / 1000)); render(); if (state.remaining === 0) switchMode(); save(); } function start() { if (state.isRunning) return; state.isRunning = true; state.endTime = Date.now() + state.remaining * 1000; timer = setInterval(tick, 1000); save(); } function pause() { state.isRunning = false; clearInterval(timer); state.remaining = Math.round((state.endTime - Date.now()) / 1000); save(); } function switchMode() { if (state.mode === 'work') { state.completed++; state.mode = (state.completed % 4 === 0) ? 'long_break' : 'break'; } else { state.mode = 'work'; } state.remaining = state.mode === 'work' ? WORK : BREAK; new Audio('bell.mp3').play(); } // 页面加载时恢复 const saved = JSON.parse(localStorage.getItem('pomodoro') || 'null'); if (saved) { const isToday = saved.date === new Date().toDateString(); state = { ...saved, completed: isToday ? saved.completed : 0 }; if (state.isRunning) { state.isRunning = false; start(); } }
定时器是前端最容易写错的地方:你以为每秒减一就准了,其实后台标签会被浏览器限流、定时器本身也有几毫秒漂移。这个项目让我第一次学会「不要相信时间间隔,要相信时间戳」——所有计时器都应该基于 Date.now() 推算,setInterval 只是渲染的节拍器,不是真相来源。
继续下一个案例
Web 端的项目到此告一段落,最后一个案例我们去终端里,用 Python 写一个命令行待办工具。