小项目里藏不住的 5 个前端工程问题:从 300 行原生 JS 番茄钟说起
上周我用纯原生 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'),点了播放也没声音。
两个可行做法:
- 用户首次交互后再创建 AudioContext(点击"开始"时创建),此时上下文处于 running 状态;
- 用
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 条当成分发前的自检清单:计时用绝对时间、状态用状态机、存储要能降级、媒体等交互、动画只改单值。
- 点赞
- 收藏
- 关注作者
评论(0)