跨设备阅读进度同步,从底层技术角度来看,HTML 本身确实无法独立完成这一任务。它既不提供持久化状态存储能力,也不支持跨设备间的实时通信。实现这一功能的唯一路径,是前端 JavaScript 与后端服务(或稳定的第三方平台)协同配合。

坦白说,这个问题没有捷径可走。在设计和实现阶段,必须将“本地存储”与“服务端同步”这两件事彻底拆分开来思考,才能构建出可靠的跨设备体验。
localStorage 仅限同源同设备,无法跨设备使用
不少开发者可能会想,通过 localStorage.setItem('readProgress', '123') 写入进度后,换一台手机打开同一网址,进度就能自动同步。可惜现实并非如此。localStorage 严格遵循同源策略,只存在于当前浏览器 + 当前域名这一封闭环境中。它不会主动上传数据,也不会向外共享,更无法感知其他设备的存在。
- 两台设备同时访问
https://example.com/article.html,各自的localStorage完全隔离——数据互不干扰,也无法互通。 - 同一台设备上,不同浏览器(如 Chrome 和 Safari)之间,localStorage 的数据同样不共享。
- 在无痕模式下,写入操作可能直接抛出
QuotaExceededError,因此必须使用try/catch进行降级处理。
localStorage 加时间戳实现“伪同步”存在明显缺陷
有人想到一种“取巧”方案:写入时附带时间戳,然后通过轮询比较谁的时间戳更新。表面上看起来可行,但本质上并非真正的同步。问题主要集中于以下几点:
- 缺乏服务端作为中间协调者,设备 A 写入数据后,设备 B 无从得知该去哪里拉取最新进度。
- 时间戳仅代表“本地存储的时刻”,而非“服务端确认的时刻”,两者含义完全不同,无法作为同步依据。
- 用户离线时,localStorage 的任何更新都无法被其他设备感知,更无法妥善处理冲突合并。
- 多个标签页同时写入时,
Date.now()微秒级的差异就可能导致覆盖逻辑完全混乱,产生数据丢失。
真正可行的方案只有两种路径
要实现跨设备同步,要么依赖服务端作为中转枢纽(这是最推荐的方式),要么借助成熟的第三方平台来托管状态数据。
- 服务端路径:前端通过
POST /api/progress提交当前章节、滚动位置及时间戳,服务端将其存入用户关联的数据库。另一设备打开文章时,再通过GET /api/progress?articleId=123拉取最新记录,从而实现同步。 - 第三方路径:接入 Firebase Realtime Database 或 Supabase 等平台,使用
ref('users/{uid}/progress/{articleId}').set(...)写入进度,并监听onValue事件自动响应变化。这种方式将协调任务交给云服务,减轻自建服务端的负担。 - 不要尝试将进度存储在 URL hash 中并通过参数传递同步。这种做法容易被截断、存在安全风险,且不支持后台更新,属于“看起来能用,实际体验很差”的方案。
UI 层需清晰区分“已同步”与“待同步”状态
当用户滑动到 85% 位置时,前端应立刻更新本地显示,同时将同步操作放到后台异步执行。务必给出明确的视觉反馈,避免用户感到卡顿或困惑。
- 提交成功之前,显示“正在保存…”并禁用重复操作,防止产生冗余请求。
- 如果同步失败,先保留本地值,并标记
isStale: true,等待网络恢复后自动重试。 - 不要等服务端响应后才更新 UI,否则滚动操作会明显卡顿。核心原则是“先响应本地,再后台同步”,确保交互流畅。
- 服务端返回的
updatedAt时间,建议使用Intl.DateTimeFormat进行格式化,而非new Date().toString(),否则在中文环境下可能出现英文月份,影响用户体验。
跨设备阅读进度同步,从来不是 HTML 能够独立解决的技术问题。真正的关键不在于“如何存储”,而在于“谁负责协调、谁保证最终一致性、谁处理离线与冲突”——这些角色都需要由服务端来兜底。前端能做的,是把状态传准、反馈做及时、降级处理设计得合理。做到这三点,整体体验基本就稳定可靠了。
