乐闻世界logo
搜索文章和话题

Service Worker 中的 Background Sync 是什么?如何使用?

3月6日 22:01

Service Worker Background Sync 详解

Background Sync(后台同步)是 Service Worker 的重要功能,允许在网络恢复后自动执行延迟的任务,特别适用于离线场景下的数据同步。

核心概念

Background Sync 解决了什么问题:

  • 用户在离线状态下提交表单或操作
  • 网络恢复后自动同步数据到服务器
  • 无需用户手动重试操作

使用场景

  1. 表单提交:离线时保存表单,联网后自动提交
  2. 消息发送:离线时保存消息,联网后自动发送
  3. 数据同步:离线操作的数据在联网后同步
  4. 日志上传:离线日志在网络恢复后批量上传

实现步骤

1. 注册同步事件

javascript
// main.js async function registerBackgroundSync(tag) { const registration = await navigator.serviceWorker.ready; try { await registration.sync.register(tag); console.log(`后台同步已注册: ${tag}`); } catch (error) { console.error('后台同步注册失败:', error); } } // 使用示例 async function submitForm(data) { // 先保存到本地数据库 await saveToIndexedDB('pending-forms', data); // 注册后台同步 await registerBackgroundSync('sync-forms'); // 如果在线,立即尝试发送 if (navigator.onLine) { try { await sendToServer(data); await removeFromIndexedDB('pending-forms', data.id); } catch (error) { console.log('发送失败,将在网络恢复后重试'); } } }

2. Service Worker 处理同步

javascript
// sw.js // 监听 sync 事件 self.addEventListener('sync', event => { console.log('后台同步触发:', event.tag); if (event.tag === 'sync-forms') { event.waitUntil(syncPendingForms()); } else if (event.tag === 'sync-messages') { event.waitUntil(syncPendingMessages()); } }); // 同步待提交的表单 async function syncPendingForms() { const pendingForms = await getFromIndexedDB('pending-forms'); for (const form of pendingForms) { try { await fetch('/api/submit-form', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form) }); // 同步成功,从待处理列表移除 await removeFromIndexedDB('pending-forms', form.id); // 通知用户 self.registration.showNotification('同步成功', { body: '您的表单已成功提交', icon: '/icons/success.png' }); } catch (error) { console.error('表单同步失败:', error); // 失败后会自动重试 throw error; } } } // 同步待发送的消息 async function syncPendingMessages() { const messages = await getFromIndexedDB('pending-messages'); const syncPromises = messages.map(async message => { try { await fetch('/api/send-message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(message) }); await removeFromIndexedDB('pending-messages', message.id); } catch (error) { console.error('消息同步失败:', error); throw error; } }); return Promise.all(syncPromises); }

3. IndexedDB 辅助函数

javascript
// db.js - IndexedDB 操作封装 const DB_NAME = 'background-sync-db'; const DB_VERSION = 1; function openDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => reject(request.error); request.onsuccess = () => resolve(request.result); request.onupgradeneeded = event => { const db = event.target.result; // 创建存储对象 if (!db.objectStoreNames.contains('pending-forms')) { db.createObjectStore('pending-forms', { keyPath: 'id' }); } if (!db.objectStoreNames.contains('pending-messages')) { db.createObjectStore('pending-messages', { keyPath: 'id' }); } }; }); } async function saveToIndexedDB(storeName, data) { const db = await openDB(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, 'readwrite'); const store = transaction.objectStore(storeName); const request = store.put(data); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function getFromIndexedDB(storeName) { const db = await openDB(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, 'readonly'); const store = transaction.objectStore(storeName); const request = store.getAll(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function removeFromIndexedDB(storeName, id) { const db = await openDB(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, 'readwrite'); const store = transaction.objectStore(storeName); const request = store.delete(id); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); }

完整示例:离线表单提交

javascript
// form-handler.js class OfflineFormHandler { constructor(formId) { this.form = document.getElementById(formId); this.setupEventListeners(); } setupEventListeners() { this.form.addEventListener('submit', async (e) => { e.preventDefault(); await this.handleSubmit(); }); } async handleSubmit() { const formData = new FormData(this.form); const data = { id: Date.now().toString(), timestamp: Date.now(), ...Object.fromEntries(formData) }; // 显示提交中状态 this.showStatus('保存中...'); try { // 保存到本地 await saveToIndexedDB('pending-forms', data); // 注册后台同步 await this.registerSync('sync-forms'); // 尝试立即发送 if (navigator.onLine) { await this.sendImmediately(data); } else { this.showStatus('已离线保存,联网后自动同步'); } this.form.reset(); } catch (error) { this.showStatus('保存失败: ' + error.message); } } async registerSync(tag) { const registration = await navigator.serviceWorker.ready; if ('sync' in registration) { await registration.sync.register(tag); } else { // 降级方案:立即发送或提示用户 console.log('浏览器不支持后台同步'); } } async sendImmediately(data) { try { await fetch('/api/submit-form', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); await removeFromIndexedDB('pending-forms', data.id); this.showStatus('提交成功!'); } catch (error) { this.showStatus('提交失败,将在后台重试'); } } showStatus(message) { const statusEl = document.getElementById('form-status'); statusEl.textContent = message; setTimeout(() => { statusEl.textContent = ''; }, 3000); } } // 初始化 const formHandler = new OfflineFormHandler('my-form');

定期后台同步(Periodic Background Sync)

javascript
// 请求定期同步权限 async function requestPeriodicSync() { const registration = await navigator.serviceWorker.ready; try { await registration.periodicSync.register('daily-sync', { minInterval: 24 * 60 * 60 * 1000 // 最少 24 小时 }); console.log('定期同步已注册'); } catch (error) { console.error('定期同步注册失败:', error); } } // sw.js 处理定期同步 self.addEventListener('periodicsync', event => { if (event.tag === 'daily-sync') { event.waitUntil(performDailySync()); } }); async function performDailySync() { // 执行每日同步任务 const data = await fetch('/api/daily-data').then(r => r.json()); await caches.open('daily-cache').then(cache => { cache.put('/daily-data', new Response(JSON.stringify(data))); }); }

浏览器兼容性

功能ChromeFirefoxSafariEdge
Background Sync
Periodic Sync

最佳实践

  1. 数据持久化:使用 IndexedDB 存储待同步数据
  2. 错误处理:同步失败时抛出错误以触发重试
  3. 幂等性设计:确保重复执行不会产生副作用
  4. 用户反馈:同步完成后通知用户
  5. 降级方案:为不支持的浏览器提供替代方案
javascript
// 降级方案示例 async function syncWithFallback() { const registration = await navigator.serviceWorker.ready; if ('sync' in registration) { // 使用后台同步 await registration.sync.register('sync-data'); } else { // 降级:立即尝试或提示用户 if (navigator.onLine) { await syncData(); } else { alert('您处于离线状态,请联网后重试'); } } }
标签:Service Worker