w3ctech

[译]ServiceWorker与逐步联网策略

点击原文观看作者原文。

这篇文章是关于我在处理ServiceWorker的过期缓存的一些心得。在后续的文章我会描述如何部署和客户端交流的方法。然而在此文,我更偏向于描述基于swivel的策略。swivel是我制作的用于简化ServiceWorker和客户端交流的一个消息传递库。

逐步联网

我坚定地认为我之前提出的“缓存、联网、发送信息”是能够最大程度上使用ServiceWorker的策略。 ServiceWorker最强大的功能就是离线优先,虽然我更喜欢用其他名字称呼他,如逐步联网。事实上,ServiceWorker并不是真正的做到完全离线,因为ServiceWorker要被下载和运行。这意味着在初始页面加载的时候,服务端渲染仍旧是十分重要的。而这也同样意味着,没有什么比离线更加优先了。

就好像Jake Archibald坚持的,在那种几乎不能下载东西的低网速里,联网优先会给用户带来极差的体验。对于这种情况,超时是一个十分无用的策略,而及时地从缓存中获取所需的内容才是一种更好的策略。毕竟,逐步联网最强大的功能就是能够提供即时式的反馈。简单点来说,人们所期待的移动浏览器就是像原生APP一样的体验。(很快人们就会期待它会像桌面应用一样,因为传递性)。

更新过时的缓存内容

如果我们严格地执行逐步联网理念,则意味我们要总是去访问缓存,只要有需求,我们就要及时为其作出反应.无论什么需求,马上作出回应,返回的未必是最正确的结果,但是快速是最重要的。问题是,缓存的内容有可能过期了。于是在上周,我提出了以下策略。

  • 访问缓存,如果命中,马上返回。
  • 联网访问,即使我们已经在缓存中命中了所需的内容。
  • 使用联网后的响应,如果我们的缓存没有命中。
  • 如果用户已经命中了所需要的信息,那么我们在获得联网响应后只通知客户端需要更新的内容。

在此,我想着重讨论一下最后一点。 首先,我们需要界定一下什么需要更新。很不幸地,这大部分都是由应用程序自身所决定的。举个例子,一个图像编辑网站会认为图像时最重要的内容,因此会希望更新图像无论他们是否经过ServiceWorker的验证。而以我这个可怜的博客为例来研究,变化得最为频繁的内容是评论,还有偶尔的文章编辑。

在大部分的应用里,最重要更新的是视图。不幸的是,特定的应用-框架机制是解决这种问题最好的做法。

假设一个网页更新是采用json经过某种客户端利用,然后返回视图的方式。在我们目前的状况下,如果有一个激活的ServiceWorker拥有可以响应这一要求的缓存副本,那么我们几乎是可以瞬间反应。稍后,ServiceWorker会得到来自网络的响应并且更新缓存。尽管到了那时候,我们的客户端已经愉快的接受并渲染了(可能过时的)早前缓存的响应。 这就像我们曾经的做法一样。如果可行,缓存马上响应,否则,我们等待网路上的响应。一旦有新的网路响应获得,我们把它存在缓存里。

function queriedCache (cached) {
  var networked = fetch(request)
    .then(fetchedFromNetwork, unableToResolve)
    .catch(unableToResolve);
  return cached || networked;

  function fetchedFromNetwork (response) {
    var cacheCopy = response.clone();
    caches.open(version + 'pages').then(function add (cache) {
      cache.put(request, cacheCopy);
    });
    return response;
  }
}

当之前的缓存响应被更新的时候,我们需要重新配置ServiceWorker,让客户端得知。在fetchedFromNetwork的回调函数中,我们可以测试缓存是否真实,请求是否符合我们的需求,响应是否是JSON。这需要具体到个案研究,你可能需要用别的方法进行过滤,避免把GET api调用为视图更新。你可能也想检验一下你的响应是否有所不同,例如通过内容长度。利用这种方式,你就不会浪费用户的时间了。

var url = new URL(request.url);
var json = response.headers.get('Content-Type').indexOf('application/json') !== -1;
if (cached && url.origin === location.origin && json) {
  // let the client know we have an updated response
}

一个广义的做法是是按照视图和组件来进行维护,这比尝试模块化这些更新更为高效。因为这样在实施和未来维护上会更为省力。在用户体验方面,更多的针对性方案会更好。而对于这所有方法,用户测试才是王道。毕竟,你之所以阅读这篇文章,是因为你关心你的用户,并且让他们能够获得更有价值、快速、新鲜的内容。

另外要注意的是,你应该复制多一份响应消息的副本。原响应返回给客户端,第一份副本返回给缓存,利用第二份副本读取消息,并且把需要更新的消息传递给用户。你如何做到正确地传输呢?我们会在下一章节讲述这一部分。现在,我只会介绍我在swivel上简化过统一过的消息API。

在下面的代码里我假设你已经创造了一份响应的副本如 updateCopy = response.clone()。基于性能的原因,你应该提取相关逻辑并且弄清楚你是否需要实时通知你的客户端(更新),只有在他们需要的情况下你再进行克隆,减轻CPU的负荷。

var url = new URL(request.url);
var json = response.headers.get('Content-Type').indexOf('application/json') !== -1;
if (cached && url.origin === location.origin && json) {
  updateCopy.json().then(function parsed (data) {
    swivel.broadcast('view-update', request.url, data);
  });
}

在理想情况下,我们会单播需要更新的视图给请求的客户端而不是向每个客户端进行广播。可惜的是,这种获取特定客户端请求的机制尚未被浏览器所支持,只是ServiceWorker规范中的一部分。 在客户端,即网页端,我们可以注册一个swivel事件用于更新视图的。同样地,我们迟点将会深入讨论swivel如何工作。

navigator.serviceWorker
  .register('/service-worker.js')
  .then(navigator.serviceWorker.ready)
  .then(setupMessaging);

function setupMessaging () {
  swivel.on('view-update', function renderUpdate (context, href, data) {
    // use data to re-render view
  });
}

什么时候需要更新视图呢,这是取决于你如何实现的。如果你正在使用像React或者Taunus这种框架,那就很简单了。你只需要重新启用视图组件的模型然后让框架去渲染他。如果你需要更新的是服务端渲染的HTML,那就有点棘手了。因为更新后响应的也是HTML,并且当你的响应到达客户端时,JavaScript很可能尚未执行。这意味着,在这种情况下你必须要使用不同的机制去更新视图。

一种可能的解决办法是采取相反的做法:让你的客户端在JavaScript执行的第一时间询问ServiceWorker是否有缓存的更新。如果你的视图渲染框架足够灵活,你甚至可以在获得ServiceWorker的更新视图JSON后,重新调用renderUpdate的方法。

在客户端第一次加载页面的时候,你可以运行一下代码,告诉ServiceWorker你想更新你刚刚渲染的缓存内容。

swivel.emit('active-client', location.href);

ServiceWorker会听取这些信息并且回复相关页面的JSON。

swivel.on('active-client', function activeClient (context, href) {
  caches
    .match(href)
    .then(function (response) {
      return response.json();
    }).then(function (data) {
      context.reply('view-update', href, data);
    });
});

然后在客户端,按照顺序,将他们需要的变化渲染到视图上。要注意的是,你不需要实时更新你的视图。哪怕你是在使用React或者其余高效的虚拟DOM引擎。在只需要进行局部更新甚至是只是展示“有更多内容”的消息的情况下,更新整个页面会带来十分奇怪的用户体验。你有好多种选择去达成你的目的,例如让用户去点击消息来获取更新。

如果你有更好的想法,我会仔细聆听的。

w3ctech微信

扫码关注w3ctech微信公众号

共收到0条回复