现代番茄时钟
从零到一:用原生 Web 技术构建现代番茄时钟
一款面向工作人群的专注计时工具,从需求分析到上架华为云开发者作品展览馆的完整开发纪实。
前言
番茄工作法(Pomodoro Technique)由 Francesco Cirillo 在 1987 年提出,核心简单:25 分钟专注 + 5 分钟休息,循环往复。这个方法至今仍是无数知识工作者对抗拖延、保持专注的利器。
本文记录了「番茄时钟 · Pomodoro Timer」应用从需求定义、架构设计、编码实现到最终发布上架的全过程。没有花哨的框架,只有 HTML + CSS + JavaScript 三件套——有时候,最朴素的技术栈反而能交付最纯粹的产品体验。
一、需求分析
目标用户
工作人群——程序员、设计师、产品经理、文案写作者……他们的共同特征是:
- 需要长时间保持专注,但容易被消息、邮件打断
- 对工具的 UI 品质有要求,拒绝粗糙的界面
- 希望工具轻量、即开即用,不想安装额外软件
核心功能矩阵
| 功能 | 优先级 | 说明 |
|---|---|---|
| 25 分钟专注倒计时 | P0 | 番茄工作法的基础单元 |
| 5 分钟短休息 / 15 分钟长休息 | P0 | 每 4 个番茄后自动进入长休息 |
| 开始 / 暂停 / 重置 / 跳过 | P0 | 完整的计时控制 |
| 圆形进度环 | P0 | 视觉化倒计时,一眼可知剩余时间 |
| 任务标签 | P1 | 标注当前在做什么,便于回顾 |
| 会话统计 | P1 | 今日完成数、专注总时长、当前轮次 |
| 历史记录 | P1 | 记录每次完成的番茄钟 |
| 声音 + 浏览器通知 | P1 | 番茄完成时提醒切换 |
| 深色模式 | P1 | 夜间工作者必备 |
| 自定义时长 | P2 | 不同人有不同的专注节奏 |
| 键盘快捷键 | P2 | Power user 的效率需求 |
设计风格
现代极简——这是定调。具体拆解为:
- 卡片式布局,柔和阴影,圆角设计
- 颜色语义化:工作用番茄红、短休息用青绿、长休息用蓝色
- 流畅的 CSS 过渡动画,不突兀也不拖沓
- 响应式设计,桌面和移动端皆可用
二、技术选型
为什么不用框架?
这是一个单页应用,交互逻辑集中在计时器状态机上。引入 React/Vue 意味着:
- 构建工具链(Vite/Webpack)的配置成本
- 框架运行时的体积开销
- 对一个 3 文件的项目来说,框架的收益趋近于零
原生 JS 的 IIFE 模式 + DOM 直接操作,代码量约 400 行,完全可控。
最终技术栈
HTML — 语义化结构
CSS — CSS 变量 + 媒体查询,主题切换零 JS 重绘
JavaScript — IIFE 封装,状态机模式
Node.js — 仅用于静态文件服务器(40 行)
文件结构
pomodoro-timer/
├── index.html # 页面结构(157 行)
├── style.css # 现代样式 + 深色模式(573 行)
├── app.js # 核心逻辑 + 状态机(427 行)
├── server.js # 静态文件服务器(40 行)
├── package.json # 项目配置
└── README.md # 项目文档
三、架构设计
状态机模型
整个应用的核心是一个三态有限状态机:
┌──────────────────────────────┐
│ │
┌────▼────┐ 完成 ┌──────────┐ │
│ WORK │──────────▶│ SHORT │ │
│ (25min) │ │ BREAK │ │
└─────────┘ │ (5min) │ │
▲ └──────────┘ │
│ │ │
│ 完成 │ 完成 │
│ ▼ │
│ ┌──────────┐ │
│ │ LONG │ │
│ 完成 │ BREAK │ │
└────────────────│ (15min) │ │
└──────────┘ │
(每 4 个番茄触发)│
状态对象
const state = {
mode: 'work', // 'work' | 'shortBreak' | 'longBreak'
isRunning: false, // 是否正在倒计时
timeLeft: 25 * 60, // 剩余秒数
totalTime: 25 * 60, // 当前会话总秒数
pomodoroCount: 0, // 今日完成番茄数
focusMinutes: 0, // 今日专注总分钟数
cyclePosition: 0, // 当前轮次位置 (0~N)
history: [], // 历史记录数组
settings: { ... }, // 用户自定义设置
intervalId: null, // setInterval 句柄
};
设计要点:
- 单一状态源:所有 UI 更新都从
state派生,避免多源同步问题 - 模式切换即状态重置:
setMode()同时重置timeLeft和totalTime,保证一致性 - 轮次追踪:
cyclePosition从 0 递增到longBreakInterval,达到阈值后归零并进入长休息
四、核心实现
4.1 圆形进度环
用 SVG 的 stroke-dasharray + stroke-dashoffset 实现进度动画:
const RING_RADIUS = 130;
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS; // ~816.4
function updateDisplay() {
const progress = state.totalTime > 0
? state.timeLeft / state.totalTime : 0;
const offset = RING_CIRCUMFERENCE * (1 - progress);
el.ringProgress.style.strokeDashoffset = offset;
}
原理:stroke-dasharray 设为圆周长(816.4),stroke-dashoffset 从 0(满圆)到 816.4(空圆)线性变化。CSS transition: stroke-dashoffset 1s linear 让每秒的进度变化丝滑过渡。
SVG 整体旋转 -90deg,让进度从顶部 12 点钟方向开始,符合直觉。
4.2 模式切换与颜色系统
三种模式各有专属配色,通过 CSS 变量动态切换:
:root {
--work-color: #e8553e; /* 番茄红 */
--short-color: #2db89a; /* 青绿 */
--long-color: #5b8def; /* 海蓝 */
--accent: var(--work-color); /* 当前模式色 */
}
function updateModeColors() {
const config = MODE_CONFIG[state.mode];
document.documentElement.style
.setProperty('--accent', `var(${config.color})`);
}
切换模式时,所有引用 --accent 的元素(按钮、进度环、徽章、统计数字)同步变色,零 JS 重绘,纯 CSS 过渡。
4.3 深色模式
通过 data-theme="dark" 属性 + CSS 变量覆盖实现:
[data-theme="dark"] {
--bg: #1a1a2e;
--surface: #232342;
--text: #e8e8f0;
/* ... */
}
主题偏好存储在 localStorage,刷新页面后自动恢复。切换按钮的太阳/月亮图标通过 CSS display 切换,无需 JS 判断。
4.4 声音提醒
不依赖任何音频文件,用 Web Audio API 实时合成双音提示:
function playSound() {
const audioCtx = new AudioContext();
[880, 1320].forEach((freq, i) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.frequency.value = freq;
osc.type = 'sine';
// ADSR 包络
gain.gain.setValueAtTime(0, now + i * 0.15);
gain.gain.linearRampToValueAtTime(0.3, now + i * 0.15 + 0.02);
gain.gain.exponentialRampToValueAtTime(0.001, now + i * 0.15 + 0.4);
osc.start(now + i * 0.15);
osc.stop(now + i * 0.15 + 0.4);
});
}
880Hz → 1320Hz 的上行二音,听感温和不刺耳,适合频繁触发的工作场景。
4.5 键盘快捷键
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT') return; // 输入框中不拦截
switch (e.code) {
case 'Space': toggleStartPause(); break;
case 'KeyR': resetTimer(); break;
case 'KeyS': skipSession(); break;
case 'Escape': closeSettings(); break;
}
});
空格键开始/暂停是最常用的操作,单键直达,无需鼠标移动。
五、UI 设计细节
视觉层级
┌─────────────────────────────┐
│ 🍅 番茄时钟 [☀️] │ ← Header(品牌 + 主题切换)
├─────────────────────────────┤
│ [专注工作] [短休息] [长休息] │ ← 模式切换 Tab
├─────────────────────────────┤
│ ┌───────────┐ │
│ │ 25:00 │ │ ← 圆形进度环 + 时间显示
│ │ 点击开始 │ │
│ └───────────┘ │
│ [你正在做什么?] │ ← 任务标签输入
│ [↺] [开始] [⏭] │ ← 控制按钮
├─────────────────────────────┤
│ 0 0分 0/4 │ ← 统计面板
│ 今日番茄 专注时长 轮次 │
├─────────────────────────────┤
│ 今日记录 │ ← 历史列表
│ 🍅 写博客 14:30 │
└─────────────────────────────┘
设计决策记录
| 决策 | 选择 | 理由 |
|---|---|---|
| 字体 | Inter | 现代无衬线,数字字形优秀,tabular-nums 等宽显示 |
| 计时数字 | 56px / 700 weight | 视觉焦点,一眼可读 |
| 进度环 | 280px / 6px stroke | 不会喧宾夺主,但足够醒目 |
| 主按钮 | 140px min-width | 拇指可触达,移动端友好 |
| 圆角 | 16px 卡片 / 10px 内部 | 柔和但不失结构感 |
| 阴影 | 0 8px 40px rgba(0,0,0,0.1) |
轻浮于背景,但不突兀 |
六、服务器实现
极简静态文件服务器,40 行代码:
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer((req, res) => {
let urlPath = req.url === '/' ? '/index.html' : req.url;
const filePath = path.join(__dirname, path.basename(urlPath));
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 Not Found');
return;
}
const ext = path.extname(filePath);
const mime = MIME_TYPES[ext] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': mime });
res.end(data);
});
});
server.listen(8080, '0.0.0.0');
关键细节:0.0.0.0 而非 127.0.0.1——在华为云 CloudIDE 沙箱中,服务必须监听所有网卡才能被端口转发机制访问。
开发反思
做对了的事
1. 技术栈克制
没有引入任何框架、构建工具、依赖包。npm install 零依赖,node server.js 即跑。对于这个体量的应用,这是正确的选择——复杂度与收益成正比。
2. CSS 变量驱动主题
深色模式和模式配色都通过 CSS 变量实现,JS 只需修改一个变量值,所有相关元素自动过渡。这比 JS 逐个操作 DOM style 的方案优雅得多。
3. 状态集中管理
单一 state 对象 + 纯函数式 UI 更新,调试时只需检查状态变化点,不需要追踪多个分散的变量。
可以改进的地方
1. 数据持久化
当前设置和历史记录存在 localStorage,换设备即丢失。如果接入云端同步(如华为云 OBS),体验会更完整。
2. 单元测试
计时逻辑(特别是模式切换、轮次追踪)适合用单元测试覆盖。当前纯手测,边界 case(如自定义时长为 1 分钟、长休息间隔为 2)未系统验证。
3. PWA 支持
添加 Service Worker + manifest 可以让应用离线可用、可安装到桌面。对于「即开即用」的工具类应用,这是自然的下一步。
结语
这个项目证明了一件事:不是所有应用都需要框架。
当你清楚自己的需求边界——一个单页、一个状态机、三种模式——原生 Web 技术的三件套足以交付一个设计精良、体验流畅的产品。克制的技术选型反而让开发更快、代码更可读、维护更简单。
番茄时钟现在每天陪我工作。如果你也是那个在消息洪流中试图保持专注的人,不妨试试。
2026-10-14 · 华为云开发者作品展览馆
- 点赞
- 收藏
- 关注作者

评论(0)