上一章我们对离线存储进行了讲解,本节我们继续介绍离线处理中的另外一个话题 - 后台同步,该机制允许用户随时随地进行事务处理,而无需关心网络的连接状态。比如以下示例:

通过演示我们可以看到,无论在线还是离线,甚至在触发了后台同步之后关闭页面,只要网络处于在线状态都会执行后台同步事件,并将缓存在本地的请求数据发送到服务端。对于传统的 Web 应用来说,该特性是极其令人兴奋的,因为它解决了传统 Web 应用所存在的以下几个问题:

  • 页面发起的请求会随着页面的关闭而终止。
  • 在离线状态下,很难将用户的网络请求缓存起来,并在网络恢复正常后再次进行请求。

那么它究竟是如何工作的?下面我们通过上面的演示实例对其进行说明。

# 基本使用

# 注册

在上面的演示中,当我们点击添加按钮后,控制台首先输出了已触发后台同步:add-todo,这便完成了后台同步事件注册,主要代码如下:

<script src="./db.js"></script>
<script src="./ui.js"></script>
<script src="./network.js"></script>
<script>
  window.addEventListener('load', function() {
    if ('serviceWorker' in navigator && 'SyncManager' in window) {
      navigator.serviceWorker.register('./sw.js').then(function(registration) {
        document.getElementById('submit').addEventListener('click', function() {
          ui.submit(function(name) {
            db.addTodo(name).then(function() {
              registration.sync.register('add-todo').then(function() {
                console.log('已触发后台同步:add-todo');
              });
            });
          });
        });
      });
      navigator.serviceWorker.addEventListener('message', function(event) {
        ui.render(event.data);
      });
    } else {
      document.getElementById('submit').addEventListener('click', function() {
        ui.submit(function(name) {
          network.addTodos([{ name: name }]).then(function(todos) {
            ui.render(todos);
          });
        });
      });
    }
  });
</script>

上述代码中,我们首先判断当前环境下 Service WorkerSyncManager 是否可用,如不可用则按照传统方式对按钮的点击事件进行处理,否则按照以下步骤进行处理:

  • 注册 Service Worker
  • 注册完成后,使用回调参数 registrationsync.register方法注册一个后台同步事件;在该例中,我们在回调中监听按钮的点击事件,并在点击事件中进行后台同步事件注册。
  • 当添加按钮点击且页面验证通过后(通过 ui.submit 方法),我们通过 db.addTodo 方法将需要发送到服务端的数据缓存在本地。
  • 数据缓存成功后,则调用 registration.sync.register 方法注册一个名为 add-todo 的后台同步事件。

以上便是后台同步注册的常规流程,其过程非常简单,但也需要注意以下几点:

  • registration.sync.register 的参数是事件的唯一标识,为了减少设备、浏览器需要唤醒的次数,浏览器可能会将多个具有相同标识的事件合并为一个;如果想要每个事件都触发一次,则需要使用不同的标识。
  • 由于 Service Worker 内不允许直接访问 DOM 元素,因此我们需要将发送到服务端的数据缓存到本地(根据上一章的讨论,我们一般使用 IndexedDB 进行处理)。

# 响应

完成了注册,接下来我们就需要在 Service Worker 中对事件进行响应,主要代码如下:

importScripts('./db.js');
importScripts('./network.js');

function notification(todos) {
  self.clients.matchAll().then(function(clients) {
    if (clients && clients.length) {
      clients.forEach(function(client) {
        client.postMessage(todos);
      });
    }
  });
}

self.addEventListener('sync', function(event) {
  if (event.tag === 'add-todo') {
    console.log(`开始进行后台同步:${event.tag}`);
    event.waitUntil(
      db.getTodos().then(function(todos) {
        return network.addTodos(todos).then(function(todos) {
          console.log('来自服务器的响应:', todos);
          notification(todos);
          return db.clearTodos();
        });
      })
    );
  }
});

因为 Service Worker 是独立于主线程运行的,所以即使在页面中引入了 ./db.js./network.js,我们仍需要通过 importScripts 方法将其引入。 而后,我们通过监听 sync 事件来响应同步事件,在回调函数中,我们主要做了以下事情:

  • 检查当前同步的标签(这里为 add-todo),从而触发正确的同步逻辑。
  • 获取存储在本地的需要发送到服务端的数据,并将其发送到服务端。
  • 同步完成后通知主线程并删除本地缓存。

至此我们完成了后台同步事件的响应,过程依旧非常简单,这里需要注意以下事项:

  • sync 回调函数返回值为 Promise 对象,由于浏览器内置了智能的重试机制,所以我们无需自行设计重试机制。
  • 上文已提到过,Service Worker 是无法直接操作 DOM 元素的,因此如果我们在同步处理成功后想要对 DOM 进行处理,可通过给主线程发送消息来实现:
// Service Worker 内实现参见 notification 方法

// 主线程
navigator.serviceWorker.addEventListener('message', function(event) {
  // doSomething with event.data;
});

# 重试

上文中我们提到,针对 sync 事件中的异常,浏览器内置了智能的重试机制,但它究竟何时执行且是否会一直执行下去呢?下面我们通过实际测试来回答这个问题。

通过上图的输出,我们可以看出:

  • 第一次执行失败后,第二次会在 5 分钟之后触发;
  • 第二次执行失败后,第三次会在 15 分钟后触发;
  • 如果第三次执行失败后,该同步事件将不会再触发。

总的来说,从我们通过 registration.sync.register 注册一个同步事件开始,到该事件的落幕,这期间它最多可被执行 3 次。如果想要在它惨淡落幕前给予用户提示,可通过以下方式实现:

self.addEventListener('sync', function(event) {
  if (event.tag === 'add-todo') {
    event.waitUntil(
      doSomething().catch(function(error) {
        if (event.lastChance) {
          //给予用户以友好提示
        }
        throw error;
      })
    );
  }
});

# 总结

本章中,我们详细介绍了后台同步,它为实现恶劣网络环境下,用户进行无感知的事务处理提供了可能。至此我们完成了 Service Worker离线存储、后台同步的学习,通过这些技术我们可以构建出应对复杂网络状况的 Web 应用,以此提高用户体验并逐步抹平与原生应用在离线处理方面的差异。在接下来的一个章节中,我们将对推送通知进行讨论,通过它我们可以让 Web 应用彻底突破浏览器限制,以实现曾经可望而不可及的系统深度集成

示例代码:github.com/nanjingboy/…

阅读全文