JS `Service Worker` `Background Sync` / `Periodic Sync` `API` 离线数据同步策略

各位观众老爷们,大家好!今天给大家带来的节目是“Service Worker 的离线数据同步大戏”,保证让各位看得津津有味,学得乐此不疲。

咱们今天的主角是 Service Worker,它就像一个默默守护 Web 应用的忠实管家,即便在网络不给力的时候,也能让你的应用保持坚挺。而今天,我们要聊的是它的两个重要技能:Background Sync 和 Periodic Sync,它们分别负责在离线状态下完成数据同步的重任。

第一幕:Service Worker 登场,奠定离线基础

首先,咱们得确保 Service Worker 已经成功注册并激活。如果没有 Service Worker,一切都是空谈。来,先上点代码:

// index.js (或你的入口文件)

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('Service Worker 注册成功:', registration);
    })
    .catch(error => {
      console.log('Service Worker 注册失败:', error);
    });
} else {
  console.log('你的浏览器不支持 Service Worker,换个浏览器试试?');
}

这段代码的作用很简单:检查浏览器是否支持 Service Worker,如果支持,就注册 sw.js 文件(Service Worker 的脚本)。

接下来是 sw.js 的内容:

// sw.js

self.addEventListener('install', event => {
  console.log('Service Worker 安装成功');
  // 跳过等待,直接激活
  event.waitUntil(self.skipWaiting());
});

self.addEventListener('activate', event => {
  console.log('Service Worker 激活成功');
  // 控制所有的 clients
  event.waitUntil(self.clients.claim());
});

self.addEventListener('fetch', event => {
  // 这里先简单处理,后续会涉及缓存策略
  event.respondWith(
    fetch(event.request)
      .catch(() => {
        // 如果网络请求失败,返回一个默认的响应
        return new Response('<h1>网络好像出了点问题...</h1>', {
          headers: { 'Content-Type': 'text/html' }
        });
      })
  );
});

这段代码定义了 Service Worker 的生命周期事件:install(安装)、activate(激活)和 fetch(拦截网络请求)。 目前,fetch 事件只是简单地尝试从网络获取资源,如果失败,就返回一个错误页面。

第二幕:Background Sync 上演,离线提交数据不再是梦

现在,我们来聊聊 Background Sync。想象一下,用户在离线状态下填写了一个表单,点击了提交按钮。如果没有 Background Sync,用户只能眼睁睁地看着提交失败,体验非常糟糕。有了 Background Sync,Service Worker 会在后台等待网络恢复,然后自动重新提交表单,给用户一个丝滑的体验。

首先,我们需要在 sw.js 中监听 sync 事件:

// sw.js

self.addEventListener('sync', event => {
  console.log('收到 sync 事件:', event);

  if (event.tag === 'new-post') {
    event.waitUntil(
      postNewData() // 稍后定义这个函数
    );
  }
});

这段代码监听了 sync 事件,并根据 event.tag 来判断需要执行哪个同步任务。这里我们假设 event.tagnew-post,表示需要提交新的文章。

接下来,我们需要定义 postNewData 函数,这个函数负责从某个地方(例如 IndexedDB)读取需要提交的数据,然后发送到服务器:

// sw.js

function postNewData() {
  return readAllData('posts') // 假设我们把离线数据存储在 IndexedDB 的 'posts' 存储桶中
    .then(allData => {
      const promises = [];
      for (const data of allData) {
        console.log('正在同步数据:', data);
        promises.push(
          fetch('https://your-api.com/posts', { // 替换成你的 API 地址
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'Accept': 'application/json'
            },
            body: JSON.stringify(data)
          })
            .then(res => {
              if (res.ok) {
                // 同步成功,从 IndexedDB 中删除这条数据
                return deleteData('posts', data.id);
              } else {
                // 同步失败,抛出错误,让 Background Sync 稍后重试
                throw new Error('同步失败');
              }
            })
            .catch(err => {
              console.error('同步数据失败:', err);
              throw err; // 重要:抛出错误,让 Background Sync 知道需要重试
            })
        );
      }
      return Promise.all(promises);
    });
}

// 假设我们有 readAllData 和 deleteData 函数来操作 IndexedDB
// 稍后会给出示例代码

这个 postNewData 函数做了以下几件事:

  1. 从 IndexedDB 中读取所有需要提交的数据。
  2. 遍历这些数据,逐个发送到服务器。
  3. 如果同步成功,从 IndexedDB 中删除这条数据。
  4. 如果同步失败,抛出一个错误。这是非常重要的一步,Background Sync 会根据这个错误来判断是否需要重试。

现在,我们需要在前端代码中注册 Background Sync:

// index.js (或你的表单提交代码)

function registerSync() {
  navigator.serviceWorker.ready
    .then(swRegistration => {
      return swRegistration.sync.register('new-post'); // 注册一个名为 'new-post' 的同步任务
    })
    .then(() => {
      console.log('Background Sync 注册成功');
    })
    .catch(error => {
      console.log('Background Sync 注册失败:', error);
    });
}

// 在表单提交时调用 registerSync 函数
document.getElementById('new-post-form').addEventListener('submit', event => {
  event.preventDefault(); // 阻止默认的表单提交行为

  const title = document.getElementById('title').value;
  const content = document.getElementById('content').value;

  const post = {
    id: new Date().toISOString(), // 生成一个唯一的 ID
    title: title,
    content: content
  };

  // 离线状态下,将数据存储到 IndexedDB
  writeData('posts', post)
    .then(() => {
      // 注册 Background Sync
      registerSync();
    });
});

这段代码做了以下几件事:

  1. 在表单提交时,阻止默认的提交行为。
  2. 从表单中获取数据,并生成一个唯一的 ID。
  3. 将数据存储到 IndexedDB 中。
  4. 注册一个名为 new-post 的 Background Sync 任务。

现在,让我们来补充一下 IndexedDB 的操作函数:

// utility.js (或者你存放 IndexedDB 操作代码的文件)

let dbPromise = idb.openDB('posts-db', 1, { // 使用 idb-keyval 库简化 IndexedDB 操作
  upgrade(db) {
    if (!db.objectStoreNames.contains('posts')) {
      db.createObjectStore('posts', { keyPath: 'id' });
    }
  }
});

function writeData(st, data) {
  return dbPromise
    .then(db => {
      const tx = db.transaction(st, 'readwrite');
      const store = tx.objectStore(st);
      store.put(data);
      return tx.done;
    });
}

function readAllData(st) {
  return dbPromise
    .then(db => {
      const tx = db.transaction(st, 'readonly');
      const store = tx.objectStore(st);
      return store.getAll();
    });
}

function deleteData(st, id) {
  return dbPromise
    .then(db => {
      const tx = db.transaction(st, 'readwrite');
      const store = tx.objectStore(st);
      store.delete(id);
      return tx.done;
    });
}

这里我们使用了 idb-keyval 库来简化 IndexedDB 的操作。如果没有安装,可以通过 npm 安装:npm install idb-keyval。 你也可以直接使用原生的 IndexedDB API,但是代码会比较冗长。

第三幕:Periodic Sync 闪亮登场,定期更新数据不是问题

Background Sync 解决了离线提交数据的问题,而 Periodic Sync 则解决了定期更新数据的问题。 想象一下,你的新闻应用需要在后台定期更新新闻列表,即便用户没有打开应用。有了 Periodic Sync,Service Worker 就可以在后台定期执行更新任务,给用户带来最新的内容。

首先,我们需要在 sw.js 中监听 periodicsync 事件:

// sw.js

self.addEventListener('periodicsync', event => {
  console.log('收到 periodicsync 事件:', event);

  if (event.tag === 'update-news') {
    event.waitUntil(
      updateNews() // 稍后定义这个函数
    );
  }
});

这段代码监听了 periodicsync 事件,并根据 event.tag 来判断需要执行哪个同步任务。这里我们假设 event.tagupdate-news,表示需要更新新闻列表。

接下来,我们需要定义 updateNews 函数,这个函数负责从服务器获取最新的新闻列表,并更新本地缓存:

// sw.js

function updateNews() {
  console.log('正在更新新闻列表...');
  return fetch('https://your-api.com/news') // 替换成你的新闻 API 地址
    .then(res => {
      if (res.ok) {
        return res.json();
      } else {
        throw new Error('获取新闻列表失败');
      }
    })
    .then(news => {
      // 将新闻列表存储到缓存中
      return caches.open('news-cache')
        .then(cache => {
          const promises = news.map(item => {
            return cache.put(new Request(`/news/${item.id}`), new Response(JSON.stringify(item), {
              headers: { 'Content-Type': 'application/json' }
            }));
          });
          return Promise.all(promises);
        });
    })
    .catch(err => {
      console.error('更新新闻列表失败:', err);
      throw err; // 重要:抛出错误,让 Periodic Sync 知道需要重试
    });
}

这个 updateNews 函数做了以下几件事:

  1. 从服务器获取最新的新闻列表。
  2. 将新闻列表存储到缓存中。
  3. 如果获取新闻列表失败,抛出一个错误。这是非常重要的一步,Periodic Sync 会根据这个错误来判断是否需要重试。

现在,我们需要在前端代码中注册 Periodic Sync:

// index.js (或你的应用初始化代码)

function registerPeriodicSync() {
  navigator.serviceWorker.ready
    .then(swRegistration => {
      return swRegistration.periodicSync.register('update-news', {
        minInterval: 24 * 60 * 60 * 1000, // 最小同步间隔:24 小时
      });
    })
    .then(() => {
      console.log('Periodic Sync 注册成功');
    })
    .catch(error => {
      console.log('Periodic Sync 注册失败:', error);
    });
}

// 在应用初始化时调用 registerPeriodicSync 函数
if ('periodicSync' in navigator.serviceWorker) {
  registerPeriodicSync();
} else {
  console.log('你的浏览器不支持 Periodic Sync');
}

这段代码做了以下几件事:

  1. 检查浏览器是否支持 Periodic Sync。
  2. 如果支持,注册一个名为 update-news 的 Periodic Sync 任务,并设置最小同步间隔为 24 小时。

第四幕:缓存策略,让数据更快更可靠

有了 Background Sync 和 Periodic Sync,我们已经可以实现离线数据提交和定期更新了。但是,为了让应用更快更可靠,我们还需要制定合理的缓存策略。

常见的缓存策略有以下几种:

策略名称 描述 适用场景
Cache First 优先从缓存中获取资源,如果缓存中没有,再从网络获取,并缓存到本地。 静态资源(例如 CSS、JavaScript、图片等),以及不经常更新的数据。
Network First 优先从网络获取资源,如果网络请求失败,再从缓存中获取。 经常更新的数据,以及对实时性要求较高的数据。
Cache Only 只从缓存中获取资源,如果缓存中没有,则返回错误。 完全离线可用的资源,以及不依赖网络的数据。
Network Only 只从网络获取资源,不使用缓存。 实时性要求非常高的数据,以及不适合缓存的数据。
Stale-While-Revalidate 先从缓存中获取资源,然后发起网络请求更新缓存。 即使缓存中没有,也会先返回一个默认值,然后发起网络请求更新缓存。 这种策略可以保证用户始终能看到内容,同时也能及时更新数据。 适用于对实时性要求不高,但希望尽快显示内容的数据。 例如,新闻列表可以先显示缓存中的内容,然后后台更新。 这种策略可以提高用户体验,减少等待时间。

让我们来修改 sw.js 中的 fetch 事件,使用 Cache First 策略:

// sw.js

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // 缓存命中,直接返回
        if (response) {
          return response;
        }

        // 缓存未命中,从网络获取
        return fetch(event.request)
          .then(networkResponse => {
            // 检查是否是有效的响应
            if (!networkResponse || networkResponse.status !== 200 || networkResponse.type !== 'basic') {
              return networkResponse;
            }

            // 克隆一份 response,因为 response body 只能读取一次
            const responseToCache = networkResponse.clone();

            caches.open('my-site-cache') // 替换成你的缓存名称
              .then(cache => {
                cache.put(event.request, responseToCache);
              });

            return networkResponse;
          });
      })
  );
});

这段代码做了以下几件事:

  1. 尝试从缓存中获取资源。
  2. 如果缓存命中,直接返回缓存中的资源。
  3. 如果缓存未命中,从网络获取资源。
  4. 如果网络请求成功,将资源缓存到本地。

第五幕:调试与测试,确保一切正常

调试 Service Worker 可能会比较棘手,但是 Chrome 开发者工具提供了一些强大的功能,可以帮助我们进行调试。

  • Application 面板: 可以查看 Service Worker 的状态、缓存、IndexedDB 等信息。
  • Network 面板: 可以查看网络请求的详细信息,以及 Service Worker 的拦截情况。
  • Console 面板: 可以查看 Service Worker 的日志输出。

此外,我们还可以使用一些工具来模拟离线状态,例如:

  • Chrome 开发者工具: 可以在 Network 面板中设置 "Offline" 模式。
  • Service Worker API: 可以使用 navigator.onLine 属性来检测当前是否处于离线状态。

总结:离线数据同步,让 Web 应用更强大

通过 Background Sync 和 Periodic Sync,我们可以实现离线数据提交和定期更新,让 Web 应用在没有网络连接的情况下也能正常工作。 合理的缓存策略可以提高应用的性能和可靠性。

当然,这只是一个简单的示例,实际应用中可能需要考虑更多细节,例如:

  • 错误处理: 需要处理各种可能出现的错误,例如网络错误、服务器错误等。
  • 数据冲突: 如果多个设备同时修改了同一份数据,需要解决数据冲突的问题。
  • 安全性: 需要确保离线数据的安全性,防止数据泄露。

希望今天的节目能给大家带来一些启发,让大家对 Service Worker 的离线数据同步有更深入的了解。 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注