玩转Service Worker生命周期

4,258 阅读9分钟

先来了解一下Service Worker

Service Worker简介及其注意事项

Service Worker 是浏览器在后台独立于网页运行的脚本,它打开了通向不需要网页或用户交互的功能的大门。 现在,它们已包括如推送通知和后台同步等功能。 将来,Service Worker 将会支持如定期同步或地理围栏等其他功能。

使用注意事项

  • 它是一种 JavaScript Worker,无法直接访问 DOM。 Service Worker 通过响应 postMessage 接口发送的消息来与其控制的页面通信,页面可在必要时对 DOM 执行操作。
  • Service Worker 是一种可编程网络代理,能够控制页面所发送网络请求的处理方式。
  • 在开发过程中,可以通过 localhost 使用 Service Worker,但如果要在网站上部署 Service Worker,则需要在服务器上设置 HTTPS。
  • Service Worker 在不用时会被中止,并在下次有需要时重启,因此,不能依赖 Service Worker onfetch 和 onmessage 处理程序中的全局状态。 如果存在需要持续保存并在重启后加以重用的信息,Service Worker 可以访问 IndexedDB API。

接下来先大概了解一下Service Wroker的生命周期

生命周期简介

Service Worker 的生命周期完全独立于网页。

安装:在安装过程中,通常需要缓存某些静态资产,如果某些资源已成功缓存,那么 Service Worker 就安装完毕。如果任何文件下载失败或缓存失败,那么安装步骤将会失败,Service Worker 就无法激活(也就是说, 不会安装)。如果发生这种情况,不必担心,它下次会再试一次。

激活:安装成功之后,接下来就是激活步骤,通常会在这个阶段管理旧缓存,下文会对这一阶段详细介绍。 页面控制:Service Worker 将会对其作用域内的所有页面实施控制,不过,首次注册该 Service Worker 的页面需要再次加载才会受其控制。服务工作线程实施控制后,它将处于以下两种状态之一:服务工作线程终止以节省内存,或处理获取和消息事件,从页面发出网络请求或消息后将会出现后一种状态。

以下是 Service Worker 初始安装时的简化生命周期:

我们先从第一次安装说起,首次安装经历的过程如下:

首次安装使用

可先查看Service Worker的使用模式

执行过程

  • install 事件是 Service Worker 获取的第一个事件,并且只发生一次。
  • 传递到 installEvent.waitUntil() 的一个 promise 可表明安装的持续时间以及安装是否成功。
  • 在成功完成安装并处于“活动状态”之前,Service Worker 不会收到 fetch 和 push 等事件。
  • 默认情况下,不会通过 Service Worker 提取页面,除非页面请求本身需要执行 Service Worker。因此,需要刷新页面以查看 Service Worker 的影响。
  • clients.claim() 可替换此默认值,并提前控制未控制的页面。

我们通过以下脚本注册service worker并且3秒后动态添加一张图片

<script>
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered!', reg))
    .catch(err => console.log('Boo!', err));

  setTimeout(() => {
    const img = new Image();
    img.src = '/dog.svg';
    document.body.appendChild(img);
  }, 3000);
</script>

Service Worker(sw.js)的代码如下:

self.addEventListener('install', event => {
  console.log('V1 installing…');

  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/cat.svg'))
  );
});

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

service worker在安装时缓存了一张猫的图片,并在请求 /dog.svg 时提供该图像,不过在你运行这个示例,首次加载时会看到小狗的图片,刷新一次后会看到猫的图片。

作用域和控制

Service Worker 注册的默认作用域是与脚本网址相对的 ./。这意味着如果在 //example.com/foo/bar.js 注册一个 Service Worker,则它的默认作用域为 //example.com/foo/。 通过 navigator.serviceWorker.controller(其将为 null 或一个 Service Worker 实例)检测客户端是否受控制。

下载、解析和执行

在调用 .register() 时,将下载第一个 Service Worker。如果脚本在初始执行中未能进行下载、解析,或引发错误,则注册器 promise 将拒绝,并舍弃此 Service Worker。

Chrome 的 DevTools 在控制台和应用标签的 Service Worker 部分中显示此错误:

Install Service Worker

获取的第一个事件为 install。该事件在 Worker 执行时立即触发,并且它只能被每个 Service Worker调用一次。如果更改Service Worker 脚本,则浏览器将其视为一个不同的 Service Worker,并且它将获得自己的 install 事件。将在后面对更新进行详细介绍。

在能够控制客户端之前,install 事件中可以缓存需要的所有内容。 event.waitUntil()一直等待promise完成,然后判断安装是否成功。如果 promise 拒绝,则表明安装失败,浏览器将丢弃 Service Worker。它将无法控制客户端。这意味着我们不能依靠 fetch 事件的缓存中存在的“cat.svg”。它是一个依赖项。

Activate

Service Worker 准备控制客户端并处理 push 和 sync 等功能事件时,将获得一个 activate 事件。但这不意味着调用 .register() 的页面将受控制。就像上面示例中展示的,即使Service Worker 激活很长时间后请求 dog.svg,它也不会处理此请求,仍会看到小狗的图像。默认值为 consistency。

clients.claim

激活 Service Worker 后,可以通过调用 clients.claim() 提起控制未受控制的页面。可以通过这个示例查看其作用,在查看这个示例时,先清除网站的缓存数据

其在 activate 事件中调用 clients.claim()。首先应该看到一只猫。说“应该”是因为这受时间约束。如果图像尝试加载之前,Service Worker 激活且 clients.claim() 生效,那么看到的是猫。

具体代码如下

self.addEventListener('install', event => {
  console.log('Installing…');
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('cat.svg'))
  );
});

self.addEventListener('activate', event => {
  clients.claim();
  console.log('Now ready to handle fetches!');
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  if (url.origin == location.origin && url.pathname.endsWith('/dog.svg')) {
    event.respondWith(caches.match('cat.svg'));
  }
});

更新 Service Worker

触发更新时机

  • 导航到一个作用域内的页面。
  • 更新 push 和 sync 等功能事件,除非在前 24 小时内已进行更新检查。
  • 调用 .register(),仅在 Service Worker 网址已发生变化时。

大多数浏览器检查已注册的 Service Worker 脚本的更新时,默认情况下都会忽略缓存标头。

更新过程

  • 如果 Service Worker 的字节与浏览器已有的字节不同,则考虑更新 Service Worker。
  • 更新的 Service Worker 与现有 Service Worker 一起启动,并获取自己的 install 事件。
  • 如果新 Worker 出现不正常状态代码(例如,404)、解析失败,在执行中引发错误或在安装期间被拒,则系统将舍弃新 Worker,但当前 Worker 仍处于活动状态。
  • 安装成功后,更新的 Worker会处于等待状态,直到现有 Worker 让出授权。(注意,在刷新期间客户端会重叠。)
  • self.skipWaiting()会跳过等待,Service Worker 在安装完后立即激活。

示例

我们更新上面的示例,在响应时使用马的图片而不是猫的图片。

const expectedCaches = ['static-v2'];

self.addEventListener('install', event => {
  console.log('V2 installing…');

  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(() => {
      console.log('V2 now ready to handle fetches!');
    })
  );
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

查看上述示例,你应该还是看见猫的图片,具体原因咱们往下看

更新过程如下:

Install

我已将缓存名称从 static-v1 更改为 static-v2。这意味着我可以设置新的缓存,而无需覆盖旧 Service Worker 仍在使用的当前缓存中的内容。

Waiting

成功安装 Service Worker 后,更新的 Service Worker 将延迟激活,直到现有 Service Worker 不再控制任何客户端。此状态称为“waiting”,这是浏览器确保每次只运行一个 Service Worker 版本的方式。 上述示例是因为 V2 Worker 尚未激活。在 DevTools 的“Application”标签中,会看到等待的新 Service Worker:

即使在演示中仅打开一个标签,刷新页面时也不会显示新版本。原因在于浏览器导航的工作原理。当导航时,在收到响应标头前,当前页面不会消失,即使此响应具有一个 Content-Disposition 标头,当前页面也不会消失。由于存在这种重叠情况,在刷新时当前 Service Worker 始终会控制一个页面。

要获取更新,需要关闭或退出使用当前 Service Worker 的所有标签。然后在查看这个示例就会看到马的图片。

Activate

旧 Service Worker 退出时将触发 Activate,新 Service Worker 将能够控制客户端。此时可以处理一些迁移数据库或清除缓存的工作。在上面的演示中,在 activate 事件中,删除了所有其他缓存,从而也移除了旧的 static-v1 缓存。

注意:不要更新以前的版本。它可能是许多旧版本的 Service Worker

将一个 promise 传递到 event.waitUntil(),它将缓冲功能事件(fetch、push、sync 等),直到 promise 进行解析。因此,fetch 事件触发时,激活已全部完成。

跳过等待阶段

可以通过调用 self.skipWaiting() 尽快将新 Service Worker 激活。

这会导致这次的 Service Worker 将当前活动的 Worker 逐出,并在进入等待阶段时尽快激活自己(或立即激活,前提是已经处于等待阶段)。这不能让 Worker 跳过安装,只是跳过等待阶段。

skipWaiting() 在等待期间调用还是在之前调用并没有什么不同。一般情况下是在 install 事件中调用它:

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

避免更改 Service Worker 脚本的网址

一定不要随意更改Service Worker的地址,这将会导致以下问题:

  1. index.html 将 sw-v1.js 注册为 Service Worker。
  2. sw-v1.js 缓存并提供 index.html,以实现离线优先。
  3. 更新 index.html,以便注册全新的 sw-v2.js。

执行上述操作,用户将永远无法获取 sw-v2.js,因为 sw-v1.js 将从其缓存中提供旧版本的 index.html。

注意:上面的那些示例只是为了演示作用

开发调试

Update on reload

这可使生命周期变得对开发者友好。每次浏览时都将:

  • 重新提取 Service Worker。
  • 即使字节完全相同,也将其作为新版本安装,这表示运行 install 事件并更新缓存。
  • 跳过等待阶段,以激活新 Service Worker。
  • 浏览页面。这意味着每次浏览时(包括刷新)都将进行更新,无需重新加载两次或关闭标签。

Skip waiting

强制重新加载页面 (shift-reload),则将完全绕过 Service Worker。页面将变得不受控制。此功能已列入规范,因此,它在其他支持 Service Worker 的浏览器中也适用。

通过api监听更新

Service Worker暴露了api可以监听更新状态变化,代码如下:

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // the installing worker, or undefined
  reg.waiting; // the waiting worker, or undefined
  reg.active; // the active worker, or undefined

  reg.addEventListener('updatefound', () => {
    // 当一个service worker安装时触发
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" 安装事件已经触发,但尚未完成
    // "installed" 安装完成
    // "activating" 激活事件已经触发,但还没有完成
    // "activated" 激活完成
    // "redundant" 丢弃。要么是安装失败,要么被新版本取代。

    newWorker.addEventListener('statechange', () => {
      // newWorker.state 变化时
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // 当service worker管控页面时触发
});