Background Sync实战:让离线操作不再丢失数据

IT人卿硕 移动 阅读 2,089
赞 24 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

Background Sync 是个好东西,但用不好真的会把自己坑死。我最早在项目里尝试它的时候,以为就是注册个 sync 事件、发个请求就完事了,结果线上用户反馈“数据没同步”“点了提交半天没反应”,查日志发现根本没进 sync 事件。折腾了两天才搞明白:这玩意儿不是你想触发就触发的,得满足一堆条件,而且浏览器策略还特别“吝啬”。

Background 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 里加 setTimeoutwhile 循环重试。大错特错!浏览器有自己的重试策略(指数退避),你手动重试反而可能触发频率限制,导致彻底停掉 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 成功率——这些我还在摸索中,目前项目里还没上监控,有点心虚 😅

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论