背景
Service Worker 可以使你的应用先访问本地缓存资源,所以在离线状态时,在没有通过网络接收到更多的数据前,仍可以提供基本的功能(一般称之为 Offline First)。这是原生APP 本来就支持的功能,这也是相比于 web app,原生 app 更受青睐的主要原因。
基本架构
通常遵循以下基本步骤来使用 service workers:
- service worker URL 通过 serviceWorkerContainer.register() 来获取和注册。
- 如果注册成功,service worker 就在 ServiceWorkerGlobalScope 环境中运行; 这是一个特殊类型的 woker 上下文运行环境,与主运行线程(执行脚本)相独立,同时也没有访问 DOM 的能力。
- service worker 现在可以处理事件了。
- 受 service worker 控制的页面打开后会尝试去安装 service worker。最先发送给 service worker 的事件是安装事件(在这个事件里可以开始进行填充 IndexDB和缓存站点资源)。这个流程同原生 APP 或者 Firefox OS APP 是一样的 — 让所有资源可离线访问。
- 当 oninstall 事件的处理程序执行完毕后,可以认为 service worker 安装完成了。
- 下一步是激活。当 service worker 安装完成后,会接收到一个激活事件(activate event)。 onactivate 主要用途是清理先前版本的service worker 脚本中使用的资源。
- Service Worker 现在可以控制页面了,但仅是在 register() 成功后的打开的页面。也就是说,页面起始于有没有 service worker ,且在页面的接下来生命周期内维持这个状态。所以,页面不得不重新加载以让 service worker 获得完全的控制。
Service Worker生命周期
Service Worker支持的事件
一个完整的Service Worker示例
'use strict';
// 我们当前的缓存版本及其内容。
var CACHE = {
version: 'site-version-number',
resources: [
'/index.html', // 缓存index.html
'/css/', // 缓存/css文件夹中的所有内容
],
};
// 安装service worker,添加所有缓存条目
this.addEventListener('install', function (event) {
event.waitUntil(
caches.open(CACHE.version).then(function (cache) {
return cache.addAll(CACHE.resources);
})
);
});
// 处理fetch请求。如果未从缓存中获取,则尝试添加到缓存中。
this.addEventListener('fetch', function (event) {
event.respondWith(
caches
.match(event.request)
.then(function (resp) {
return (
resp ||
fetch(event.request)
.then(function (response) {
return caches
.open(CACHE.version)
.then(function (cache) {
cache.put(event.request, response.clone()).catch(function (error) {
console.log('无法添加到缓存!' + error);
});
return response;
})
.catch(function (error) {
console.log('无法打开缓存!' + error);
});
})
.catch(function (error) {
console.log('资源未找到!' + error);
})
);
})
.catch(function (error) {
console.log('缓存中未找到资源!' + error);
})
);
});
// 激活service worker
this.addEventListener('activate', function (event) {
// 移除所有不在白名单中的缓存
var cacheWhitelist = [CACHE.version];
event.waitUntil(
caches.keys().then(function (keyList) {
return Promise.all(
keyList.map(function (key) {
if (cacheWhitelist.indexOf(key) === -1) {
return caches.delete(key);
}
})
);
})
);
});
Service Worker和浏览器缓存的区别
从上图可以看出,请求缓存流程如下
- 访问Service Worker缓存,如果存在就使用Service Worker缓存,不检测浏览器缓存
- 访问浏览器缓存,如果存在就使用,不访问服务器
- 上述缓存都没有,访问服务器获取资源
为什么Service Worker注册失败了?
你没有在 HTTPS 下运行你的程序
service worker文件的地址没有写对— 需要相对于 origin , 而不是 app 的根目录。在我们的例子例, service worker 是在 https://mdn.github.io/sw-test/sw.js,app 的根目录是 https://mdn.github.io/sw-test/。应该写成 /sw-test/sw.js 而非 /sw.js.
service worker 在不同的 origin 而不是你的app的,这是不被允许的。
也请注意:
service worker 只能抓取在 service worker scope 里从客户端发出的请求。
最大的 scope 是 service worker 所在的地址
如果你的 service worker 被激活在一个有 Service-Worker-Allowed header 的客户端,你可以为service worker 指定一个最大的 scope 的列表。
在 Firefox, Service Worker APIs 在用户在 private browsing mode 下会被隐藏而且无法使用。
workbox
workbox是一个库来方便我们使用service worker,创建自己的pwa应用,我们可以先了解一下 workbox的特点
- 不管你的站点是何种方式构建的,都可以为你的站点提供离线访问能力。
- 就算你不考虑离线能力,也能让你的站点访问速度更加快。
- 几乎不用考虑太多的具体实现,只用做一些配置。
- 简单却不失灵活,可以完全自定义相关需求(支持 Service Worker 相关的特性如 Web Push, Background sync 等)。
- 针对各种应用场景的多种缓存策略。
workbox使用
类似service worker,在 sw.js 文件里面注册workbox
// index.html
<script>
// Check that service workers are registered
if ('serviceWorker' in navigator) {
// Use the window load event to keep the page load performant
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}
</script>
// sw.js
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.6.1/workbox-sw.js');
if (workbox) {
console.log(`Yay! Workbox is loaded 🎉`);
} else {
console.log(`Boo! Workbox didn't load 😬`);
}
workbox缓存策略
缓存图片
workbox.routing.registerRoute(
/\.(?:png|gif|jpg|jpeg|svg)$/,
workbox.strategies.cacheFirst({
cacheName: 'images',
plugins: [
new workbox.expiration.Plugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
}),
],
}),
);
缓存css和javascript
workbox.routing.registerRoute(
/\.(?:js|css)$/,
workbox.strategies.staleWhileRevalidate(),
);
缓存网页字体
// Cache the Google Fonts stylesheets with a stale while revalidate strategy.
workbox.routing.registerRoute(
/^https:\/\/fonts\.googleapis\.com/,
workbox.strategies.staleWhileRevalidate({
cacheName: 'google-fonts-stylesheets',
}),
);
// Cache the Google Fonts webfont files with a cache first strategy for 1 year.
workbox.routing.registerRoute(
/^https:\/\/fonts\.gstatic\.com/,
workbox.strategies.cacheFirst({
cacheName: 'google-fonts-webfonts',
plugins: [
new workbox.cacheableResponse.Plugin({
statuses: [0, 200],
}),
new workbox.expiration.Plugin({
maxAgeSeconds: 60 * 60 * 24 * 365,
}),
],
}),
);