PWA必须在 HTTPS 环境下才能工作或者http://localhost
各项技术相互之间没有依赖,可以独立实施。如果某项技术在客户端上不支持,那就对其无效,仅此而已。实施新特性无需破坏应用的向后兼容性。
借助App manifest,允许用户将应用添加到桌面。
借助 Service Worker,可以在离线或低速网络状态下工作。
可以和app一样,拥有首屏加载动画,可以隐藏地址栏等沉浸式体验
通过 Web Push API 实现消息推送, Notifications API 实现桌面通知,能够吸引用户从浏览器外再次访问。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xv0KuaWL-1586084403618)(./imgs/manifest配置.png)]
service-worker是一种客户端代理
浏览器的 JavaScript 都是运行在一个主线程上,随着业务不断复杂,性能问题不断凸显。W3C 提出了 Web Worker API,将一些耗时、耗资源的任务交给这个 API,完成后通过 post Message 方法告诉主线程,主线程通过 onMessage 方法得到反馈结果。但 Web Worker 是临时的,每次进行的操作不能被持久化保存下来,不能解决重复访问时的耗时问题。在此基础上,Service Worker 被提出,在 Web Worker 的基础上增加了持久的离线缓存能力。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lX4ltgix-1586084403622)(./imgs/service-worker.png)]
生命周期
parse
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('service-worker.js')
.then(function() { console.log('Service Worker Registered');
});
}
install
install阶段主要是添加缓存文件到指定缓存中
const PRECACHE_URLS = ["../", "../styles/index.css", "../scripts/index.js"]
self.addEventListener('install',event=>{
event.waitUntil(
caches.open('shell')
.then(cache => cache.addAll(PRECACHE_URLS))
.then(self.skipWaiting())
)
})
self.skipWaiting() 用于跳过等待状态,这个方法主要涉及新的 Service Worker 安装和老的 Service Worker 废弃的过程,即 Service Worker 的更新。一般情况下,新的 Service Worker 安装完成后将会进入等待状态,需要在老的 Service Worker 停止工作(一般是关闭浏览器)后才会取代。
由于系统会随时休眠 Service Worker,为了防止执行中断,需要使用 event.waitUntil() 进行捕获,它会监听异步请求返回的 promise,如果其中有 reject 的情况,则会导致 Service Worker 开启失败。
activate
Service Worker 处于 activated 状态下时可以处理事件,如请求拦截与缓存捕获。在这之前,我们可以监听 activate 事件,在回调函数中对旧的无用缓存文件进行清理。
self.addEventListener("activate",event => {
const currentCaches = [SHELL,RUNTIME];
event.waitUntil(
caches.keys()
.then(cacheNames => {
return cacheNames.filter(
cacheName => !currentCaches.includes(cacheName)
);
})
.then(cachesToDelete => {
return Promise.all(
cachesToDelete.map(cacheToDelete => {
return caches.delete(cacheToDelete);
})
)
})
.then(() => self.clients.claim()) //self.clients.claim() 做的是在不重新加载的前提下取得页面控制权。
)
})
fetch
请求拦截与缓存捕获,
简单的例子:
self.addEventListener('fetch', event=>{
// 响应页面的请求
event.respondWith(
// 判断缓存当中是否有该请求,有的话就使用缓存
caches.match(event.request)
.then(response => {
if(response){
return response;
}
return fetch(event.request);
})
)
})
首先我们需要监听浏览器本身的 fetch 事件,respondWith 用来响应页面的请求。这里使用了 Catch API 的 match 方法来查找 Cache 中是否存在与 request 请求匹配的缓存,如果不存在则再通过 Fetch API 进行远程请求。
如果我们在 install 时把页面和相关的资源缓存下来,在这一步已经能够实现页面的离线访问了。
这段代码可以进行优化,当没有命中 cache 进行远程请求后,可以将 fetch 的内容加入缓存中,这样这些资源在下一次访问的时候就可以直接使用了。
self.addEventListener('fetch', event => {
// 判断请求地址和当前地址是否同源
if(event.request.url.startsWith(self.location.origin)){
// 响应页面请求
event.respondWith(
// 匹配名为RUNTIME的缓存,类似于连接数据库
caches.open(RUNTIME)
.then(cache => {
return cache.match(event.request).then(response => {
// 请求最新的数据替换缓存中旧的内容
var fetchPromise = fetch(event.request).then(networkResponse => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
})
return response || fetchPromise;
})
})
)
}
})
值得注意的是这里的 response 需要给浏览器进行渲染,并同时保存的缓存中。由于 caches.put 使用的是文件的响应流,一旦使用就会造成 response 无法访问(可以理解为破坏性读出),所以需要事先使用 clone 方法复制一份。