Jason Pan

Service Workers 及其生命周期和使用场景

潘忠显 / 2021-04-08


“JavaScript 工作原理”系列文章是翻译和整理自 SessionStack 网站的 How JavaScript works。因为博文发表于2017年,部分技术或信息可能已经过时。本文英文原文链接,作者 Alexander Zlatkov,翻译 潘忠显

这是致力于探索JavaScript及其构建组件的系列文章的第 8 篇。

This is post # 8 of the series dedicated to exploring JavaScript and its building components. In the process of identifying and describing the core elements, we also share some best practice we use when building SessionStack, a JavaScript application that has to be robust and highly-performant in order to show you real-time exactly how your users ran into a technical or UX issue in your web app.

img

您可能已经知道,渐进式 Web 应用 (Progressive Web Apps) 旨在使Web应用程序更加顺畅,能让用户体验到似本机应用程序的体验,而不仅仅是浏览器的外观和感受,因此它可能会变得越来越流行。

构建渐进式 Web 应用程序需要在不确定或者不存在网络的条件下都可用,既在网络和负载方面都要求非常可靠

概览

如果您想了解有关 Service Workers 的内容,则应先阅读我们之前关于 Web Workers 的文章。

基本上,Service Worker 是 Web Worker 的一种,更具体地说,它就像共享 worker (Shared Worker):

Service Worker API 如此令人兴奋的主要原因之一是:它允许您的 Web 应用程序支持脱机体验,从而使开发人员可以完全控制流程。

One of the main reasons why the Service Worker API is so exciting is that it allows your web apps to support offline experiences, giving developers complete control over the flow.

Service Worker 的生命周期

Service Worker 的生命周期与网页完全分开。它包括以下几个阶段:

下载

这个阶段,浏览器下载 .js 文件,其中包含了 Service Worker。

安装

要为 Web 应用安装 Service Worker,首先需要在 JavaScript 中进行注册,然后它会提示浏览器在后台启动Service Worker安装步骤。

通过注册 Service Worker,您可以告诉浏览器 Service Worker JavaScript 文件所在的位置。让我们看下面的代码:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/sw.js').then(function(registration) {
      // Registration was successful
      console.log('ServiceWorker registration successful');
    }, function(err) {
      // Registration failed
      console.log('ServiceWorker registration failed: ', err);
    });
  });
}

该代码检查当前环境中是否支持 Service Worker API。如果支持,则注册 /sw.js Service Worker。

您无需担心浏览器是否已将 service worker 注册,可以在每次加载页面时都调用 register() 方法,浏览器能进行适当的处理。

register() 方法的一个重要细节是 service worker 文件位置。在上边的示例中,您可以看到服务工作者文件位于域的根目录。这意味着 service worker 的范围将是整个来源。换句话说,该 service worker 将收到此域中所有内容的 fetch 事件(我们将在后面讨论)。如果我们在 /example/sw.js 上注册 service worker 文件,则服务工作者只会捕获到 URL 以 /example/ 开头的页面的 fetch 事件(如 /example/page1/, /example/page2/ 等)。

在安装阶段,最好加载并缓存一些静态资源。成功缓存后,Service Worker 安装就完成了。如果因加载失败而没有完成安装,service worker 将进行重试。成功安装后,静态资源就在缓存中了。

是否需要在加载事件之后进行注册?这不是必须的,但绝对推荐。当用户首次访问您的网络应用,还没有 service worker,浏览器也无法提前知道是否会安装 service worker。如果安装了 Service Worker,则浏览器将需要为此额外的线程花费更多的 CPU 和内存,否则浏览器将花费在网页渲染上。

如果您仅在页面上安装 Service Worker,由于无法尽快将页面提供给用户,会存在着延迟加载和呈现的风险。

请注意,这仅仅对首页访问很重要,后续页面访问不会受到 Service Worker 安装的影响。首次访问页面时激活了 Service Worker 后,它就可以处理加载/缓存事件,以供您以后访问您的 Web 应用程序。这点很有意义,因为它需要准备好处理有限的网络连接。

激活

安装 Service Worker 之后,下一步就是激活它。此步骤是管理以前的缓存的绝好机会。

After the Service Worker is installed, the next step will be its activation. This step is a great opportunity to manage previous caches.

激活后,service worker 将开始控制其范围内的所有页面。一个有趣的事实:首次注册Service Worker 的页面将不会受到控制,除非该页面再次被加载。一旦 Service Worker 处于控制状态,它将处于以下状态之一:

Once the Service Worker is in control, it will be in one of the following states:

生命周期如下所示:

img

处理 Service Worker 内部的安装

Handling the installation inside the Service Worker

页面启动注册过程后,Service Worker 脚本通过向 Service Worker 实例添加事件侦听器来处理 install 事件。以下是处理 install 事件时需要采取的步骤:

这是一个简单的安装,在 Service Worker 中可能看起来像这样:

var CACHE_NAME = 'my-web-app-cache';
var urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/app.js',
  '/scripts/lib.js'
];

self.addEventListener('install', function(event) {
  // event.waitUntil takes a promise to know how
  // long the installation takes, and whether it 
  // succeeded or not.
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

如果所有文件都已成功缓存,则将安装service worker。如果其中任何文件下载失败,则安装步骤将失败。因此,需要小心此处放置的文件。

处理 install 事件完全是可选的,您也可以绕过它。在这种情况下,您无需执行此处的任何步骤。

运行时缓存请求

这部分是真实的场景,将展示如何拦截请求并返回创建的缓存,以及创建新的缓存。

在安装 Service Worker 之后,如果用户导航到另一个页面或刷新他所在的页面时,Service Worker 将收到 fetch 事件。请看示例:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    // This method looks at the request and
    // finds any cached results from any of the
    // caches that the Service Worker has created.
    caches.match(event.request)
      .then(function(response) {
        // If a cache is hit, we can return thre response.
        if (response) {
          return response;
        }

        // Clone the request. A request is a stream and
        // can only be consumed once. Since we are consuming this
        // once by cache and once by the browser for fetch, we need
        // to clone the request.
        var fetchRequest = event.request.clone();
        
        // A cache hasn't been hit so we need to perform a fetch,
        // which makes a network request and returns the data if
        // anything can be retrieved from the network.
        return fetch(fetchRequest).then(
          function(response) {
            // Check if we received a valid response
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            // Cloning the response since it's a stream as well.
            // Because we want the browser to consume the response
            // as well as the cache consuming the response, we need
            // to clone it so we have two streams.
            var responseToCache = response.clone();

            caches.open(CACHE_NAME)
              .then(function(cache) {
                // Add the request to the cache for future queries.
                cache.put(event.request, responseToCache);
              });

            return response;
          }
        );
      })
    );
});

概括以下发生了什么:

由于请求和响应是流 (streams),流的主体只能使用一次,我们和浏览器都要使用它们,因此请求和响应必须被克隆。

更新 Service Worker

当用户访问您的 Web 应用程序时,浏览器将尝试重新下载包含您的 Service Worker 代码的 .js 文件。该过程发生在后台。

如果现在下载的 Service Worker 文件与当前 Service Worker 文件相比有差异,哪怕只有一个字节的差异,则浏览器将假定存在更改,并且必须启动新的 Service Worker。

新的 Service Worker 将被启动,并且将触发 install 事件。与此同时,旧的 Service Worker 仍在控制 Web 应用程序的页面,这意味着新的 Service Worker 将进入 waiting 状态。

一旦 当前已打开 Web 应用页面被关闭,旧的 Service Worker 将被浏览器杀死,新安装的 Service Worker 将获得完全控制权。这时,会触发 activate 事件。

为什么需要以上的这些动作?为了避免在不同的选项卡中,同时运行两个版本的 Web应用程序。实际上这种情况很常见,并且可能会造成非常严重的错误。例如,在浏览器本地存储数据时具有不同结构 (schema) 的情况。

删除缓存数据

activate 回调中最常见的步骤是缓存管理 (cache management)。如果要在 install 步骤中清除所有旧的缓存,旧的 Service Worker 将突然停止提供该缓存中的文件的功能。

通过以下示例,您可以从缓存中删除一些未列入白名单的文件(在这种情况下,其名称下具有 page-1 page-2):

self.addEventListener('activate', function(event) {

  var cacheWhitelist = ['page-1', 'page-2'];

  event.waitUntil(
    // Retrieving all the keys from the cache.
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        // Looping through all the cached files.
        cacheNames.map(function(cacheName) {
          // If the file in the cache is not in the whitelist
          // it should be deleted.
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

HTTPS 要求

在构建 Web 应用程序阶段,开发者可以通过 localhost 使用 Service Worker;但是一旦将其部署到生产环境中,就需要准备好 HTTPS。

利用 Service Worker,可以劫持连接并构造响应。如果不使用 HTTPS,Web 应用程序容易受到中间人攻击。为了提高安全性,您必须在通过 HTTPS 投放的网页上注册 Service Worker,这样才能知道浏览器收到的 Service Worker 在通过网络传播时并未被修改。

浏览器支持

浏览器对 Service Workers 的支持越来越好:

img

您可以点击这里,关注所有浏览器的最新支持进度。

Service Worker 的强大功能

Service Worker 提供的一些独特功能包括:

在本系列的以后的博客文章中,将逐一进行详细讨论。

Resources