文章来自 个人博客 。html
生命周期是 Service Worker 中比较复杂的一部分,若是你不知道它什么时间将要作什么,以及它带来的好处,那么你可能会有一种感受:就是它一直在和你较劲。若是理解它的工做机制,你就能够给用户提供完美的,无感知的更新体验。 这篇文章是 Chrome 团队最近总结的一片文章,配合例子讲述生命周期,让咱们更容易理解,也解决了我以前开发中遇到的一些困惑,因此决定翻译出来。此处 阅读原文 。git
本文中介绍利用生命周期能够实现的功能大概有以下几点:github
实现缓存优先(offline-first)web
在不打断现有 SW 的状况下,准备好一个新的 SW浏览器
让注册 SW 的页面同一时间只归属同一个 SW 控制缓存
确保你的网站只有一个版本在运行app
最后一点尤为重要,通常状况下(没有 SW 的状况),用户浏览你的网站时可能先打开一个 tab,过了一下子又打开了一个 tab,结果就是在同一时间,你的页面运行了两个版本,大部分时候,这样是没问题的,可是若是你使用了缓存,那么两个 tab 就要面临如何管理缓存的问题,若是处理很差,它可能会形成异常,严重的形成数据丢失。svg
用户很是不喜欢数据丢失,这会让他们很是忧桑。工具
install
事件是 SW 触发的第一个事件,而且仅触发一次。fetch
installEvent.waitUntil()
接收一个 Promise 参数,用它来表示 SW 安装的成功与否。
SW 在安装成功并激活以前,不会响应 fetch
或push
等事件。
默认状况下,页面的请求(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:
SW 首先会触发install
,每一个 SW 只会被触发一次,当你修改你的 SW 后,浏览器会认为这是一个新的 SW,从而会再触发这个新 SW 的install
事件,在后面会详细说到。
install
是在 SW 控制 clients
以前处理缓存很好的时机。在 event.waitUntil()
传入的 Promise 会让浏览器知道 SW 何时安装成功以及是否成功。
当 Promise reject 的时候,表明着安装失败,浏览器将这个 SW 废弃掉,不会控制任何 clients。
安装成功后并激活(activate)成功后,SW 就能够处理“功能性的事件“了,好比push
,sync
。但这并不表明调用.register()
的页面会当即生效。
第一次你请求 这个demo 的时候,虽然在 SW 被激活后好久才请求了dog.svg
(由于这里等待了三秒),但 SW 也并无处理这个请求,结果你看见了一只狗。当你第二次请求的时候,也就是刷新页面,这时请求被处理了,当前页面和图片都通过了 SW 的 fetch
事件,因此你看见了一只猫。
你能够在activate
事件中经过调用clients.claim()
来让没被控制的 clients 受控。
好比 这个demo ,可能第一次你就会看见一只猫,这里我说“可能”,是由于这时时间敏感的,仅当 SW 激活而且clients.claim()
被调用成功在图片请求以前的时候才能够。
因此,可想而知,当你用 SW 加载与正常请求不一样资源的时候(好比上面的例子),那用clients.claim()
可能会遇到一些问题,这时有些资源可能不会经过你的SW。
我见过不少人在代码中把
clients.claim()
当作了必选项,但我本身不多这样作,由于仅仅是第一次加载不会经过 SW,并且页面仍是都会正常运行的。
简单来讲:
触发更新的几种状况:
第一次导航到做用域范围内页面的时候
当在24小时内没有进行更新检测而且触发功能性时间如push
或sync
的时候
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 ,你会看到一只喵,缘由是这样的...
注意这里咱们将缓存从 static-v1 换到了 static-v2,这表明了我用了一个新的缓存空间覆盖了以前 SW 正在使用的缓存。
这里新建了一块缓存的作法相似于原生 app 中将每块资源打包到一块指定的执行空间的作法,有时候结合实际状况,你也能够不这么作。
一旦新 SW 安装成功,它会进入wait
状态直到原始 SW 不控制任何 clients。这个状态是waiting
,这也是浏览器确保在同一时间只有一个版本的 SW 运行的机制。
若是你再次打开 这个 demo ,你仍是会看到一只喵,由于新的 SW 仍是没有被激活,在开发者工具里你依然看到它是 waiting 状态。
尽管这个例子中你仅打开了一个 tab,但刷新页面并无用,这是因为浏览器自己的机制,当你刷新的时候,当前页面不会离开,直到收到了一个响应头,并且即便这样,若是响应中包含Content-Disposition
的话,当前页面仍是不会离开。因为这个时间上的重叠,在刷新的时候当前的 SW 老是控制了一个 client。
为了让 SW 更新,你须要把全部用原始 SW 的页面 tab 关闭或者跳转走,这时你再访问 这个 demo ,你就会看到了一匹野马。
这种机制相似于 Chrome 自己的更新机制,Chrome 在后台更新,只有当你重启浏览器的时候才会生效,在这期间你不会被打扰,能够继续使用当前版本。然而,这样可能会使咱们开发者比较痛苦,好在开发者工具帮咱们解决了这个事情,后面会说到。
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- 开头的缓存。
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 中这么作是“最差实践”,要在原地址上修改 SW。
举个例子来讲明为何:
1.index.html
注册了sw-v1.js
做为SW。
2.sw-v1.js
对index.html
作了缓存,也就是缓存优先(offline-first)。
3.你更新了index.html
从新注册了在新地址的 SW sw-v2.js
.
若是你像上面那么作,用户永远也拿不到sw-v2.js
,由于index.html
在sw-v1.js
缓存中,这样的话,若是你想更新为sw-v2.js
,还须要更改原来的sw-v1.js
。
在上面的 demo 里,我给每一个 SW 用了不一样的 URL,这只是为了作演示,不要在生产环境中这么作。
SW 的生命周期是为了用户构建的,但这样不免让咱们开发带来一些烦恼,幸好与一些工具来帮助咱们。
这是我最喜欢的:
这样把生命周期变得对开发友好了,每次跳转将会:
1.从新获取 SW
2.尽管字节一致,也会从新安装,也就是说install
事件被执行而且更新缓存。
3.跳过 waiting,激活新的 SW。
4.导航到这个页面。
这就是说你每次操做都会更新而不用刷新页面或者关闭 tab。
若是你有个 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 });