【Service Worker】生命周期那些事儿

文章来自 个人博客 html

生命周期是 Service Worker 中比较复杂的一部分,若是你不知道它什么时间将要作什么,以及它带来的好处,那么你可能会有一种感受:就是它一直在和你较劲。若是理解它的工做机制,你就能够给用户提供完美的,无感知的更新体验。 这篇文章是 Chrome 团队最近总结的一片文章,配合例子讲述生命周期,让咱们更容易理解,也解决了我以前开发中遇到的一些困惑,因此决定翻译出来。此处 阅读原文 git

目的

本文中介绍利用生命周期能够实现的功能大概有以下几点:github

  • 实现缓存优先(offline-first)web

  • 在不打断现有 SW 的状况下,准备好一个新的 SW浏览器

  • 让注册 SW 的页面同一时间只归属同一个 SW 控制缓存

  • 确保你的网站只有一个版本在运行app

最后一点尤为重要,通常状况下(没有 SW 的状况),用户浏览你的网站时可能先打开一个 tab,过了一下子又打开了一个 tab,结果就是在同一时间,你的页面运行了两个版本,大部分时候,这样是没问题的,可是若是你使用了缓存,那么两个 tab 就要面临如何管理缓存的问题,若是处理很差,它可能会形成异常,严重的形成数据丢失。svg

用户很是不喜欢数据丢失,这会让他们很是忧桑。工具

第一个 Service Worker

  • install 事件是 SW 触发的第一个事件,而且仅触发一次。fetch

  • installEvent.waitUntil()接收一个 Promise 参数,用它来表示 SW 安装的成功与否。

  • SW 在安装成功并激活以前,不会响应 fetchpush等事件。

  • 默认状况下,页面的请求(fetch)不会经过 SW,除非它自己是经过 SW 获取的,也就是说,在安装 SW 以后,须要刷新页面才能有效果。

  • clients.claim()能够改变这种默认行为。

举个例子:

<!DOCTYPE html>
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>

这里注册了一个 SW,而且在 3 秒后在页面上添加一个图片。

这是 SW 的代码:

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

  // 这里缓存一个 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);

  //若是是同域而且请求的是 dog.svg 的话,那么返回 cat.svg
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

这里 SW 缓存了 cat.svg,并当请求 dog.svg 的时候返回它。然而,当你运行 这个例子 的时候,你会发现第一次出现一只狗,刷新后,猫才会出现。

做用域与控制

SW 的默认做用域为基于当前文件 URL 的 ./。意思就是若是你在//example.com/foo/bar.js里注册了一个 SW,那么它默认的做用域为 //example.com/foo/

咱们把页面,workers,shared workers 叫作clients。SW 只能对做用域内的clients有效。一旦一个client被“控制”了,那么它的请求都会通过这个 SW。咱们能够经过查看navigator.serviceWorker.controller是否为 null 来查看一个client是否被 SW 控制。

下载-解析-执行

当你调用.register()的时候,第一个 SW 被下载下来,这过程当中若是下载,解析或者在初始化中有错误的话,那么 register 的Promise 会返回 reject,而后 SW 会被销毁。

Chrome 开发者工具会展现出来错误,在 Application 中的 Service Workers Tab:

alt

安装(install)

SW 首先会触发install,每一个 SW 只会被触发一次,当你修改你的 SW 后,浏览器会认为这是一个新的 SW,从而会再触发这个新 SW 的install事件,在后面会详细说到。

install是在 SW 控制 clients以前处理缓存很好的时机。在 event.waitUntil()传入的 Promise 会让浏览器知道 SW 何时安装成功以及是否成功。

当 Promise reject 的时候,表明着安装失败,浏览器将这个 SW 废弃掉,不会控制任何 clients。

激活(Activate)

安装成功后并激活(activate)成功后,SW 就能够处理“功能性的事件“了,好比push,sync但这并不表明调用.register()的页面会当即生效。

第一次你请求 这个demo 的时候,虽然在 SW 被激活后好久才请求了dog.svg(由于这里等待了三秒),但 SW 也并无处理这个请求,结果你看见了一只狗。当你第二次请求的时候,也就是刷新页面,这时请求被处理了,当前页面和图片都通过了 SW 的 fetch事件,因此你看见了一只猫。

clients.claim

你能够在activate事件中经过调用clients.claim()来让没被控制的 clients 受控。

好比 这个demo ,可能第一次你就会看见一只猫,这里我说“可能”,是由于这时时间敏感的,仅当 SW 激活而且clients.claim()被调用成功在图片请求以前的时候才能够。

因此,可想而知,当你用 SW 加载与正常请求不一样资源的时候(好比上面的例子),那用clients.claim()可能会遇到一些问题,这时有些资源可能不会经过你的SW。

我见过不少人在代码中把clients.claim()当作了必选项,但我本身不多这样作,由于仅仅是第一次加载不会经过 SW,并且页面仍是都会正常运行的。

更新 Service Worker

简单来讲:

  • 触发更新的几种状况:

    • 第一次导航到做用域范围内页面的时候

    • 当在24小时内没有进行更新检测而且触发功能性时间如pushsync的时候

    • SW 的 URL 发生变化并调用.register()

  • 当 SW 代码发生变化,SW 会作更新(还将包括引入的脚本)

  • 更新后的 SW 会和原始的 SW 共同存在,并运行它的install

  • 若是新的 SW 不是成功状态,好比 404,解析失败,执行中报错或者在 install 过程当中被 reject,它将会被废弃,以前版本的 SW 仍是激活状态不变。

  • 一旦新 SW 安装成功,它会进入wait状态直到原始 SW 不控制任何 clients。

  • self.skipWaiting()能够阻止等待,让新 SW 安装成功后当即激活。

<div style="max-width:600px;">

<iframe id="iframe-2" onload="iframeLoaded('iframe-2')" style="width:100%;border:none;" src="https://www.zhengqingxin.com/static/demo/5-lifecycle/iframe2.html">
</iframe>

</div>

下面咱们来举个 SW 更新的例子,这是一个将猫变成马的故事:

const expectedCaches = ['static-v2'];

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

  // 将 horse.svg 缓存在新的缓存 static-v2 中
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  // 删除额外的缓存,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);

  //若是是同域而且请求的是 dog.svg 的话,那么返回 horse.svg

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

若是你打开 这个 demo ,你会看到一只喵,缘由是这样的...

install

注意这里咱们将缓存从 static-v1 换到了 static-v2,这表明了我用了一个新的缓存空间覆盖了以前 SW 正在使用的缓存。

这里新建了一块缓存的作法相似于原生 app 中将每块资源打包到一块指定的执行空间的作法,有时候结合实际状况,你也能够不这么作。

Waiting

一旦新 SW 安装成功,它会进入wait状态直到原始 SW 不控制任何 clients。这个状态是waiting,这也是浏览器确保在同一时间只有一个版本的 SW 运行的机制。

若是你再次打开 这个 demo ,你仍是会看到一只喵,由于新的 SW 仍是没有被激活,在开发者工具里你依然看到它是 waiting 状态。

alt

尽管这个例子中你仅打开了一个 tab,但刷新页面并无用,这是因为浏览器自己的机制,当你刷新的时候,当前页面不会离开,直到收到了一个响应头,并且即便这样,若是响应中包含Content-Disposition的话,当前页面仍是不会离开。因为这个时间上的重叠,在刷新的时候当前的 SW 老是控制了一个 client。

为了让 SW 更新,你须要把全部用原始 SW 的页面 tab 关闭或者跳转走,这时你再访问 这个 demo ,你就会看到了一匹野马。

这种机制相似于 Chrome 自己的更新机制,Chrome 在后台更新,只有当你重启浏览器的时候才会生效,在这期间你不会被打扰,能够继续使用当前版本。然而,这样可能会使咱们开发者比较痛苦,好在开发者工具帮咱们解决了这个事情,后面会说到。

Activate

Activate 在旧的 SW 离开时会被触发,这时新的 SW 能够控制 clients。这时候你能够作一些在老 SW 运行时不能作的事情,好比清理缓存。

在上面的例子中,以前保留的缓存,在activate时间执行的时候被清理掉。

这里最好不要更新之前的版本,而是直接分配新的缓存空间。

若是你在event.waitUntil()中传入了一个 Promise,SW 将会缓存住功能性事件(fetch,push,sync等等),直到 Promise 返回 resolve 的时候再触发,也就是说,当你的fetch事件被触发的时候,SW 已经被彻底激活了。

cache storage API 和 localStorage,IndexedDB 同样是“同域”的。若是你在一个父域下运行多个网站,好比 yourname.github.io/myapp,这就要当心你不要把别的网站的缓存删掉了。避免这个问题,你能够将 cache 的 key 设的具备惟一性,好比 myapp-static-v1 而且约束不要碰不以 myapp- 开头的缓存。

skipWaiting

waiting 意在让你的网站同一时间只有一个 SW 在运行,但若是你不想要这样的话,你能够经过调用self.skipWaiting()来让新 SW 当即激活。

这么作会让你的新 SW 踢掉旧的,而后当它变为 waiting 状态时当即激活,注意这里不会跳过 installing,只会跳过 waiting。

在 waiting 以前或者以后调用skipWaiting()均可以,通常状况咱们在 install 事件中调用:

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

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

这个例子中,你可能直接能够看到一只奶牛,和clients.claim()同样,这是一场赛跑,仅当你的新 SW 安装,激活等早于你请求图片时,奶牛才会出现。

skipWaiting()意味着新 SW 控制了以前用旧 SW 获取的页面,也就是说你的页面有一部分资源是经过旧 SW 获取,剩下一部分是经过新 SW 获取的,若是这样作会给你带来麻烦,那就不要用skipWaiting(),这点咱们应该根据具体状况评估。

手动更新

像我以前说的,当页面刷新或者执行功能性事件时,浏览器会自动检查更新,其实咱们也能够手动的来触发更新:

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

若是你但愿你的用户访问页面很长时间并且不用刷新,那么你能够每一个一段时间调用一次update()

避免改变 SW 的 URL

若是你看过个人文章缓存最佳实践,你可能会考虑给每一个 SW 不一样的 URL。千万不要这么作!在 SW 中这么作是“最差实践”,要在原地址上修改 SW。

举个例子来讲明为何:

1.index.html注册了sw-v1.js做为SW。

2.sw-v1.jsindex.html作了缓存,也就是缓存优先(offline-first)。

3.你更新了index.html从新注册了在新地址的 SW sw-v2.js.

若是你像上面那么作,用户永远也拿不到sw-v2.js,由于index.htmlsw-v1.js缓存中,这样的话,若是你想更新为sw-v2.js,还须要更改原来的sw-v1.js

在上面的 demo 里,我给每一个 SW 用了不一样的 URL,这只是为了作演示,不要在生产环境中这么作。

让开发更简单

SW 的生命周期是为了用户构建的,但这样不免让咱们开发带来一些烦恼,幸好与一些工具来帮助咱们。

Update on reload

这是我最喜欢的:

alt

这样把生命周期变得对开发友好了,每次跳转将会:

1.从新获取 SW

2.尽管字节一致,也会从新安装,也就是说install事件被执行而且更新缓存。

3.跳过 waiting,激活新的 SW。

4.导航到这个页面。

这就是说你每次操做都会更新而不用刷新页面或者关闭 tab。

Skip Waiting

alt
若是你有个 SW 在等待状态,你能够点击 skipWaiting 让它当即变为激活状态。

强制刷新

若是你强制刷新页面,那么会绕过 SW,变成不受控,这个功能已被定为规范,因此在其余支持 SW 的浏览器中也适用。

处理更新

Service Worker 是 可扩展web 的一部分。想法初衷是,做为浏览器开发者,有时候咱们可能不如 web 开发者更了解 web,因此,咱们其实不该该提供仅仅能够解决具体问题的 API,而是应该给 web 开发者更多的权限从而更好的解决问题。

因此,咱们尽量的开放更多,SW 整个生命周期都是可查看的:

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // 安装中的 SW,或者是undefined
  reg.waiting; // 等待中的 SW,或者是undefined
  reg.active; // 激活中的 SW,或者是undefined

  reg.addEventListener('updatefound', () => {
    // 正在安装的新的 SW
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - 安装事件被触发,但还没完成
    // "installed"  - 安装完成
    // "activating" - 激活事件被触发,但还没完成
    // "activated"  - 激活成功
    // "redundant"  - 废弃,多是由于安装失败,或者是被一个新版本覆盖

    newWorker.addEventListener('statechange', () => {
      // newWorker 状态发生变化
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
    // 当 SW controlling 变化时被触发,好比新的 SW skippedWaiting 成为一个新的被激活的 SW
});
相关文章
相关标签/搜索