资讯动态

手写番茄钟:从纯前端实现到注意力管理的深度定制

发布时间:2026/9/15 22:57:43 来源:尧图企业网站定制
我手机里躺过十来个番茄钟App从极简风到游戏化养成换了一茬又一茬。最后真正留在日常里的却是一个花了一个晚饭时间写的网页版番茄钟。这个决定听起来有点返祖但用下来的感受是工具一旦是自己亲手写的你对它的理解和使用深度跟下载一个现成品完全不在一个量级。这篇文章就聊聊我是怎么把这个念头落地的以及手写一个番茄钟背后真正值得关注的东西。这篇文章适合两类人。一类是跟我一样被“时间碎片化”折磨的上班族、自由职业者和学生想用一个简单可靠的工具重新拿回注意力另一类是刚学前端不久、想找个练手项目的新手番茄钟是个非常完美的练习场景——它不算复杂但状态切换、定时器、本地存储、浏览器通知这些基本功全都覆盖到了。你不需要很强的编程基础跟着思路走一个能天天用的东西很快就能跑起来。1. 为什么市面上有那么多番茄钟我还是要手写一个1.1 先说说我和“时间碎片”的拉锯战我以前的工作状态很典型电脑上开着七八个标签页微信上几个群同时在跳手里的任务经常做到一半就被拖走。一顿忙到下班回头一想正经事没推进多少全是零碎响应。后来读到番茄工作法的书核心原则就两条一次只做一件事做25分钟休息5分钟。原理听着特别简单但我很快发现真正的问题不是不懂原理而是没有一个工具能帮我守住边界。下载过的番茄钟App其实都不差有漂亮的动效有数据曲线还能跟别人PK专注时长。但用一段时间后问题开始浮现有些App的通知来得太温柔我连看都不看一眼有些把专注变成了社交打卡反而增加了负担更麻烦的是很多工具把流程定死了我想调整一下工作时间、休息时长得去翻设置里藏得很深的选项甚至有的一开始就不让改。1.2 现有工具的核心痛点以及手写方案能换来什么我扒了扒自己的使用习惯总结了现成工具让我放弃的四个痛点参数不可调或调整成本高我的注意力周期其实不是标准的25分钟不同时段、不同任务类型需要的长度差很多但大多数工具把255当成了唯一的真理。通知与打断处理太弱番茄钟的核心价值是“保护一段完整时间”但如果提醒方式不够强势很容易被忽略。数据只存云端离线不可用很多App必须先登录才能用明明是个本地就能解决的问题非要引入账号体系。花里胡哨的功能反而制造了新负担有的工具带积分、排行、宠物养成专注本身反而成了次要的事。自己动手写最大的好处是“工具就是你注意力的镜子”。你想要什么样的提醒强度、什么样的节奏、什么样的数据记录自己去代码里改。你今天觉得自己需要更长的深度工作时间改一个数字就能实现。这种灵活度是现成工具给不了的。2. 番茄钟不是“25分钟倒计时”那么简单2.1 番茄工作法的真正重点打断管理而不是计时很多人的误解在于番茄钟就是一个每25分钟响一次的闹钟。其实不是它的核心是一个打断管理系统。你的注意力之所以碎片化不是因为你不会集中而是因为打断太多——外部的消息、自己脑子里的杂念、突如其来的临时任务。番茄钟做的事是帮你做了一次“决策缓存”。当你在一个番茄周期内突然想起要回复邮件或者取快递正确的动作不是立刻去做而是把它写进一张“待处理清单”等休息时间再统一处理。这样一来你没有被杂念牵着走但也没有真的忘记要做的事。这个“短暂的延迟响应”机制才是番茄钟真正值钱的地方。所以我写的工具里专门留了一个随时可以记录的输入框这个设计比计时本身重要得多。2.2 为什么默认是25分钟注意力周期的观察与调整逻辑25分钟这个数字来自弗朗西斯科·西里洛在20世纪80年代的观察当时他用的还是厨房定时器。这个长度的合理性在于它足够长让你能进入一个有意义的工作状态又足够短即使被打断损失也在可接受范围内。从认知心理学角度看人的持续注意力维持在一个相对高水平的时间是有极限的保持专注20到40分钟后大脑需要一点“离线时间”来整理信息。但这不意味着25分钟对每个人都适用。我自己实测下来下午三点左右状态好的时候能轻松续满40分钟但上午刚睡醒那会儿15分钟都费劲。所以我的工具里工作和休息时长都是可配置的甚至可以按“上午/下午/晚上”设置不同的参数。这个想法也是手写工具后慢慢琢磨出来的——先用标准25分钟建立习惯再根据自己的真实感受去调节而不是强行让自己适配一个数字。2.3 休息时间别刷手机拉开“注意力距离”番茄钟的另一个关键点是休息时间的使用方式。很多人35分钟定了5分钟休息一到就刷短视频结果注意力被新的刺激吸走下一轮根本进入不了状态。我自己的体会是休息时要做的事不是“获取信息”而是“放松注意力”比如站起来倒杯水、看看窗外、做几个拉伸动作。这里有个概念叫“注意力距离”意思是你要离刚才工作的内容越远越好。如果你刚写完代码又刷技术博客那等于没休息。所以我的番茄钟在休息阶段不会弹出什么励志语录就一个简单的全屏倒计时配一句“站起来离开屏幕”提醒我物理上离远一点。加了这句之后我下午的工作效率明显高了一截。3. 手写一个番茄钟从功能清单到核心实现3.1 技术选型为什么只用纯前端加本地存储一开始我也想过要不要上个框架、配个后端把数据同步到云端。但冷静下来想了想需求就放弃了。我需要的场景非常明确一台电脑一个浏览器标签页点击开始知道什么时候结束。没有任何跨设备、多用户协作的需求那引入框架和服务器的复杂度就是纯粹的负担。最终方案就是一份HTML文件加上JavaScript和少量CSS数据用localStorage存在本地。不用安装依赖不用构建工具双击就能运行。这个决定的性价比极高因为整个项目只有一个文件维护成本几乎为零改起来也不怕改崩什么。如果以后真需要云端同步再把数据层抽出来也不迟前期完全没必要过度设计。3.2 功能设计先想清楚“该替你做哪些决定”动手写代码之前我先把需求写成了故事不是功能列表而是使用场景。早上到了工位打开这个网页先在任务框里写下今天要攻克的一件事然后点开始。工作期间屏幕上显示剩余时间网页标题也同步显示倒计时就算切到别的窗口也能瞥见。时间到了浏览器会弹出一条系统通知同时播放一个短促的声音提醒我停手。休息结束后再进入下一轮。功能上我最终保留了这几项开始/暂停/重置、任务备注、每日完成统计、自定义时长、浏览器通知、标题栏倒计时、休息提醒。砍掉的东西包括排行榜、连续打卡天数、社交分享、复杂的数据报表。砍掉的这些不是因为做不出来而是因为它们是上个工具让我分心的原因之一。一个番茄钟界面上的每一个元素都应该服务于“帮你守住一个时间段”而不是让你在工具本身身上花更多时间。3.3 核心代码拆解状态、计时与通知的实现逻辑下面是我整理后的核心实现代码不长但每部分都有它可以说道的地方。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title我的番茄钟/title style body { font-family: -apple-system, PingFang SC, sans-serif; max-width: 480px; margin: 40px auto; text-align: center; background: #f7f7f5; color: #333; } .time { font-size: 96px; font-weight: 700; margin: 24px 0; font-variant-numeric: tabular-nums; } .status { font-size: 18px; color: #888; } button { font-size: 18px; padding: 10px 24px; margin: 8px; border: none; border-radius: 8px; background: #2d8cf0; color: #fff; cursor: pointer; } button.secondary { background: #aaa; } input { font-size: 16px; padding: 8px; width: 80%; margin: 12px auto; display: block; box-sizing: border-box; border: 1px solid #ddd; border-radius: 6px; } /style /head body h1番茄钟/h1 p classstatus idstatus准备开始/p div classtime idtimer25:00/div input idtask placeholder这一轮想完成什么 / button idstartBtn开始/button button idresetBtn classsecondary重置/button p idtodayCount今日完成0 个番茄/p script // 可调参数 const CONFIG { work: 25 * 60, // 工作时长秒 shortBreak: 5 * 60, // 短休息时长秒 longBreak: 15 * 60, // 长休息时长秒 longBreakEvery: 4, // 每几个番茄后进入长休息 }; // 运行时状态 let state idle; // idle | work | shortBreak | longBreak let endTime null; // 本轮结束的时间戳 let timerId null; let pomodoroCount 0; // 本轮连续的番茄数用于判断长休息 let todayDone 0; // 今日完成总数 // 工具函数 function formatTime(seconds) { const m String(Math.floor(seconds / 60)).padStart(2, 0); const s String(seconds % 60).padStart(2, 0); return m : s; } function getDuration() { if (state work) return CONFIG.work; if (state shortBreak) return CONFIG.shortBreak; if (state longBreak) return CONFIG.longBreak; return CONFIG.work; } function nextState() { if (state work) { pomodoroCount; todayDone; localStorage.setItem(pomodoroToday, String(todayDone)); if (pomodoroCount % CONFIG.longBreakEvery 0) return longBreak; return shortBreak; } return work; } function notify(title, body) { if (Notification in window Notification.permission granted) { new Notification(title, { body }); } } function playBeep() { try { const ctx new (window.AudioContext || window.webkitAudioContext)(); const osc ctx.createOscillator(); const gain ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type sine; osc.frequency.value 880; gain.gain.setValueAtTime(0.15, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime 0.8); osc.start(); osc.stop(ctx.currentTime 0.8); } catch (e) { // 某些浏览器没有Web Audio支持忽略声音 } } function render() { let remaining 0; if (endTime) { remaining Math.max(0, Math.round((endTime - Date.now()) / 1000)); } document.getElementById(timer).textContent formatTime(remaining); const statusText state work ? 专注中 : (state idle ? 准备开始 : 休息中); document.getElementById(status).textContent statusText; document.getElementById(todayCount).textContent 今日完成 todayDone 个番茄; document.title state idle ? 番茄钟 : (statusText formatTime(remaining)); } // 核心逻辑 function startTimer() { if (timerId) return; if (state idle) { state work; } const duration getDuration(); endTime Date.now() duration * 1000; // 关键基于时间戳而非计数器 timerId setInterval(tick, 250); // 每250ms刷新一次界面 render(); } function pauseTimer() { if (timerId) { clearInterval(timerId); timerId null; const remaining Math.max(0, Math.round((endTime - Date.now()) / 1000)); endTime Date.now() remaining * 1000; } } function resetTimer() { clearInterval(timerId); timerId null; state idle; endTime null; render(); } function tick() { const remaining Math.round((endTime - Date.now()) / 1000); if (remaining 0) { finishPhase(); } else { render(); } } function finishPhase() { clearInterval(timerId); timerId null; const prevState state; if (prevState work) { notify(番茄完成, 干得不错起来活动一下吧); playBeep(); } else { notify(休息结束, 准备好了就继续开始下一轮); playBeep(); } state nextState(); endTime null; render(); // 自动进入下一阶段也可以改成手动开始 startTimer(); } // 初始化 document.getElementById(startBtn).addEventListener(click, function() { if (timerId) { pauseTimer(); this.textContent 继续; } else { if (this.textContent 继续) { startTimer(); this.textContent 暂停; } else { if (state idle) { const task document.getElementById(task).value.trim(); if (task) { // 把任务写入本地日志方便复盘 const log JSON.parse(localStorage.getItem(pomodoroLog) || []); log.push({ time: new Date().toISOString(), task: task }); localStorage.setItem(pomodoroLog, JSON.stringify(log)); } } startTimer(); this.textContent 暂停; } } }); document.getElementById(resetBtn).addEventListener(click, function() { resetTimer(); document.getElementById(startBtn).textContent 开始; }); // 读取今天的完成数 (function init() { const todayKey new Date().toDateString(); const saved localStorage.getItem(pomodoroToday); if (saved localStorage.getItem(pomodoroDay) todayKey) { todayDone parseInt(saved, 10); } else { localStorage.setItem(pomodoroToday, 0); localStorage.setItem(pomodoroDay, todayKey); } render(); })(); /script /body /html这里有几个关键实现点值得单独拿出来敲黑板为什么基于时间戳而不是setInterval累加如果每秒执行一次remaining--浏览器因为卡顿或切换到后台导致定时器被节流时倒计时就会变慢。而用Date.now()计算结束时间与当前时间的差值无论界面刷新频率怎么变倒计时的时间都是准确的。这是计时类应用最核心的一个坑很多人写定时器都是挂在计数器累加上。刷新界面间隔设250ms而不是1s这样在页面从后台切回来时标题栏和显示区能较快地反映真实剩余时间不会出现“切回来发现已经超时几十秒”的尴尬。Notification需要用户授权第一次打开页面的浏览器会询问是否允许通知点了允许之后定时结束才能弹系统通知。如果拒绝我的代码里保留了一个降级方案把剩余时间写进document.title只要你开了其他标签页也能瞥见倒计时。localStorage用法上的一个小坑我最初把“今日完成数”只存了一个数字第二天打开就发现归零了。后来改成同时存一个日期字符串读取时先比对日期不是今天就直接清零是今天才恢复原来的计数。4. 实操中的调优让番茄钟从“能用”变成“好用”4.1 我踩过的坑浏览器后台休眠、通知失效、计时漂移工具跑起来之后真正放到日常工作流里用问题才开始暴露。第一个大坑是浏览器对后台标签页的节流策略。Chrome为了省电会把后台页面里的定时器降到每秒甚至更长才执行一次我的setInterval直接被拖慢。但因为计时是时间戳方案倒计时本身依然准确只是界面刷新会延迟。这也从侧面验证了用时间戳是正确的选择。第二个坑是系统通知不可靠。我一开始只调用了NotificationAPI结果发现浏览器收到提醒但如果当时弹窗焦点在别的应用上就很容易错过。后来我给通知加了一个配套动作同时播放提示音。Web Audio API直接生成一个短促的“叮”声不依赖任何音频文件。别小看这个声音它配合弹窗成功率比单纯弹窗高很多。第三个坑比较隐蔽休息阶段结束时如果人不在电脑前自动进入下一轮会浪费时间的保护机制。我最初版本是自动开始的后来发现休息时站起来喝水、跟同事说两句话回来发现下一轮已经跑掉好几分钟了。所以我改成了“自动进入下一状态但如果是休息结束需要手动点开始才进入工作”。这个微调让工具从“机械闹钟”变成了“可控节拍器”。4.2 参数调优不是所有人都适合255用了一个月之后我开始调整参数。我个人的注意力特点上午精神好但容易急躁适合20分钟一个单元快速见效下午状态平稳可以撑到35分钟再休息晚上的碎片时间就用15分钟的小单元来“见缝插针”。所以我把配置改成了分时段的参数放在配置文件里需要的话直接改数值就行。调整的过程其实是在做自我观察。你会发现某个固定时长会让自己频繁看表另一个时长则能自然忘记时间这就是你的注意力周期。标准25分钟只是起点不是终点。我周围有人用5010模式也有人用307都很好用。关键是找到适合自己的节拍而不是迷信某一个“科学数字”。4.3 结合任务的番茄安排估算、拆解、复盘番茄钟真正发挥威力是在跟任务管理结合之后。我现在的用法是每天晚上花五分钟把第二天的任务列出来每个任务后面标一个预计番茄数。目标是“三到四个番茄”的任务说明任务太大要拆成更小的子任务预计半个番茄的任务就并到相近的任务里去。这个估算做多了你对自己做事速度的判断会越来越准。复盘也一样重要。一天结束后我会看一眼今日完成的番茄数以及每个任务实际花了多少番茄跟昨晚上估的对比找出偏差大的原因。是任务理解错了还是中途被打断了还是自己状态不好这种记录和复盘的价值胜过任何先进的时间管理理论。我的日志就存在localStorage里一个pomodoroLog数组每条记录带着任务名和时间戳想复盘时自己写几行代码就能统计。5. 常见问题速查与避坑记录用了一段时间我把遇到过的典型问题和排查思路整理成了速查表给你做个参考。现象可能原因解决办法倒计时偶尔偏慢浏览器后台节流导致界面刷新延迟时间戳方案继续保留接受界面延迟工作时打开该标签页即可系统通知不弹Notification权限未授权点击浏览器地址栏左侧图标手动允许通知通知弹了但没注意缺少声音或声音太小加入Web Audio提示音或配合系统的“勿扰模式”关闭休息回来发现浪费了时间休息结束自动开启了下一轮改成休息结束后需手动点击“开始”计步的数字第二天归零localStorage跨天逻辑没写日期判断像初始化代码里那样同时存日期和数字手机浏览器上计时不稳移动端浏览器对后台标签页限制更严保证屏幕常亮或使用桌面端或配合省电策略适当调整再补充几个避开过的操作禁忌不要在工作时间内干“顺便看一眼”的事看一眼邮件、刷一下朋友圈都不是真正的休息它们会打破你建立起来的沉浸状态。把这些事写进待处理清单等休息时间统一处理。不要频繁切换工具一开始写这个番茄钟的时候我同时还在测另外两个App结果统计到处分散反而更乱。选定一个主工具至少坚持一周再评估效果。不要忽略了前一天的任务规划番茄钟只是个闹钟它不能告诉你该做什么。没有任务清单的番茄钟容易变成“为了凑番茄而工时”的自我感动。6. 从工具到习惯最后再分享一点我的实际体会现在这个番茄钟我已经用了好几个月代码其实没怎么大改过更多是在使用方式上不断调整。我慢慢发现手写一个小工具带来的不只是功能上的适配更重要的是每一次调整代码都是一次对自己注意力策略的反思。今天想把工作时间改成30分钟说明我意识到自己的节奏变了明天想把通知声音调小说明我在优化提醒的侵入感。这种深度定制是任何闭源App都给不了的体验。如果你也想试着自己写一个我给你的建议很直接先别追求功能多先做一个能响能停的倒计时用上几天把不舒服的地方记下来再一个一个去改。这个“使用—发现问题—修改—再使用”的循环本身就是一场和注意力的深度对话。到最后你会发现真正难的不是那25分钟的倒计时而是每天决定把这25分钟用来做什么。工具只是守住了边界方向从来都在你自己手里。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价