Do you use responsive images, lazy loading, or other optimizations? If not, why not? What affects your decisions?
Answer a few simple questions to help us understand your needs, and what we can do to make your life easier. Get Started

Service Worker 生命周期

Service Worker 的生命周期是最复杂的一环。如果您不了解它要做什么以及它有哪些优势,那么您会感觉它让您败下阵来。然而,一旦您明白它的工作原理,您就可以向用户提供几乎无法察觉的无缝更新,从而使网络和原生模式的优势为您所用。

这是一个深度教程,但每个章节开头的项目列表包含了您需要了解的大部分内容。

目的

Service Worker 生命周期的目的:

  • 实现离线优先。
  • 允许新 Service Worker 自行做好运行准备,无需中断当前的 Service Worker。
  • 确保整个过程中作用域页面由同一个 Service Worker(或者没有 Service Worker)控制。
  • 确保每次只运行网站的一个版本。

最后一点非常重要。如果没有 Service Worker,用户可以将一个标签加载到您的网站,稍后打开另一个标签。这会导致同时运行网站的两个版本。有时候这样做没什么问题,但如果您正在处理存储,那么,出现两个标签很容易会让您的操作中断,因为它们的共享的存储空间管理机制大相径庭。这可能会导致错误,更糟糕的情况是导致数据丢失。

第一个 Service Worker

简介:

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

选取以下 HTML:

<!DOCTYPE html>
An image will appear here in 3 seconds:
<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,并在 3 秒后添加一个小狗的图像。

下面是它的 Service Worker:sw.js:

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

  // cache a cat SVG
  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);

  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

它缓存一个小猫的图像,并在请求 /dog.svg 时提供该图像。不过,如果您运行上述示例,首次加载页面时您看到的是一条小狗。按 refresh,您将看到小猫。

注:猫比狗好。确实如此

作用域和控制

Service Worker 注册的默认作用域是与脚本网址相对的 ./。这意味着如果您在 //example.com/foo/bar.js 注册一个 Service Worker,则它的默认作用域为 //example.com/foo/

我们调用页面、Worker 和共享的 Worker clients。您的 Service Worker 只能控制位于作用域内的客户端。在客户端“受控制”后,它在提取数据时将执行作用域内的 Service Worker。您可以通过 navigator.serviceWorker.controller(其将为 null 或一个 Service Worker 实例)检测客户端是否受控制。

下载、解析和执行

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

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

Service Worker DevTools 标签中显示的错误

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 准备控制客户端并处理 pushsync 等功能事件时,您将获得一个 activate 事件。但这不意味着调用 .register() 的页面将受控制。

首次加载此演示时,即使在 Service Worker 激活很长时间后请求 dog.svg,它也不会处理此请求,您仍会看到小狗的图像。默认值为 consistency,如果在页面加载时不使用 Service Worker,那么也不会使用它的子资源。如果您第二次加载此演示(换言之,刷新页面),该页面将受控制。页面和图像都将执行 fetch 事件,您将看到一只猫。

clients.claim

激活 Service Worker 后,您可以通过在其中调用 clients.claim() 控制未受控制的客户端。

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

如果您使用 Service Worker 加载页面的方式与通过网络加载页面的方式不同,clients.claim() 会有些棘手,因为您的 Service Worker 最终会控制一些未使用它加载的客户端。

注:我看到很多人添加 clients.claim() 作为样板文件,但我自己很少这么做。该事件只是在首次加载时非常重要,由于渐进式增强,即使没有 Service Worker,页面也能顺利运行。

更新 Service Worker

简介:

  • 以下情况下会触发更新:
    • 导航到一个作用域内的页面。
    • 更新 pushsync 等功能事件,除非在前 24 小时内已进行更新检查。
    • 调用 .register()仅在 Service Worker 网址已发生变化时。
  • 大部分浏览器(包括 Chrome 68 和更高版本)在检查已注册的 Service Worker 脚本的更新时,默认情况下都会忽略缓存标头。在通过 importScripts() 提取 Service Worker 内加载的资源时,它们仍会遵循缓存标头。您可以在注册 Service Worker 时,通过设置 updateViaCache 选项来替换此默认行为。
  • 如果 Service Worker 的字节与浏览器已有的字节不同,则考虑更新 Service Worker。(我们正在扩展此内容,以便将导入的脚本/模块也包含在内。)
  • 更新的 Service Worker 与现有 Service Worker 一起启动,并获取自己的 install 事件。
  • 如果新 Worker 出现不正常状态代码(例如,404)、解析失败,在执行中引发错误或在安装期间被拒,则系统将舍弃新 Worker,但当前 Worker 仍处于活动状态。
  • 安装成功后,更新的 Worker 将 wait,直到现有 Worker 控制零个客户端。(注意,在刷新期间客户端会重叠。)
  • self.skipWaiting() 可防止出现等待情况,这意味着 Service Worker 在安装完后立即激活。

假设我们已更改Service Worker脚本,在响应时使用马的图片而不是猫的图片:

const expectedCaches = ['static-v2'];

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

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

self.addEventListener('activate', event => {
  // delete any caches that aren't in expectedCaches
  // which will get rid of static-v1
  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);

  // serve the horse SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

注:我对马没有什么强烈的看法。

查看上面的演示。您应还会看到一个猫的图像。原因是…

Install

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

就像本机应用会为其可执行文件绑定资源那样,此模式会创建特定于版本的缓存。您可能还有不属于版本特定的缓存,如 avatars

Waiting

成功安装 Service Worker 后,更新的 Service Worker 将延迟激活,直到现有 Service Worker 不再控制任何客户端。此状态称为“waiting”,这是浏览器确保每次只运行一个 Service Worker 版本的方式。

如果您运行更新的演示,您应仍会看到一个猫的图片,因为 V2 Worker 尚未激活。在 DevTools 的“Application”标签中,您会看到等待的新 Service Worker:

DevTools 显示等待的新 Service Worker

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

要获取更新,需要关闭或退出使用当前 Service Worker 的所有标签。然后,当您再次浏览演示时,您看到的应该是一匹马。

此模式与 Chrome 更新的方式相似。Chrome 的更新在后台下载,但只有在 Chrome 重启后才能生效。在此期间,您可以继续使用当前版本而不会受干扰。不过,这在开发期间却是个难题,但 DevTools 为我们提供了可简化它的方法,本文后面会进行介绍

Activate

旧 Service Worker 退出时将触发 Activate,新 Service Worker 将能够控制客户端。此时,您可以执行在仍使用旧 Worker 时无法执行的操作,如迁移数据库和清除缓存。

在上面的演示中,我维护了一个期望保存的缓存列表,并且在 activate 事件中,我删除了所有其他缓存,从而也移除了旧的 static-v1 缓存。

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

跳过等待阶段

等待阶段表示您每次只能运行一个网站版本,但如果您不需要该功能,您可以通过调用 self.skipWaiting() 尽快将新 Service Worker 激活。

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

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

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

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

但是,您可能想在对 Service Worker 发出 postMessage() 时调用它。例如,在用户交互后您想要 skipWaiting()

下面是一个使用 skipWaiting() 的演示。无需离开您就应能看到一头牛的图片。与 clients.claim() 一样,它是一个竞态,因此,如果新 Service Worker 在页面尝试加载图像前提取数据、安装并进行激活,那么,您将只会看到牛。

手动更新

如前所述,在执行导航和功能事件后,浏览器将自动检查更新,但是您也可以手动触发更新:

navigator.serviceWorker.register('/sw.js').then(reg => {
  // sometime later…
  reg.update();
});

如果您期望用户可以长时间使用您的网站而不必重新加载,您需要按一定间隔(如每小时)调用 update()

避免更改 Service Worker 脚本的网址

如果您读过我的一篇有关缓存最佳做法的博文,您可能会考虑为每个 Service Worker 版本提供唯一的网址。请一定不要这么做!这种做法并不适用于 Service Worker,您只需在其当前位置更新脚本即可。

它将给您带来如下问题:

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

如果您执行上述操作,用户将永远无法获取 sw-v2.js,因为 sw-v1.js 将从其缓存中提供旧版本的 index.html。因此,您将自己置于这样的境地:您需要更新 Service Worker 才能更新 Service Worker。这真得很让人讨厌。

不过,对于上面的演示,我更改 Service Worker 的网址。这样做是为了进行演示,让您可以在版本间进行切换。在生产环境中我不会这么做。

让开发更简单

Service Worker 生命周期是专为用户构建的,这就给开发工作带来一定的困难。幸运的是,我们可通过以下几个工具解决这个问题:

Update on reload

这是我最喜欢的工具。

DevTools 显示“update on reload”

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

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

Skip waiting

DevTools 显示“skip waiting”

如果您有一个 Worker 在等待,您可以按 DevTools 中的“skip waiting”以立即将其提升到“active”。

Shift-reload

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

处理更新

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', () => {
    // A wild service worker has appeared in reg.installing!
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - the install event has fired, but not yet complete
    // "installed"  - install complete
    // "activating" - the activate event has fired, but not yet complete
    // "activated"  - fully active
    // "redundant"  - discarded. Either failed install, or it's been
    //                replaced by a newer version

    newWorker.addEventListener('statechange', () => {
      // newWorker.state has changed
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // This fires when the service worker controlling this page
  // changes, eg a new worker has skipped waiting and become
  // the new active worker.
});

您成功了!

真是太棒了!这里介绍了许多技术理论。未来数周,我们将深入介绍上面的一些实用的应用,敬请关注!

反馈

Was this page helpful?
Yes
What was the best thing about this page?
It helped me complete my goal(s)
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It had the information I needed
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It had accurate information
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It was easy to read
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
Something else
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
No
What was the worst thing about this page?
It didn't help me complete my goal(s)
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It was missing information I needed
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It had inaccurate information
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It was hard to read
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
Something else
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.