Background Sync实战:让离线操作不再丢失数据
我的写法,亲测靠谱
Background Sync 是个好东西,但用不好真的会把自己坑死。我最早在项目里尝试它的时候,以为就是注册个 sync 事件、发个请求就完事了,结果线上用户反馈“数据没同步”“点了提交半天没反应”,查日志发现根本没进 sync 事件。折腾了两天才搞明白:这玩意儿不是你想触发就触发的,得满足一堆条件,而且浏览器策略还特别“吝啬”。
后来我总结了一套相对稳妥的写法,核心思路是:别把 Background Sync 当主力,只当兜底;主流程该走正常请求还得走,sync 只负责处理失败或离线的情况。
下面是我现在项目里用的代码骨架:
// service-worker.js
self.addEventListener('sync', event => {
if (event.tag === 'submit-form') {
event.waitUntil(
submitPendingData()
.then(() => {
// 清理本地缓存
return clearPendingData();
})
.catch(err => {
// 可选:记录失败次数,避免无限重试
console.error('Sync failed, will retry later', err);
// 注意:这里不要 reject,否则可能停止重试
// 浏览器会自动重试,直到成功或用户清除数据
})
);
}
});
async function submitPendingData() {
const data = await getPendingData(); // 从 IndexedDB 读取
if (!data) return;
const response = await fetch('https://jztheme.com/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(HTTP ${response.status});
}
return response.json();
}
前端页面注册 sync 的时机也很关键。我一般不会在用户点“提交”按钮时立刻注册,而是先尝试正常请求,只有失败了才注册 sync:
// main.js
async function handleSubmit(formData) {
try {
// 先走正常流程
const res = await fetch('/api/submit', {
method: 'POST',
body: formData
});
if (res.ok) return showSuccess();
// 如果失败,存到本地并注册 sync
await saveToIndexedDB(formData);
registerBackgroundSync();
} catch (err) {
// 网络错误,直接走离线流程
await saveToIndexedDB(formData);
registerBackgroundSync();
}
}
function registerBackgroundSync() {
if ('serviceWorker' in navigator && 'sync' in self.registration) {
navigator.serviceWorker.ready.then(reg => {
reg.sync.register('submit-form').catch(console.error);
});
}
}
这种写法的好处是:90% 的正常情况走直连,速度快、体验好;只有真出问题(网络断、服务器挂)才靠 sync 兜底。而且 sync 事件里做了错误捕获但不 reject,避免浏览器过早放弃重试。
这几种错误写法,别再踩坑了
我见过太多人把 Background Sync 当成“万能异步任务”,结果翻车。以下是我踩过或看别人踩过的典型反面案例:
- 一上来就注册 sync,不管有没有数据:比如用户打开页面就
reg.sync.register('fetch-news'),结果浏览器觉得你滥用,直接不触发。Background Sync 不是定时任务,它是为了“恢复失败操作”设计的,不是轮询工具。 - 在 sync 事件里做耗时操作但没用 waitUntil:比如直接
fetch().then(...),没包在event.waitUntil()里。这样 SW 可能被终止,请求发一半就断了。必须用 waitUntil 告诉浏览器:“等我干完活再关”。 - 重试逻辑自己写,还写死循环:有人怕浏览器不重试,自己在 sync 里加
setTimeout或while循环重试。大错特错!浏览器有自己的重试策略(指数退避),你手动重试反而可能触发频率限制,导致彻底停掉 sync。 - 依赖 sync 保证 100% 成功:现实是,sync 可能永远不触发——比如用户清了浏览器数据、禁用了后台同步、或者手机省电模式杀掉了 SW。所以主流程不能依赖它,只能当备胎。
最惨的一次是我同事在 sync 里直接调 location.reload(),想刷新页面提示用户。结果 SW 里根本没有 location 对象,直接报错,sync 事件中断,数据永远没发出去……这种低级错误其实很常见,因为大家习惯在主线程写代码,忘了 SW 是独立环境。
实际项目中的坑
除了代码逻辑,实际部署时还有不少细节要注意:
权限和用户行为:Chrome 要求用户必须和页面有互动(比如点击)后才能注册 sync,否则 reg.sync.register() 会静默失败。所以别在页面加载时自动注册,一定要绑在用户操作后(比如点击提交按钮)。
调试困难:Background Sync 在 DevTools 里很难模拟。Application → Service Workers 面板有个 “Offline” 和 “Update on reload”,但 sync 事件触发要看浏览器心情。我一般用这段代码手动触发测试:
// 在 DevTools Console 执行(需先 focus 到 SW)
self.registration.sync.register('test-sync').then(() => {
console.log('Sync registered');
});
// 然后在 SW 里加个 test-sync 处理逻辑
但注意:真实设备上,sync 触发时间不确定,可能几秒,可能几小时(尤其 Android 低电量模式)。所以测试时要有耐心,或者用 Chrome 的 “Bypass for network” + 手动断网模拟。
iOS 支持问题:Safari 直到 iOS 16.4 才支持 Background Sync,而且行为和 Chrome 有差异。我建议先 feature detect:
if ('serviceWorker' in navigator && 'sync' in Registration.prototype) {
// 安全使用
}
别假设所有移动端都支持。对于不支持的设备,就老老实实用“本地存草稿+下次打开时自动提交”的方案。
另外,别在 sync 里传敏感数据。虽然 IndexedDB 本身在 SW 里是安全的,但如果用户共享设备,别人可能通过 DevTools 看到 pending 数据。重要数据该加密还是得加密。
结尾唠叨两句
Background Sync 不是银弹,但它在特定场景(比如表单提交、日志上报)下能极大提升离线体验。我的经验是:主流程别依赖它,只当保底;代码要健壮,别让异常中断 sync;测试要充分,尤其真机。
以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流,比如怎么结合 Workbox 简化流程,或者如何监控 sync 成功率——这些我还在摸索中,目前项目里还没上监控,有点心虚 😅

暂无评论