ServiceWorker生命周期,一个调皮的孩子

1,513 阅读9分钟

前言

本文主要讲ServiceWorker的生命周期,以解决在阅读MDN官网上描述Registering your worker的示例中碰到的一点疑惑,以及为什么ServiceWorker多了一个waiting状态;请大家指正。 articles/2023-03-11-ServiceWorker生命周期(一个调皮的孩子).md at main · slshsl/articles (github.com)

ServiceWorker三剑客

本段主要通过注册ServiceWorker来引出ServiceWorkerContainerServiceWorkerRegistrationServiceWorker这3个接口。

假如通过一个register.js文件来注册ServiceWorker,代码如下(下面的代码主要是为了更好的观察serviceWorker的状态变化):

if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("./sw.js").then((registration) => {    
    registration.addEventListener("updatefound", () => {
      let curUpdateWorker = registration.installing;
      console.log(`当前更新的serviceWorker目前的状态为${curUpdateWorker.state}`);
      curUpdateWorker.addEventListener("statechange", (e) => {        
        console.log(`当前更新的serviceWorker目前的状态变更为${e.target.state}`);
        if (e.target.state === "actived") {
          console.log("更新完成,已接管页面.");
        }
      });
    });
  });
} else {
  console.error("Service worker are not supported!");
}

在上面代码中navigator.serviceWorker是一个ServiceWorkerContainer的实例,可以简单理解为serviceWorker的容器或者工厂,以下用swC来表示它的实例。

通过swCregister方法注册一个ServiceWorker,该方法返回一个promise,该promise resolve的返回值是一个ServiceWorkerRegistration的实例,对应上面代码中的registration,以下就用swR来表示ServiceWorkerRegistration的实例。

ServiceWorker是一个继承EventTarget的类。以下用sw来表示它的实例。我们可以从swC.controller(当前页面激活的sw)或者swR.installingswR.waitingswR.activate来获得处于不同状态的sw

注册过程

先简单说一下sw的几个状态,以方便讲解其注册过程,详细的状态后面会提到;swstate属性保存了sw的状态,主要有:

  • installing:正在安装
  • installed:安装完成
  • activated:已激活

当第一次注册的过程中会触发 swRupdatefound事件,在该事件中,我们可以从swR.installing拿到正在安装的sw(该sw此时状态为installing)并注册该swstateChange事件;此时swR.waitingswR.activate都为null;观察sw状态最后变为activated(先暂时不描述状态变化的详细过程),此时swR.installingswR.waiting都为null,swR.activate为我们之拿到的sw

概述来讲,注册的sw会随着它的状态的变化,不断的在swRinstallingwaitingactivate这三个属性中擦肩而过;当sw.stateinstalling时,它跑到了swR.installing这里,当sw.stateinstalled时,它跑到了swR.waiting那里,当sw.stateactivated时,停在了swR.activate;就像一个调皮的孩子玩累了回到了家。这时该sw已经控制了后续的事件。

在MDN web官网上描述Registering your worker的部分是我想写这篇文章的原因,当我看到它的示例代码时,代码如下:

    const registerServiceWorker = async () => {
      if ("serviceWorker" in navigator) {
        try {
          const registration = await navigator.serviceWorker.register("/sw.js", {
            scope: "/",
          });
          if (registration.installing) {
            console.log("Service worker installing");
          } else if (registration.waiting) {
            console.log("Service worker installed");
          } else if (registration.active) {
            console.log("Service worker active");
          }
        } catch (error) {
          console.error(`Registration failed with ${error}`);
        }
      }
    };

    // …

    registerServiceWorker();

上面的代码容易让人觉得这三个属性的值不会同时存在,也许官网以此例子来讲解sw的状态变化;至于如何注册sw,在MDN web官网上描述ServiceWorkerContainer中给出的注册sw更为合适,代码如下:

if ("serviceWorker" in navigator) {
  // Register a service worker hosted at the root of the
  // site using the default scope.
  navigator.serviceWorker
    .register("/sw.js")
    .then((registration) => {
      console.log("Service worker registration succeeded:", registration);

      // At this point, you can optionally do something
      // with registration. See https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
    })
    .catch((error) => {
      console.error(`Service worker registration failed: ${error}`);
    });

  // Independent of the registration, let's also display
  // information about whether the current page is controlled
  // by an existing service worker, and when that
  // controller changes.

  // First, do a one-off check if there's currently a
  // service worker in control.
  if (navigator.serviceWorker.controller) {
    console.log(
      "This page is currently controlled by:",
      navigator.serviceWorker.controller
    );
  }

  // Then, register a handler to detect when a new or
  // updated service worker takes control.
  navigator.serviceWorker.oncontrollerchange = () => {
    console.log(
      "This page is now controlled by",
      navigator.serviceWorker.controller
    );
  };
} else {
  console.log("Service workers are not supported.");
}

可能是我又较真了,人家不就是给个例子嘛;是的,所以不要在意,正好也借此深入了解。

更新过程

在第一次注册完成后,当前存在activatedsw;此时,假设我们改变了注册的sw.js文件中的内容,这时浏览器在拿到新的sw.js文件后,内部去diff是否需要更新sw。更新过程也会触发swRupdatefound事件。与第一次注册过程中不同的是,新的sw的状态变化到installed(安装完成)后,就一直等待,因为当前存在activatedsw,无法进入下面的状态。

这时swR.waitingswR.activate上都有值,swR.waiting对应着新的swswR.activate对应着老的sw

然后需要关闭该页面(此时刷新页面没有用,原因后面或则下一篇文章会提到,也可以参考下web.dev中Service workers这篇文章的这个段落,简单来讲就是浏览器从一个旧的过渡到新的sw是平滑的过度),然后再打开,新的sw才会过渡到activated状态,接管后续的事件。

当然有解决方案可以实现刷新页面就可以使新的sw激活(通过self.skipWaiting()swCcontrollerchange事件回调实现),也有解决方案可以实现不刷新页面也可以使新的sw激活(通过self.skipWaiting()clients.claim()实现);后续也许会单独出一章节来详细讲解。本篇文章主要讲解sw在其生命周期里状态变化的过程中,swR.installingswR.waitingswR.activate的三个属性值的变化情况。

以上主要说明swR.installingswR.waitingswR.activate会出现两个同时有值,且对应不同的sw,目前个人理解以上三个属性不会出现同时有值的逻辑。

ServiceWorker实例

swstate属性一共有以下6个取值:

  • parsed:永远不会被swstateChange事件监听到

  • installing:表示该sw正在安装

  • installed:表示该sw安装完成,此状态经常被称为waiting状态

    The service worker in this state is considered a waiting worker. 这里需要强调一下,网上看了许多PWA的文章,主要描述sw在安装成功后一直处于waiting状态,这里就比较模糊,需要特殊说明一下,这个waiting状态实际上是installed状态。之所以也可以称为waiting状态,是因为swR中用waiting属性会保存installed状态的sw的引用。这也是我写这篇文章的另一个原因,网上大部分的文章都提到sw会有可能一直处于waiting的状态,这是为什么呢?官网上给出的状态没有这个waiting状态,所以仔细阅读了官网,给出了上面的解释,希望能帮到同样困惑的小伙伴。

  • activating:表示该sw正在激活中

  • activated:该状态表示当前sw已经准备好接管事件(fetch等)

  • redundant:表示该sw已被丢弃。发生的场景一般有三个:

    1. 当前的sw安装失败
    2. 当前存在activatedsw,也存在waiting状态的sw(此场景比较好解释,比如第一次注册成功后,假设改变了注册的sw.js文件中的内容,触发sw第一次更新(假设没有通过self.skipWaiting()使其跳过installed状态,一直处于waiting状态),此时又接着改变sw.js文件中的内容,触发sw的第二次更新,则之前第一次更新后一直处于waiting状态的sw状态变为redundant
    3. 当前存在activated状态的sw,不存在waiting状态的sw(此场景也比较好解释,比如,假设改变了注册的sw.js文件中的内容,触发sw第一次更新,并且使用了self.skipWaiting()使其跳过waiting,通过刷新页面或者clients.claim()使其进入activated状态,则之前活跃的sw被丢弃)。

ServiceWorker生命周期

  • install:此时sw状态处于installing,该回调只发生一次。

    The install event is the first event a service worker gets, and it only happens once.

    一般来处理预加载哪些资源缓存到cache storage中;也可以在该回调内执行self.skipWaiting()使其跳过waiting状态。

    self.addEventListener("install", function (event) {
      self.skipWaiting();  
      event.waitUntil(
        caches
          .open(CACHE_NAME)
          .then((cache) => cache.addAll(REQUESTS_EXPECTED_CACHE))
      );
    });
    
  • activate:此时sw状态处于activating

    一般来清理旧版本sw缓存的资源,也可以在该回调内执行clients.claim()使其不刷新页面就可以接管页面(此处可能会有一些问题,暂不展开)。

    self.addEventListener("activate", function (event) {
      event.waitUntil(
        caches.keys().then((keys) => {
          return Promise.all(
            keys.map((key) => {
              if (CACHE_NAME !== key) {
                return caches.delete(key);
              } else {
                return caches
                  .open(CACHE_NAME)
                  .then((cache) => cache.addAll(REQUESTS_EXPECTED_CACHE));
              }
            })
          );
        })
      );
      event.waitUntil(clients.claim());
    });
    
  • fetch:此时sw状态处于activated

    监听请求事件,有多种策略可以选择,这里不作过多的描述。

    self.addEventListener("fetch", (event) => {
      event.respondWith(
        caches.match(event.request).then((response) => {
          if (response !== undefined) {
            return response;
          } else {
            return fetch(event.request)
              .then((response) => {
                let responseClone = response.clone();
                caches.open(CACHE_NAME).then((cache) => {
                  cache.put(event.request, responseClone);
                });
                return response;
              })
              .catch(() => {
                return new Response("", { status: 404 });
              });
          }
        })
      );
    });
    

ServiceWorker生命周期过程中的状态变化完整代码

  • register.js:采用通知用户有新数据更新,将是否更新交给用户来决定

    if ("serviceWorker" in navigator) {
      // 数据有更新,需要通知用户,来触发skipWaiting
      function postSkipWaiting(curWaitingWorker) {
        console.log("提示用户更新");
        if (window.confirm("站点数据有更新,请手动刷新!")) {
          curWaitingWorker.postMessage("skipWaiting");
        }
      }
    
      navigator.serviceWorker.register("./sw.js").then((registration) => {
        //当前活跃的serviceWorker
        let curActiveWorker = navigator.serviceWorker.controller;
        //上次更新,已安装完成,处于installed状态,一直等待激活的serviceWorker
        let curWaitingWorker = registration.waiting;
    
        // 如果用户在第一次更新提示没有选择更新,而是手动刷新页面,则再次弹出提示
        if (
          curWaitingWorker &&
          curActiveWorker &&
          curWaitingWorker !== curActiveWorker // 只是逻辑洁癖,如果两个都存在,应该不相等
        ) {
          postSkipWaiting(curWaitingWorker);
        }
    
        // 有serviceWorker更新发生
        registration.addEventListener("updatefound", () => {
          // 下面三个变量不可能同时存在,curUpdateWorker一定存在
          // 当前更新的serviceWorker
          let curUpdateWorker = registration.installing;
          // 当前活跃的serviceWorker
          curActiveWorker = registration.active;
          // 上次更新,已安装完成,处于installed状态,一直等待激活的serviceWorker
          curWaitingWorker = registration.waiting;
    
          console.log(`当前更新的serviceWorker目前的状态为${curUpdateWorker.state}`);
    
          if (curWaitingWorker && curWaitingWorker !== curUpdateWorker) {
            curWaitingWorker.addEventListener("statechange", (e) => {
              // 当前安装新的serviceWorker时,如果存在上次更新未最后激活的而一直处于waiting状态的serviceWoker
              // 那么这个之前处于waiting状态的serviceWoker应该被标记为多余状态redundant
              console.log(`上次更新未最后激活的而一直处于waiting状态的serviceWoker被标记为${e.target.state}`
              );
            });
          }
    
          curUpdateWorker.addEventListener("statechange", (e) => {
            // 监听状态改变
            console.log(`当前更新的serviceWorker目前的状态变更为${e.target.state}`);
    
            // 当前存在活跃的serviceWorker,当前更新的serviceWorker已安装完毕处于installed,可以提示用户更新
            if (
              curActiveWorker &&
              curActiveWorker !== e.target &&
              e.target.state === "installed" //注意这里不要写成了waiting
            ) {
              postSkipWaiting(e.target);
            }
    
            if (e.target.state === "actived") {
              // 当前更新的serviceWorker变为活跃的,已接管页面与事件
              console.log("当前更新的serviceWorker已完成.");
            }
          });
        });
      });
    } else {
      console.error("Service worker are not supported!");
    }
    
  • sw.js:采用了无刷新接管页面

    // 一般来处理预加载哪些请求的资源作为缓存到cache storage中
    self.addEventListener("install", function (event) {
      // 以下可自行添加预加载资源代码
      // .......
    });
    
    // 在对应回调中一般来清理旧版本serverWorker缓存的资源
    self.addEventListener("activate", function (event) {
      // 以下可自行添加清理旧版本资源代码
      // .......
      // 无刷新接管页面
      event.waitUntil(clients.claim());
    });
    
    // 当前有新的serviceWork正处在handled状态,一直在等待更新,需要接受用户的通知来决定是否跳过等待
    self.addEventListener("message", (event) => {
      if (event.data === "skipWaiting") {
        self.skipWaiting();
      }
    });
    
    // 监听请求事件
    self.addEventListener("fetch", (event) => {
      // 以下可自行添加缓存策略相关代码
      // .......
      
    });
    

    如果采用刷新页面的方式来接管页面,先删除sw.js里注册的activate事件中的event.waitUntil(clients.claim())这句代码;然后在register.js中添加给swC注册controllerchange事件的代码,如下:

    navigator.serviceWorker.addEventListener("controllerchange", () => {    
      window.location.reload();
    });
    

    以上代码主要是为了观察sw的状态变化。

总结

大体描述了sw生命周期中的事件及其状态的变化,里面没有讲到self.skipWaiting()clients.claim()详细的使用方式;没有讲到为什么要使用waitUntil;也没有讲到Cache Storage;后续有时间会再写篇文章单独讲解,欢迎大家留言指正。

参考文献

本文发布内容来自个人对于MDNweb.dev网站关于Service Worker的阅读及实践后的理解,文章未经授权禁止任何形式的转载。