小项目里藏不住的 5 个前端工程问题:从 300 行原生 JS 番茄钟说起

举报
yd_285576981 发表于 2026/10/29 09:21:40 2026/10/29
【摘要】 不依赖框架写小工具时,定时器漂移、状态打架、存储抛异常、自动播放拦截、渲染性能这些问题都会暴露出来。本文用 300 行原生 JS 番茄钟的真实代码,给出 5 条可直接复用的自检清单。

上周我用纯原生 JavaScript(没有任何框架、没有构建步骤)写了一个番茄钟应用,前后不到 300 行。原以为是个"玩具级"的小项目,做完才发现:越小的项目越藏不住工程问题——没有框架帮你兜底,每一个细节都得自己想清楚。

这里记录 5 个我在这个项目里真实踩到、并且值得写进 checklist 的问题。

一、用 setInterval 累加计时,一定会漂

最常见的写法是 remaining--,每秒减一。问题是浏览器会对后台标签页节流定时器,最小间隔可能被拉到 1 秒以上甚至分钟级;用户切回来时,计时器已经慢了十几秒。

正确做法是只存绝对截止时间,每次渲染都用它反算:

// 开始/继续时
state.deadline = Date.now() + state.remaining * 1000;

// 每次 tick
state.remaining = (state.deadline - Date.now()) / 1000;

再补一个 visibilitychange 监听,页面重新可见时立刻按 deadline 校正一次。这样即便标签页被挂起十分钟,回来显示的仍是真实剩余时间。

顺带一个细节:刷新间隔用 200ms 而不是 1000ms。因为要用它驱动进度环,1 秒一跳会明显卡顿,200ms 肉眼几乎无感,成本也可以忽略。

二、状态散成一堆布尔量,迟早互相打架

一开始我写的是 isRunning、isPaused、isFinished 三个布尔量,很快就出现了"既 running 又 finished"的非法组合。后来收敛成一个状态机:

idle → running ⇄ paused

再配上独立的 mode(focus / short / long)。好处是:开始、暂停、重置、模式切换这四个入口,都只改状态、不直接改 UI;UI 是状态的纯函数。从"改 UI 的地方有 5 处"变成"只有 1 个渲染函数",调试成本直接下降一个数量级。

三、localStorage 不是永远可用,也不是永远可信

两个坑:

  • 写入会抛异常:Safari 隐私模式、存储配额满、站点被限制时,setItem 会直接抛 QuotaExceededError。不包 try/catch 的话,整个初始化流程会在这里断掉,用户看到的是白屏。
  • 读到的数据不可信:用户可能手改过、旧版本结构可能不同。JSON.parse 要包在 try/catch 里,解析失败就降级为内存状态,功能照常可用,只是不持久化。

原则是:存储永远只当作加速器,不能当作唯一真相。存不进去时应用要能跑,只是体验降级。

四、Web Audio 会被自动播放策略拦截

想在番茄结束时"叮"一声,最省事的做法是引一个 mp3。但更大的问题是:直接 new AudioContext() 在页面加载时创建,会被浏览器的自动播放策略挂起(state === 'suspended'),点了播放也没声音。

两个可行做法:

  1. 用户首次交互后再创建 AudioContext(点击"开始"时创建),此时上下文处于 running 状态;
  2. 用 OscillatorNode 现场合成短音,不依赖任何音频文件——省掉一次网络请求,也不会有资源加载失败的分支。

我选了方案 2,顺便把"外部资源"这个变量从项目里彻底删掉了。

五、进度环别逐帧改 DOM 结构

用 SVG 画进度环时,容易写成"每次 tick 重建 <circle> 或者改一堆属性"。实际上只需要一个值:

stroke-dasharray  = 2πr          // 固定,只算一次
stroke-dashoffset = -2πr * 进度   // 每次改这一个

半径 98 时 2πr ≈ 615.75。配合 transition: stroke-dashoffset 0.24s linear,浏览器会用合成层平滑插值,比自己算缓动省事得多。

小结

这五条没有一条是"新知识",它们都是框架平时替你处理掉的东西:定时器精度、状态一致性、存储容错、媒体策略、渲染性能。只有在不依赖框架时,你才会真正意识到这些默认值值多少钱。

如果你也在写小工具类项目,可以把这 5 条当成分发前的自检清单:计时用绝对时间、状态用状态机、存储要能降级、媒体等交互、动画只改单值。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。