一 前言
浏览器一般有三类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)。
(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) 强制更新
- 距离上一次更新检查已超过24小时, 会忽略浏览器缓存, 强制到服务器更新一次.
(2) 检查更新(Soft Update)
一般在下面情况会检查更新,
- 第一次访问scope里的页面.
- 距离上一次更新检查已超过24小时.
- 有功能性事件发生, 比如push, sync.
- 在ServiceWorker URL发生变化时调用了.register()方法.
- ServiceWorker JS的缓存时间已超出其头部的max-age设置的时间 (注: max-age大于24小时, 会使用24小时作为其值).
- ServiceWorker JS的代码只要有一个字节发生了变化, 就会触发更新, 包括其引入的脚本发生了变化.
我们看看浏览器内核是怎样实现周期性的检查更新的.
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