Service Worker 的 Background Sync 功能,常被开发者寄予厚望,希望能精确地在“网络空闲时”触发同步任务。但这里需要先澄清一个关键点:它本身并不提供对“网络空闲”状态的直接监听或控制能力。浏览器内部会基于网络可用性、设备电量、连接类型(比如是否处于蜂窝网络)以及任务优先级等多种因素,来综合调度 sync 事件的触发时机。开发者无法主动指定“等网络变空闲再执行”。
那么,这是否意味着“网络空闲时同步”的设想就落空了呢?并非如此。通过一套合理的设计逻辑与数据管理策略,我们完全可以间接实现类似的效果:让那些因网络问题而失败的同步操作,在真正合适的时机(例如网络恢复且系统资源相对宽松时)被可靠地异步恢复。

确保原始数据可靠持久化
一切可靠同步的前提,是数据本身不能丢失。所有待同步的原始数据——无论是表单内容、上传文件的元信息,还是应用日志条目——都必须在主线程中,优先存入 IndexedDB 或 Cache API 这类持久化存储中。切忌仅将其保留在内存或 localStorage 里。
- IndexedDB 因其支持结构化数据和事务操作,非常适合存储带有时间戳、状态标记和重试次数的请求对象。
- 建议每条记录至少包含以下字段:id、url、method、body(需序列化)、headers、createdAt、retryCount、lastAttemptAt。
- 一个关键的操作顺序是:先确保数据写入数据库成功,再注册 sync 任务。这样可以有效避免“任务注册了,数据却没存住”的尴尬局面。
用 sync 事件触发异步恢复逻辑
在 Service Worker 的 sync 事件处理器中,逻辑设计不应是简单地发起 fetch 请求。取而代之的,应该是一个具备可中断、可重试且带有节流判断的智能恢复流程:
- 通过 event.tag 来匹配对应类型的同步任务(例如 ‘recover-failed-uploads’)。
- 使用 event.waitUntil() 包裹一个 Promise,在这个 Promise 内部,首先读取 IndexedDB 中状态为 ‘pending’ 或 ‘failed’ 的记录。
- 对每一条待恢复的记录,进行一次轻量级的预检:检查 navigator.onLine 是否为 true,确认 fetch API 可用,甚至可以利用 navigator.connection.effectiveType 来判断当前网络类型是否为 ‘4g’ 或更好(此步骤可选,但能提升体验)。
- 使用 Promise.allSettled() 来并行处理多条记录,但务必设置并发数上限(例如最多同时处理3个),避免瞬间占满所有网络连接,反而影响用户体验。
失败后智能重试,而非立即重注册
Background Sync 并不保证一次成功,也不会自动为你重试。因此,重试节奏的控制权完全在开发者手中,目标是避免因频繁重试导致网络“雪崩”或过度消耗设备电量:
- 每次同步尝试失败后,立即在 IndexedDB 中更新对应记录的 retryCount(重试次数)和 lastAttemptAt(最后尝试时间)。
- 根据失败原因决定后续策略:对于服务端 5xx 错误,可以延后重试;对于客户端 4xx 错误(如参数错误),应标记为永久失败,不再重试;对于网络超时,则可以采用指数退避策略(例如等待 1秒 → 5秒 → 30秒)。
- 在决定重新注册 sync 事件前,加入防抖逻辑:如果上次失败时间距离现在不足30秒,则跳过本次注册。这能有效防止因页面频繁交互而触发大量重复的同步任务。
- 重注册时,使用相同的 tag。这样,当下一次 sync 事件触发时,就能继续处理这批尚未成功的数据。
配合周期性同步(Periodic Sync)补充兜底
虽然一次性的 sync 事件是处理即时失败的主流方式,但 Periodic Sync(需在 manifest.json 中配置并获取用户授权)可以作为一项有力的补充。它能在设备长时间离线后,定期唤醒 Service Worker,检查是否有积压的未恢复数据:
- 在 manifest.json 中声明 “periodicSync”: { “minInterval”: 120000 }(最小间隔,例如至少2分钟)。
- 在主页面中调用 registration.periodicSync.register() 来请求周期同步任务。
- Service Worker 监听 periodicsync 事件,其内部逻辑应设计为“扫描 IndexedDB + 触发一次恢复流程”,而不是直接发起网络请求。
- 需要明确的是,Periodic Sync 并非用来替代 one-shot sync,而是作为一种后台健康检查机制,进一步提升数据最终一致性的保障水平。
