PWA系列 - Service Workers 生命周期

一 前言

浏览器一般有三类Worker,

(1) Dedicated Worker, 专用worker, 只能被创建它的JS访问. 创建它的页面关闭, 它的生命周期就结束了.

(2) Shared Worker, 共享worker, 可以被同一域名下的JS访问. 关联的页面都关闭时, 它的生命周期就结束了.

(3) ServiceWorker, 是事件驱动的worker, 生命周期与页面无关. 关联页面未关闭时, 它也可以退出, 没有关联页面时, 它也可以启动.

这三类Worker, 一个非常重要的区别在于不同的生命周期.  ServiceWorker与文档无关的生命周期, 是它能提供可靠Web服务的一个重要基础.

本文重点描述ServiceWorker如果管理它与文档无关的生命周期, 以及如何管理各种版本状态.

二 生命周期

官方文档提到, ServiceWorker生命周期的目的是,

  • 实现离线优先.
  • 在不打断现有ServiceWorker的情况下,准备好一个新的ServiceWorker.
  • ServiceWorker注册的scope下的页面, 同一时间只由一个ServiceWorker控制.
  • 确保你的网站只有一个版本在运行.

Service Worker 可能有以下几种状态:解析成功(parsed),正在安装(installing),安装成功(installed),正在激活(activating),激活成功(activated),废弃(redundant)。

08f326c3cae549f0408f0bb25624c191668bd25a

(1) Service Worker 注册成功,navigator.serviceWorker.register返回成功, 并不意味着它已经完成安装或已经激活,仅仅是worker的脚本被成功解析,比如, 注册worker的URL与文档同源,协议是 HTTPS。

(2) Service Worker 注册成功后,会转入installing状态, 此时, install事件会被触发, 比较典型的做法是在install事件的处理函数中提前加载相关静态文件进缓存.

self.addEventListener('install', function(event) {  
  event.waitUntil(
    caches.open(currentCacheName).then(function(cache) {
      return cache.addAll(arrayOfFilesToCache);
    })
  );
});

如果有event.waitUntil方法, 必须等待它里面的操作成功完成, 否则会失败, 转入redundant状态.

(3) Service Worker 安装成功后, 会转入installed/waiting状态, 此时, ServiceWorker已准备好, 在等待接管页面已有的worker, 从而可以控制页面.

(4) Service Worker在满足下面条件之一时, 可以转入activating状态:

  • 没有active worker在运行.
  • JS调用self.skipWaiting()跳过waiting状态.
  • 用户关闭页面, 释放了当前处于active状态的worker.
  • 一定时间之后, 系统释放了当前处于active状态的worker.

在activating状态中, activate事件会触发, 比较典型的做法是在activate事件的处理函数中清理无用的缓存.

self.addEventListener('activate', function(event) {  
  event.waitUntil(
    // Get all the cache names
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        // Get all the items that are stored under a different cache name than the current one
        cacheNames.filter(function(cacheName) {
          return cacheName != currentCacheName;
        }).map(function(cacheName) {
          // Delete the items
          return caches.delete(cacheName);
        })
      ); // end Promise.all()
    }) // end caches.keys()
  ); // end event.waitUntil()
});

如果有event.waitUntil方法, 必须等待它里面的操作成功完成, 否则会失败, 转入redundant状态.

(5) Service Worker激活成功, 会转入active状态, 此时的worker已在控制页面的行为, 可以处理一些功能事件, 比如fetch, push, message.

self.addEventListener('fetch', function(event) {  
  // Do stuff with fetch events
});

self.addEventListener('message', function(event) {  
  // Do stuff with postMessages received from document
});

(6) Service Worker在满足下面条件之一时, 会转入redundant状态:

  • 安装失败
  • 激活失败
  • 被新的Service Worker取代

本节内容参考 The Service Worker Lifecycle

三 状态管理

ServiceWorker在浏览器内核有两类状态, 一类是ServiceWorker线程的运行状态, 另一类是ServiceWorker脚本版本的状态.

(1) ServiceWorker线程的运行状态, 一般对应ServiceWorker线程的状态. 这类状态只保存在内存中.

  • STOPPED: 已停止, EmbeddedWorkerInstance::OnStopped时设置.
  • STARTING: 正在启动, EmbeddedWorkerInstance::Start时设置.
  • RUNNING: 正在运行, EmbeddedWorkerInstance::OnStarted时设置.
  • STOPPING: 正在停止, EmbeddedWorkerInstance::Stop --> EmbeddedWorkerRegistry::StopWorker返回status为SERVICE_WORKER_OK时设置.

(2) ServiceWorker脚本版本(即注册函数中指定的serviceworker.js)的状态, 这类状态中的INSTALLED和ACTIVATED可以被持久化存储.

  • NEW:  浏览器内核的ServiceWorkerVersion已创建, 属于一个初始值.
  • INSTALLING:  Install事件被派发和处理. 一般在ServiceWorker线程启动后, 即ServiceWorkerVersion::StartWorker返回status为SERVICE_WORKER_OK时设置.
  • INSTALLED:  Install事件已处理完成, 准备进入ACTIVATING状态. 一般在注册信息已存储到数据库, 即ServiceWorkerStorage::StoreRegistration返回status为SERVICE_WORKER_OK时设置.
  • ACTIVATING:  Activate事件被派发和处理. 一般在当前scope下没有active ServiceWorker或INSTALLED状态的ServiceWorker调用了skipWaiting, ServiceWorker就会从INSTALLED状态转为ACTIVATING状态.
  • ACTIVATED:  Activate事件已处理完成, 已正式开始控制页面, 可处理各类功能事件. 一般在activate事件处理完成后就会转为ACTIVATED状态, 此时ServiceWorker就可以控制页面行为, 可以处理功能事件, 比如, fetch, push.
  • REDUNDANT: ServiceWorkerVersion已失效, 一般是因为执行了unregister操作或已被新ServiceWorker更新替换. 

注1: ServiceWorker规范 中提到的 "service workers may be started and killed many times a second", 指的是ServiceWorker线程随时可以Started和Killed. 在关联文档未关闭时, ServiceWorker线程可以处于Stopped状态; 在全部关联文档都已关闭时, ServiceWorker线程也可以处于Running状态.

注2: ServiceWorker脚本版本的状态, 也是独立于文档生命周期的, 与ServiceWorker线程的运行状态无关, ServiceWorker线程关闭时, ServiceWorker脚本版本也可处于ACTIVATED状态.

注3: ServiceWorker脚本版本的状态, INSTALLED和ACTIVATED是稳定的状态, ServiceWorker线程启动之后一般是进入这两种状态之一. INSTALLING和ACTIVATING是中间状态, 一般只会在ServiceWorker新注册或更新时触发一次, 刷新页面一般不会触发. INSTALLING成功就转入INSTALLED, 失败就转入REDUNDANT. ACTIVATING成功就转入ACTIVATED, 失败就转入REDUNDANT. 

注4: 如果ServiceWorker脚本版本处于ACTIVATED状态, 功能事件处理完之后, ServiceWorker线程会被Stop, 当再次有功能事件时, ServiceWorker线程又会被Start, Start完成后ServiceWorker就可以立即进入ACTIVATED状态.

四 版本管理

ServiceWorker脚本版本, 浏览器内核会管理三种版本.

(1) installing_version: 处于INSTALLING状态的版本

(2) waiting_version: 处于INSTALLED状态的版本

(3) active_version: 处于ACTIVATED状态的版本

installing_version 一般是在ServiceWorker线程启动后, 即ServiceWorkerVersion::StartWorker返回status为SERVICE_WORKER_OK时, 处于此版本状态, 这是一个中间版本, 在正确安装完成后会转入waiting_version.

waiting_version 一般在注册信息已存储到数据库, 即ServiceWorkerStorage::StoreRegistration返回status为SERVICE_WORKER_OK时, 处于此版本状态. 或者在再次打开ServiceWorker页面时, 检查到ServiceWorker脚本版本的状态为INSTALLED, 也会进入此版本状态. waiting_version 的存在确保了当前scope下只有一个active ServiceWorker.

active_version 一般在activate事件处理完成后, 就会处于此版本状态, 同一scope下只有一个active ServiceWorker. 需要特别注意的是, 当前页面已有active worker控制, 刷新页面时, 新版本Waiting(Installed)状态的ServiceWorker并不能转入active状态. 

ServiceWorker可以从waiting_version转入active_version的条件:

  • 当前scope下没有active ServiceWorker在运行.
  • 页面JS调用self.skipWaiting跳过waiting状态.
  • 用户关闭页面, 释放了当前处于active状态的ServiceWorker.
  • 浏览器周期性检测, 发现active ServiceWorker处于idle状态, 就会释放当前处于active状态的ServiceWorker.

五 脚本更新

ServiceWorker注册函数中指定的scriptURL(比如, serviceworker.js), 会在什么情况下请求更新呢? 一般有两种更新方式.

(1) 强制更新

(2) 检查更新(Soft Update)

一般在下面情况会检查更新,

  • 第一次访问scope里的页面.
  • 距离上一次更新检查已超过24小时.
  • 有功能性事件发生, 比如push, sync.
  • 在ServiceWorker URL发生变化时调用了.register()方法.
  • ServiceWorker JS的缓存时间已超出其头部的max-age设置的时间 (注: max-age大于24小时, 会使用24小时作为其值).
  • ServiceWorker JS的代码只要有一个字节发生了变化, 就会触发更新, 包括其引入的脚本发生了变化.

我们看看浏览器内核是怎样实现周期性的检查更新的.

ServiceWorker Schedule Update
ServiceWorkerControlleeRequestHandler::~ServiceWorkerControlleeRequestHandler 
// Navigation triggers an update to occur shortly after the page and its initial subresources load.
--> ServiceWorkerVersion::ScheduleUpdate   // if (is_main_resource_load_)
--> ServiceWorkerVersion::StartUpdate

从上述代码流程可以看到, ServiceWorker页面主文档加载完成时, 就会触发active_version的一次检查更新, 如果距离上一次脚本更新的时间超过了24小时, 就会设置LOAD_BYPASS_CACHE的标记, 忽略浏览器缓存, 直接从网络加载.

上一次脚本更新的时间, 一般在ServiceWorker安装完成时会更新为当前时间, 或者检查到脚本超过24小时都没有发生变化也会更新为当前时间, 这样就能保证ServiceWorker在安装完成之后, 每隔24小时, 至少会更新一次.

六 线程退出

ServiceWorker线程一般在什么情况下会被停止呢?

(1)ServiceWorker JS有任何异常,都会导致ServiceWorker线程退出。包括但不限于, JS文件存在语法错误, ServiceWorker安装失败/ 激活失败,ServiceWorker JS执行时出现未捕获的异常。

(2)ServiceWorker 功能事件处理完成,处于空闲状态,ServiceWorker线程会自动退出。

(3)ServiceWorker JS执行时间过长,ServiceWorker线程会自动退出。比如, ServiceWorker JS执行时间超过30秒,或Fetch请求在5分钟内还未完成。

(4)浏览器会周期性检查各个ServiceWorker线程是否可以退出, 一般在启动ServiceWorker线程30秒会检查一次,杀掉空闲超过30秒的ServiceWorker线程。

(5)为了方便开发者调试, Chromium进行了特殊处理, 在连上devtools之后,ServiceWorker线程不会退出。
参考:Keep a serviceworker alive when devtools is attached

七 参考文档

Stackoverflow - Service Worker vs Shared Worker

The Service Worker Lifecycle - By Ire Aderinokun

The Service Worker Lifecycle - By Jake Archibald

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值