从零实现一个离线优先的专注工具:FocusFlow 的架构与取舍
从零实现一个离线优先的专注工具:FocusFlow 的架构与取舍
很多效率工具默认假设用户始终在线:登录、云同步、协作空间一应俱全。但对于番茄计时、待办清单这类高频而私人的场景,网络并不是必需品。FocusFlow 是一次刻意做减法的实践:只用原生 HTML、CSS 和 JavaScript,把计时器、任务清单、今日统计与数据导出放进一个可以离线运行的页面。
本文结合项目实现,聊聊一个小型前端应用如何在不引入框架和后端的前提下,保持清晰的状态边界、可追踪的数据流,以及足够低的维护成本。
一、先划清产品边界
FocusFlow 只解决三个问题:
- 用番茄计时帮助用户进入专注状态;
- 用轻量任务清单记录当前要做的事;
- 用按日统计反馈今天的投入。
因此它没有账号体系、没有服务端 API,也没有引入 React、Vue 或构建工具。页面由 index.html、style.css、app.js 三个文件组成,双击即可运行,也可以放到任意静态托管平台。
这种边界带来的直接收益是部署成本几乎为零,数据默认留在本机。代价则是不支持多设备同步,也无法跨浏览器共享。对于一个个人专注工具来说,这组取舍是合理的。
二、用单一状态对象收拢运行时数据
界面元素很多,但真正变化的数据可以收敛到一个状态对象中:
var state = {
mode: "focus",
running: false,
remaining: 25 * 60,
total: 25 * 60,
focusMinutes: 25,
breakMinutes: 5,
tasks: [],
stats: { count: 0, minutes: 0, date: "" },
intervalId: null
};
计时模式、剩余秒数、任务数组和今日统计都集中在这里。按钮事件只负责修改状态,然后再调用对应的渲染函数。这样做的目的不是追求“架构感”,而是避免同一份数据散落在多个 DOM 属性里,导致刷新后状态不一致。
持久化层使用三个明确的键空间:
var STORAGE_KEYS = {
TASKS: "focusflow-tasks",
CONFIG: "focusflow-timer-config",
STATS_PREFIX: "focusflow-stats-"
};
任务和配置可以长期保存,统计记录则追加日期后缀。每天第一次打开页面时,应用会读取当天对应的键;如果不存在,就创建一条新的统计记录。日期隔离让跨日重置变得自然,也避免了定时清理历史数据。
三、localStorage 也需要防御性读取
本地数据不代表永远可信。用户可能手动修改存储,旧版本也可能写入不同结构。因此读取时需要对 JSON 解析和字段范围做兜底:
function safeParse(str, fallback) {
try {
return JSON.parse(str);
} catch (e) {
return fallback;
}
}
function load(key, fallback) {
var raw = localStorage.getItem(key);
return raw === null ? fallback : safeParse(raw, fallback);
}
配置中的专注和休息时长还会经过 clamp 限制范围。即使本地数据损坏,页面也能回退到默认值,而不是在启动阶段直接抛异常。
写入同样包在 try/catch 中。浏览器可能因为隐私模式、存储配额或安全策略拒绝写入,但一次存储失败不应该阻断计时和任务操作。应用会记录警告,同时保留当前会话内的状态。
四、计时器先保证行为直观
当前计时器每秒执行一次 tick,递减剩余秒数,并在归零时切换阶段。专注阶段结束后,应用累加一次专注记录和对应分钟数,然后自动进入休息阶段。
为了让代码易读,状态与视图的更新被拆开:
function tick() {
state.remaining -= 1;
if (state.remaining <= 0) {
state.remaining = 0;
renderTimer();
onPhaseComplete();
return;
}
renderTimer();
}
这种实现适合短时前台计时,逻辑透明,也容易调试。如果要支持长时间后台运行,就不能完全依赖 setInterval 的累计次数,因为浏览器会节流后台标签页。更稳妥的方案是记录阶段的绝对结束时间,用 Date.now() 计算剩余时间,前台只负责定时刷新界面。这是 FocusFlow 后续可以继续演进的方向。
五、显式渲染比虚拟 DOM 更适合这个小项目
任务列表使用原生 DOM API 创建节点,没有拼接不可信字符串。任务文本通过 textContent 写入,避免把用户输入当作 HTML 解析:
var span = document.createElement("span");
span.className = "task-text";
span.textContent = task.text;
每次任务变化后,renderTasks 重新生成列表,并同步总数和完成数。对于几十条任务而言,这种显式渲染足够快;同时,所有视图变化都能顺着事件处理函数找到源头,不需要理解框架的调度与依赖追踪。
数据导出同样完全在浏览器中完成。应用用 Blob 组装结构化 JSON,再通过临时 URL.createObjectURL 触发下载,最后调用 revokeObjectURL 释放引用。过程不经过服务端,导出内容包含任务、配置、当日统计和导出时间,便于备份或二次分析。
六、把键盘与辅助技术当作一等用户
专注工具往往需要快速操作。项目提供了一组不与输入框冲突的快捷键:空格开始或暂停,R 重置,N 聚焦任务输入框,Esc 暂停或退出输入状态。处理键盘事件前先判断当前焦点是否位于输入控件,避免用户输入文字时误触计时器。
除键盘外,关键按钮和状态区域也补充了 aria-label 与 aria-live。SVG 进度环、颜色切换和动画只是视觉强化,真正的剩余时间始终由文本承载。这样即使动画被关闭,信息也不会丢失。
七、离线优先不是拒绝云端
离线优先的核心不是“永远不上云”,而是让本地能力成为默认路径。对 FocusFlow 来说,任务和统计先准确落在本地,用户可以随时导出自己的数据;未来若增加云同步,也应当把本地写入作为第一优先级,再将同步作为可选的增量能力。
如果要扩展,我会优先考虑三件事:用 IndexedDB 替换容量有限的 localStorage;增加基于绝对结束时间的计时校准;为导出数据补充版本号和迁移函数。它们都能在不破坏现有单页结构的前提下逐步完成。
结语
小型前端项目并不意味着可以忽略状态设计。相反,正因为没有框架代管数据流,才更需要明确键空间、渲染入口和故障边界。FocusFlow 用三个文件完成了一个可离线、可导出、可键盘操作的专注工具,也证明了:克制地选择技术,往往比堆叠依赖更能延长一个产品的生命周期。
项目示例代码只依赖浏览器原生能力,适合作为前端状态管理、localStorage 持久化和无障碍实践的入门参考。
- 点赞
- 收藏
- 关注作者
评论(0)