Service Worker 与 PWA
PWA全称为Progressive Web App,即渐进式Web应用。其理念是通过多种技术来增强Web应用体验,以提供接近原生应用的用户体验。
与PWA相关的技术点主要包含:
- Manifest:使得Web应用可以被添加到主屏幕。需要创建一个用以描述Web应用被添加至主屏幕的名称、图标等信息的
manifest.json
文件。 - Service Worker:为Web应用提供离线缓存,使得Web应用可以在离线状态下继续使用(部分功能)。
- Push Notification:提供了消息推送功能。
作为PWA中重要的一部分,今就来简单的了解一下Service Worker。
使用Service Worker
Service Worker遵循下载、安装、激活
的生命周期,并提供了fetch
等事件,允许我们对资源的获取实施干预。
Service Worker的生命周期:
Service Worker的事件:
注册
我们通过serviceWorker.register
注册我们的Service Worker:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(</path/to/your_service_worker.js>, { scope: './' }).then((reg) => {
//
}).catch((error) => {
//
});
}
注册时我们需要提供我们的Service Worker代码的路径。scope
参数是可选的,用以指定service worker可控制的内容的子目录:service worker 只能抓取在 service worker scope 里从客户端发出的请求。
单个service worker可以控制多个页面: 每个页面不会有自己独有的worker
你的Web应用需要在HTTPS
下运行,同时Web应用于service worker应当在同一个origin下
安装和激活
this.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
return cache.addAll([
'/sw-test/',
'/sw-test/index.html',
...
]);
})
);
});
install
事件会在安装完成之后触发。- 我们添加了
install
事件的监听器,并通过event.waitUntil
确保在安装完成之前添加这些缓存。 - 我们通过
caches.open
创建了一个名叫v1
的缓存,并通过addAll
方法将目标资源加入缓存。
Cache
Cache了提供控制缓存的能力:
cache.add(<request|url>)
和cache.addAll(<url_list>)
:抓取目标URL, 检索并把返回的response对象添加到给定的Cache对象cache.put<request>, <response>
:同时抓取一个请求及其响应,并将其添加到给定的cachecache.match(<request>)
:匹配缓存,返回空或Response
对象cache.delete(<key>)
:删除缓存对象cache.keys
:列出所有键- Cache的键为
Request
对象,值为Reponse
对象 - 方法都会返回
Promise
对象
CacheStorage提供了缓存的管理:
caches.open(<name>)
:创建一个名为name
的缓存caches.keys()
:列出所有缓存的键caches.has(<name>)
:判断缓存是否存在caches.delete(<name>)
:删除缓存- 方法都会返回
Promise
对象
caches API在页面和SW中都可以使用
自定义请求的响应
我们可以添加fetch
事件的监听,并通过event.respondWith
劫持响应:
this.addEventListener('fetch', function(event) {
event.respondWith(
//
);
});
例如,我们可以尝试先匹配缓存,若不存在,则请求网络:
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
资源缓存策略
安装:缓存关键资源
在安装过程中,我们可以将运行Web应用所需的静态资源(html、js、css、图片...)加入缓存,如同之前在介绍安装和激活
时所作的那样。
而有些资源,并非关键的依赖项,允许缓存失败:
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('mygame-core-v1').then(function(cache) {
cache.addAll(
// 非依赖项
);
return cache.addAll(
// 关键资源
);
})
);
});
非依赖项即使缓存失败,应用在离线状态下依然可用。
激活:移除旧缓存
在active
事件中,新版本的SW已经激活,旧版本的SW退出,此时我们可以通过caches.keys
、caches.delete
删除旧的缓存。
this.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.filter((cacheName) => {
// 判断是否要移除该缓存
}).map((cacheName) => {
return caches.delete(cacheName);
})
);
})
);
});
预缓存资源
caches api在页面中同样可以使用,因此我们可以想象这样一个功能:我们在点击某本书的“加入书架”按钮时,下载并缓存这本书的前几章内容,以便用户之后在离线状态下也能阅读书籍的前几章:
onAddBook({ bookId }) {
caches.open(`mysite-books-${bookId}`).then((cache) => {
return fetch(`/book/${bookId}/chapters`).then((response) => {
// 缓存前十章内容
return response.json().data.slice(0, 10).map(chapter => chapter.url)
}).then((urls) => {
cache.addAll(urls);
})
})
}
网络响应与缓存策略
仅使用缓存
event.respondWith(caches.match(event.request));
仅使用网络
event.respondWith(fetch(event.request));
缓存优先
我们可以先尝试在缓存中寻找资源,若不存在,则请求网络资源,同时在网络资源返回时放入缓存:
event.respondWith(
caches.open('mysite-dynamic').then((cache) => {
return cache.match(event.request).then((response) => {
return response || fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
});
})
);
网络优先
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(event.request);
})
);
缓存优先并更新
对于以上的例子,我们可以优先使用缓存,但同时仍然发起网络请求并更新资源:
event.respondWith(
caches.open('mysite-dynamic').then((cache) => {
return cache.match(event.request).then((response) => {
const fetchPromise = fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
return response || fetchPromise;
});
})
);
自定义404/离线页面
我们还可以自定义离线响应:
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
}).catch(() => caches.match('/offline.html'));
);
需要注意的是,缓存空间在资源紧张时会被回收
库与工具
Workbox
Workbox是谷歌推出的方便大家使用PWA/SW的库。
我们可以在Service Worker库中引入Workbox并使用:
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.6.1/workbox-sw.js');
workbox.routing.registerRoute(
/.*\.css/,
workbox.strategies.staleWhileRevalidate({
cacheName: 'css-cache',
})
);
在以上例子中,我们为css
文件资源进行了缓存,使用的策略是之前介绍的“缓存优先并更新”。
Workbox提供了不同的缓存策略,例如:CacheFirst,NetworkFirst,StaleWhileRevalidate等等。
workbox-webpack-plugin
不仅如此,Workbox还提供了workbox-webpack-plugin方便我们在Webpack打包的项目中更方便的使用Workbox。
它提供的GenerateSW
插件会在打包时生成service-worker.js
,我们只需引用它就行了。
workbox-webpack-plugin,为我们完成SW相关的许多任务,包括但不限于:
- 缓存静态资源
会为我们生成precache-manifest.<revision>.js
文件,其内容形似:
self.__precacheManifest = [
{
"revision": "bd5a1eafb4eb894699d3",
"url": "../a.47886103.css"
},
{
"revision": "15f2587b088bb42d8017",
"url": "../b.7e08be0e.js"
},
生成的SW文件会加载manifest文件并使用workbox.workbox.precaching.precacheAndRoute
添加缓存:
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
// precacheAndRoute
precache(entries);
addRoute(options);
// precache
self.addEventListener('install', (event) => {
event.waitUntil(
precacheController.install({event, plugins})
.catch((error) => {
...
})
);
});
- 自动生成route注册代码
根据Webpack配置中的runtimeCaching
设置,自动生成代码调用workbox.routing.registerRoute
注册请求过程中的缓存策略。runtimeCaching形似:
runtimeCaching: [
{
urlPattern: /api/,
handler: 'networkFirst',
cacheName: 'api-cache',
},
{
urlPattern: /*.(jpg|png|gif)$/,
handler: 'staleWhileRevalidate',
cacheName: 'image-cache',
},
...
]
register-service-worker
最后,我们可以使用register-service-worker来简化SW的注册和回调钩子:
import { register } from 'register-service-worker'
register('/service-worker.js', {
registrationOptions: { scope: './' },
ready (registration) {},
registered (registration) {},
cached (registration) {},
updatefound (registration) {},
updated (registration) {},
offline () {},
error (error) {}
})