先来了解一下Service Worker
Service Worker简介及其注意事项
Service Worker 是浏览器在后台独立于网页运行的脚本,它打开了通向不需要网页或用户交互的功能的大门。 现在,它们已包括如推送通知和后台同步等功能。 将来,Service Worker 将会支持如定期同步或地理围栏等其他功能。
使用注意事项
- 它是一种 JavaScript Worker,无法直接访问 DOM。 Service Worker 通过响应 postMessage 接口发送的消息来与其控制的页面通信,页面可在必要时对 DOM 执行操作。
- Service Worker 是一种可编程网络代理,能够控制页面所发送网络请求的处理方式。
- 在开发过程中,可以通过 localhost 使用 Service Worker,但如果要在网站上部署 Service Worker,则需要在服务器上设置 HTTPS。
- Service Worker 在不用时会被中止,并在下次有需要时重启,因此,不能依赖 Service Worker onfetch 和 onmessage 处理程序中的全局状态。 如果存在需要持续保存并在重启后加以重用的信息,Service Worker 可以访问 IndexedDB API。
接下来先大概了解一下Service Wroker的生命周期
生命周期简介
Service Worker 的生命周期完全独立于网页。
安装:在安装过程中,通常需要缓存某些静态资产,如果某些资源已成功缓存,那么 Service Worker 就安装完毕。如果任何文件下载失败或缓存失败,那么安装步骤将会失败,Service Worker 就无法激活(也就是说, 不会安装)。如果发生这种情况,不必担心,它下次会再试一次。
激活:安装成功之后,接下来就是激活步骤,通常会在这个阶段管理旧缓存,下文会对这一阶段详细介绍。 页面控制:Service Worker 将会对其作用域内的所有页面实施控制,不过,首次注册该 Service Worker 的页面需要再次加载才会受其控制。服务工作线程实施控制后,它将处于以下两种状态之一:服务工作线程终止以节省内存,或处理获取和消息事件,从页面发出网络请求或消息后将会出现后一种状态。
以下是 Service Worker 初始安装时的简化生命周期:
我们先从第一次安装说起,首次安装经历的过程如下:
首次安装使用
执行过程
- install 事件是 Service Worker 获取的第一个事件,并且只发生一次。
- 传递到 installEvent.waitUntil() 的一个 promise 可表明安装的持续时间以及安装是否成功。
- 在成功完成安装并处于“活动状态”之前,Service Worker 不会收到 fetch 和 push 等事件。
- 默认情况下,不会通过 Service Worker 提取页面,除非页面请求本身需要执行 Service Worker。因此,需要刷新页面以查看 Service Worker 的影响。
- clients.claim() 可替换此默认值,并提前控制未控制的页面。
我们通过以下脚本注册service worker并且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>
Service Worker(sw.js)的代码如下:
self.addEventListener('install', event => {
console.log('V1 installing…');
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);
if (url.origin == location.origin && url.pathname == '/dog.svg') {
event.respondWith(caches.match('/cat.svg'));
}
});
service worker在安装时缓存了一张猫的图片,并在请求 /dog.svg 时提供该图像,不过在你运行这个示例,首次加载时会看到小狗的图片,刷新一次后会看到猫的图片。
作用域和控制
Service Worker 注册的默认作用域是与脚本网址相对的 ./。这意味着如果在 //example.com/foo/bar.js 注册一个 Service Worker,则它的默认作用域为 //example.com/foo/。 通过 navigator.serviceWorker.controller(其将为 null 或一个 Service Worker 实例)检测客户端是否受控制。
下载、解析和执行
在调用 .register() 时,将下载第一个 Service Worker。如果脚本在初始执行中未能进行下载、解析,或引发错误,则注册器 promise 将拒绝,并舍弃此 Service Worker。
Chrome 的 DevTools 在控制台和应用标签的 Service Worker 部分中显示此错误:
Install Service Worker
获取的第一个事件为 install。该事件在 Worker 执行时立即触发,并且它只能被每个 Service Worker调用一次。如果更改Service Worker 脚本,则浏览器将其视为一个不同的 Service Worker,并且它将获得自己的 install 事件。将在后面对更新进行详细介绍。
在能够控制客户端之前,install 事件中可以缓存需要的所有内容。 event.waitUntil()一直等待promise完成,然后判断安装是否成功。如果 promise 拒绝,则表明安装失败,浏览器将丢弃 Service Worker。它将无法控制客户端。这意味着我们不能依靠 fetch 事件的缓存中存在的“cat.svg”。它是一个依赖项。
Activate
Service Worker 准备控制客户端并处理 push 和 sync 等功能事件时,将获得一个 activate 事件。但这不意味着调用 .register() 的页面将受控制。就像上面示例中展示的,即使Service Worker 激活很长时间后请求 dog.svg,它也不会处理此请求,仍会看到小狗的图像。默认值为 consistency。
clients.claim
激活 Service Worker 后,可以通过调用 clients.claim() 提起控制未受控制的页面。可以通过这个示例查看其作用,在查看这个示例时,先清除网站的缓存数据
其在 activate 事件中调用 clients.claim()。首先应该看到一只猫。说“应该”是因为这受时间约束。如果图像尝试加载之前,Service Worker 激活且 clients.claim() 生效,那么看到的是猫。具体代码如下
self.addEventListener('install', event => {
console.log('Installing…');
event.waitUntil(
caches.open('static-v1').then(cache => cache.add('cat.svg'))
);
});
self.addEventListener('activate', event => {
clients.claim();
console.log('Now ready to handle fetches!');
});
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (url.origin == location.origin && url.pathname.endsWith('/dog.svg')) {
event.respondWith(caches.match('cat.svg'));
}
});
更新 Service Worker
触发更新时机
- 导航到一个作用域内的页面。
- 更新 push 和 sync 等功能事件,除非在前 24 小时内已进行更新检查。
- 调用 .register(),仅在 Service Worker 网址已发生变化时。
大多数浏览器检查已注册的 Service Worker 脚本的更新时,默认情况下都会忽略缓存标头。
更新过程
- 如果 Service Worker 的字节与浏览器已有的字节不同,则考虑更新 Service Worker。
- 更新的 Service Worker 与现有 Service Worker 一起启动,并获取自己的 install 事件。
- 如果新 Worker 出现不正常状态代码(例如,404)、解析失败,在执行中引发错误或在安装期间被拒,则系统将舍弃新 Worker,但当前 Worker 仍处于活动状态。
- 安装成功后,更新的 Worker会处于等待状态,直到现有 Worker 让出授权。(注意,在刷新期间客户端会重叠。)
- self.skipWaiting()会跳过等待,Service Worker 在安装完后立即激活。
示例
我们更新上面的示例,在响应时使用马的图片而不是猫的图片。
const expectedCaches = ['static-v2'];
self.addEventListener('install', event => {
console.log('V2 installing…');
event.waitUntil(
caches.open('static-v2').then(cache => cache.add('/horse.svg'))
);
});
self.addEventListener('activate', event => {
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);
if (url.origin == location.origin && url.pathname == '/dog.svg') {
event.respondWith(caches.match('/horse.svg'));
}
});
查看上述示例,你应该还是看见猫的图片,具体原因咱们往下看
更新过程如下:
Install
我已将缓存名称从 static-v1 更改为 static-v2。这意味着我可以设置新的缓存,而无需覆盖旧 Service Worker 仍在使用的当前缓存中的内容。
Waiting
成功安装 Service Worker 后,更新的 Service Worker 将延迟激活,直到现有 Service Worker 不再控制任何客户端。此状态称为“waiting”,这是浏览器确保每次只运行一个 Service Worker 版本的方式。 上述示例是因为 V2 Worker 尚未激活。在 DevTools 的“Application”标签中,会看到等待的新 Service Worker:
即使在演示中仅打开一个标签,刷新页面时也不会显示新版本。原因在于浏览器导航的工作原理。当导航时,在收到响应标头前,当前页面不会消失,即使此响应具有一个 Content-Disposition 标头,当前页面也不会消失。由于存在这种重叠情况,在刷新时当前 Service Worker 始终会控制一个页面。
要获取更新,需要关闭或退出使用当前 Service Worker 的所有标签。然后在查看这个示例就会看到马的图片。
Activate
旧 Service Worker 退出时将触发 Activate,新 Service Worker 将能够控制客户端。此时可以处理一些迁移数据库或清除缓存的工作。在上面的演示中,在 activate 事件中,删除了所有其他缓存,从而也移除了旧的 static-v1 缓存。
注意:不要更新以前的版本。它可能是许多旧版本的 Service Worker
将一个 promise 传递到 event.waitUntil(),它将缓冲功能事件(fetch、push、sync 等),直到 promise 进行解析。因此,fetch 事件触发时,激活已全部完成。
跳过等待阶段
可以通过调用 self.skipWaiting() 尽快将新 Service Worker 激活。
这会导致这次的 Service Worker 将当前活动的 Worker 逐出,并在进入等待阶段时尽快激活自己(或立即激活,前提是已经处于等待阶段)。这不能让 Worker 跳过安装,只是跳过等待阶段。
skipWaiting() 在等待期间调用还是在之前调用并没有什么不同。一般情况下是在 install 事件中调用它:
self.addEventListener('install', event => {
self.skipWaiting();
event.waitUntil(
// caching etc
);
});
避免更改 Service Worker 脚本的网址
一定不要随意更改Service Worker的地址,这将会导致以下问题:
- index.html 将 sw-v1.js 注册为 Service Worker。
- sw-v1.js 缓存并提供 index.html,以实现离线优先。
- 更新 index.html,以便注册全新的 sw-v2.js。
执行上述操作,用户将永远无法获取 sw-v2.js,因为 sw-v1.js 将从其缓存中提供旧版本的 index.html。
注意:上面的那些示例只是为了演示作用
开发调试
Update on reload
这可使生命周期变得对开发者友好。每次浏览时都将:
- 重新提取 Service Worker。
- 即使字节完全相同,也将其作为新版本安装,这表示运行 install 事件并更新缓存。
- 跳过等待阶段,以激活新 Service Worker。
- 浏览页面。这意味着每次浏览时(包括刷新)都将进行更新,无需重新加载两次或关闭标签。
Skip waiting
强制重新加载页面 (shift-reload),则将完全绕过 Service Worker。页面将变得不受控制。此功能已列入规范,因此,它在其他支持 Service Worker 的浏览器中也适用。通过api监听更新
Service Worker暴露了api可以监听更新状态变化,代码如下:
navigator.serviceWorker.register('/sw.js').then(reg => {
reg.installing; // the installing worker, or undefined
reg.waiting; // the waiting worker, or undefined
reg.active; // the active worker, or undefined
reg.addEventListener('updatefound', () => {
// 当一个service worker安装时触发
const newWorker = reg.installing;
newWorker.state;
// "installing" 安装事件已经触发,但尚未完成
// "installed" 安装完成
// "activating" 激活事件已经触发,但还没有完成
// "activated" 激活完成
// "redundant" 丢弃。要么是安装失败,要么被新版本取代。
newWorker.addEventListener('statechange', () => {
// newWorker.state 变化时
});
});
});
navigator.serviceWorker.addEventListener('controllerchange', () => {
// 当service worker管控页面时触发
});