PWA(Progressive Web App)渐进式Web APP,它并非单只某一项技术,而是一系列技术综合应用的结果,其中主要包含的相关技术就是Service Worker、Cache Api、Fetch Api、Push API、Notification API 和 postMessage API。使用PWA能够给咱们带来什么好处呢?主要体如今以下几方面javascript
1 离线缓存css
2 web页面添加桌面快速入口html
3 消息推送java
简单来讲,Service Worker 是一个可编程的 Web Worker,它就像一个位于浏览器与网络之间的客户端代理,能够拦截、处理、响应流经的 HTTP 请求。它没有调用 DOM 和其余页面 api 的能力,但他能够拦截网络请求,包括页面切换,静态资源下载,ajax请求所引发的网络请求。Service Worker 是一个独立于JavaScript主线程的浏览器线程。Service Worker有以下特性:webpack
咱们须要在主线程中注册Service Worker,而且通常是在页面触发load事件以后进行注册。当Service Worker注册成功后便会进入其生命周期。scope表明Service Worker控制该路径下的全部请求,若是请求路径不是在该路径之下,则请求不会被拦截。git
// 注册service worker
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js', {scope: '/'})
.then(function (registration) {
// 注册成功
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch(function (err) {
// 注册失败:(
console.log('ServiceWorker registration failed: ', err);
});
});
复制代码
Service Worker生命周期大体以下github
install -> installed -> actvating -> Active -> Activated -> Redundantweb
在Service Worker注册成功以后就会触发install事件,在触发install事件后,咱们就能够开始缓存一些静态资。waitUntil方法确保全部代码执行完毕后,Service Worker 才会完成Service Worker的安装。须要注意的是只有CACHE_LIST中的资源所有安装成功后,才会完成安装,不然失败,进入redundant
状态,因此这里的静态资源最好不要太多。若是 sw.js 文件的内容有改动,当访问网站页面时浏览器获取了新的文件,它会认为有更新,因而会安装新的文件并触发 install 事件。可是此时已经处于激活状态的旧的 Service Worker 还在运行,新的 Service Worker 完成安装后会进入 waiting 状态。直到全部已打开的页面都关闭,旧的 Service Worker 自动中止,新的 Service Worker 才会在接下来打开的页面里生效。为了可以让新的Service Worker及时生效,咱们使用skipWaiting
直接使Service Worker跳过等待时期,从而直接进入下一个阶段。ajax
const CACHE_NAME = 'cache_v' + 2;
const CACGE_LIST = [
'/',
'/index.html',
'/main.css',
'/app.js',
'/icon.png'
];
function preCache() {
// 安装成功后操做 CacheStorage 缓存,使用以前须要先经过 caches.open() 打开对应缓存空间。
return caches.open(CACHE_NAME).then(cache => {
// 经过 cache 缓存对象的 addAll 方法添加 precache 缓存
return cache.addAll(CACGE_LIST);
})
}
// 安装
self.addEventListener('install', function (event) {
// 等待promise执行完
event.waitUntil(
// 若是上一个serviceWorker不销毁 须要手动skipWaiting()
preCache().then(skipWaiting)
);
});
复制代码
在安装成功后,便会触发activate
事件,在进入这个生命周期后,咱们通常会删除掉以前已通过期的版本(由于默认状况下浏览器是不会自动删除过时的版本的),并更新客户端Service Worker(使用当前处于激活状态的Service Worker)。编程
// 删除过时缓存
function clearCache() {
return caches.keys().then(keys => {
return Promise.all(keys.map(key => {
if (key !== CACHE_NAME) {
return caches.delete(key);
}
}))
})
}
// 激活 activate 事件中一般作一些过时资源释放的工做
self.addEventListener('activate', function (e) {
e.waitUntil(
Promise.all([
clearCache(),
self.clients.claim()
])
);
});
复制代码
在这里还有一个问题就是sw.js文件有可能会被浏览器缓存,因此咱们通常须要设置sw.js不缓存或者较短的缓存时间 更多详细参考 如何优雅的为 PWA 注册 Service Worker
以前说过,Service Worker 是能够拦截请求的,那么必定就会存在一个拦截请求的事件fetch。咱们须要在sw.js去监听这个事件。
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.open(CACHE_NAME).then(cache => {
return cache.match(event.request).then(function (response) {
// 若是 Service Worker 有本身的返回,就直接返回,减小一次 http 请求
if (response) {
console.log('cache 缓存', event.request.url, response);
return response;
} else {
if (navigator.online) {
return fetch(event.request).then(function(response) {
console.log('network', event.request.url, response);
// 因为响应是一个JavaScript或者HTML,会认为这个响应为一个流,而流是只能被消费一次的,因此只能被读一次
// 第二次就会报错 参考文章https://jakearchibald.com/2014/reading-responses/
cache.put(event.request, response.clone());
return response;
}).catch(function(error) {
console.error('请求失败', error);
throw error;
});
} else {
// 断网处理
offlineRequest(fetchRequest);
}
}
});
})
);
});
复制代码
这里咱们在fetch事件中监听请求事件,咱们经过cache.match来进行请求的比较,若是存再这个请求的响应咱们就直接返回缓存结果,不然就去请求。在这里咱们经过cache.add来添加新的缓存,他实际上内部是包含了fetch请求过程的(注意:Cache.put, Cache.add和Cache.addAll只能在GET请求下使用)。在match的时候,须要请求的url和header都一致才是相同的资源,能够设定第二个参数ignoreVary:true。caches.match(event.request, {ignoreVary: true})
表示只要请求url相同就认为是同一个资源。另外须要提到一点,Fetch 请求默认是不附带 Cookies 等信息的,在请求静态资源上这没有问题,并且节省了网络请求大小。但对于动态页面,则可能会由于请求缺失 Cookies 而存在问题。此时能够给 Fetch 请求设置第二个参数。示例:fetch(fetchRequest, { credentials: 'include' } );
Cache API 不只在Service Worker中可使用,在主页面中也可使用。咱们经过 caches.open(cacheName)来打开一个缓存空间,在,默认状况下,若是咱们不手动去清除这个缓存空间,这个缓存会一直存在,不会过时。在使用Cache API以前,咱们都须要经过caches.open先去打开这个缓存空间,而后在使用相应的Cache方法。这里有几个注意点:
在使用cache.add和cache.addAll的时候,是先根据url获取到相应的response,而后再添加到缓存中。过程相似于调用 fetch(), 而后使用 Cache.put() 将response添加到cache中
Fetch API不只能够在主线程中进行使用,也能够在Service Worker中进行使用。fetch 和 XMLHttpRequest有两种方式不一样:
当接收到一个表明错误的 HTTP 状态码时,从 fetch()返回的 Promise 不会被标记为 reject, 即便该 HTTP 响应的状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (可是会将 resolve 的返回值的 ok 属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。
默认状况下,fetch 不会从服务端发送或接收任何 cookies, 若是站点依赖于用户 session,则会致使未经认证的请求(要发送 cookies,必须设置 credentials 选项)
// Example POST method implementation:
postData('http://example.com/answer', {answer: 42})
.then(data => console.log(data)) // JSON from `response.json()` call
.catch(error => console.error(error))
function postData(url, data) {
// Default options are marked with *
return fetch(url, {
body: JSON.stringify(data), // must match 'Content-Type' header
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', // include(始终携带), same-origin(同源携带cookie), omit(始终不携带)
headers: {
'user-agent': 'Mozilla/4.0 MDN Example',
'content-type': 'application/json'
},
method: 'POST', // *GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, cors, *same-origin
redirect: 'follow', // manual, *follow, error
referrer: 'no-referrer', // *client, no-referrer
})
.then(response => response.json()) // parses response to JSON
}
复制代码
更多信息请查阅:使用 Fetch
Notification API 用来进行浏览器通知,当用户容许时,浏览器就能够弹出通知。这个API在主页面和Service Worker中均可以使用,MDN文档
// 先检查浏览器是否支持
if (!("Notification" in window)) {
alert("This browser does not support desktop notification");
}
// 检查用户是否赞成接受通知
else if (Notification.permission === "granted") {
// If it's okay let's create a notification
new Notification(title, {
body: desc,
icon: '/icon.png',
requireInteraction: true
});
}
// 不然咱们须要向用户获取权限
else if (Notification.permission !== 'denied') {
Notification.requestPermission(function (permission) {
// 若是用户赞成,就能够向他们发送通知
if (permission === "granted") {
new Notification(title, {
body: desc,
icon: '/icon.png',
requireInteraction: true
});
} else {
console.warn('用户拒绝通知');
}
});
}
复制代码
// 发送 Notification 通知
function sendNotify(title, options={}, event) {
if (Notification.permission !== 'granted') {
console.log('Not granted Notification permission.');
// 经过post一个message信号量,来在主页面中询问用户获取页面通知权限
postMessage({
type: 'applyNotify'
})
} else {
// 在Service Worker 中 触发一条通知
self.registration.showNotification(title || 'Hi:', Object.assign({
body: '这是一个通知示例',
icon: '/icon.png',
requireInteraction: true
}, options));
}
}
复制代码
咱们能够看见当咱们在Service Worker中进行消息提示时,用户可能关闭了消息提示的功能,因此咱们首先要再次询问用户是否开启消息提示的功能,可是在Service Worker中是不可以直接询问用户的,咱们必需要在主页面中去询问,这个时候咱们能够经过postMessage去发送一个信号量,根据这个信号量的类型,来作响应的处理(例如:询问消息提示的权限,DOM操做等等)
function postMessage(data) {
self.clients.matchAll().then(clientList => {
clientList.forEach(client => {
// 当前打开的标签页发送消息
if (client.visibilityState === 'visible') {
client.postMessage(data);
}
})
})
}
复制代码
在这里咱们只向打开的标签页发送该信号量,避免重复询问
因为Service Worker是一个单独的浏览器线程,与JavaScript主线程互不干扰,可是咱们仍是能够经过postMessage实现通讯,并且能够经过post特定的消息,从而让主线程去进行相应的DOM操做,实现间接操做DOM的方式。
function sendMsg(msg) {
const controller = navigator.serviceWorker.controller;
if (!controller) {
return;
}
controller.postMessage(msg, []);
}
// 在 serviceWorker 注册成功后,页面上便可经过 navigator.serviceWorker.controller 发送消息给它
navigator.serviceWorker
.register('/test/sw.js', {scope: '/test/'})
.then(registration => console.log('ServiceWorker 注册成功!做用域为: ', registration.scope))
.then(() => sendMsg('hello sw!'))
.catch(err => console.log('ServiceWorker 注册失败: ', err));
复制代码
在 ServiceWorker 内部,能够经过监听 message 事件便可得到消息:
self.addEventListener('message', function(ev) {
console.log(ev.data);
});
复制代码
// self.clients.matchAll方法获取当前serviceWorker实例所接管的全部标签页,注意是当前实例 已经接管的
self.clients.matchAll().then(clientList => {
clientList.forEach(client => {
client.postMessage('Hi, I am send from Service worker!');
})
});
复制代码
在主页面中监听
navigator.serviceWorker.addEventListener('message', event => {
console.log(event.data);
});
复制代码
3 manifest.json 做用 PWA 添加至桌面的功能实现依赖于 manifest.json,也就是说若是要实现添加至主屏幕这个功能,就必需要有这个文件
{
"short_name": "短名称",
"name": "这是一个完整名称",
"icons": [
{
"src": "icon.png",
"type": "image/png",
"sizes": "144x144"
}
],
"start_url": "index.html"
}
复制代码
<link rel="manifest" href="path-to-manifest/manifest.json">
name —— 网页显示给用户的完整名称
short_name —— 当空间不足以显示全名时的网站缩写名称
description —— 关于网站的详细描述
start_url —— 网页的初始 相对 URL(好比 /)
scope —— 导航范围。好比,/app/的scope就限制 app 在这个文件夹里。
background-color —— 启动屏和浏览器的背景颜色
theme_color —— 网站的主题颜色,通常都与背景颜色相同,它能够影响网站的显示
orientation —— 首选的显示方向:any, natural, landscape, landscape-primary, landscape-secondary, portrait, portrait-primary, 和 portrait-secondary。
display —— 首选的显示方式:fullscreen, standalone(看起来像是native app),minimal-ui(有简化的浏览器控制选项) 和 browser(常规的浏览器 tab)
icons —— 定义了 src URL, sizes和type的图片对象数组。
复制代码
使用workbox,若是使用webpack进行项目打包,咱们可使用workbox-webpack-plugin插件
Web Storage(例如 LocalStorage 和 SessionStorage)是同步的,不支持网页工做线程,并对大小和类型(仅限字符串)进行限制。 Cookie 具备自身的用途,但它们是同步的,缺乏网页工做线程支持,同时对大小进行限制。WebSQL 不具备普遍的浏览器支持,所以不建议使用它。File System API 在 Chrome 之外的任意浏览器上都不受支持。目前正在 File and Directory Entries API 和 File API 规范中改进 File API,但该 API 还不够成熟也未彻底标准化,所以没法被普遍采用。
同步的问题 就是负担大,若是有大量请求缓存在本地缓存中,若是是同步,可能负担重
resulted in a network error response: a Response whose "body" is locked cannot be used to respond to a request
这是由于在使用put的时候,是流的一个pipe操做,流是只能被消费一次的。咱们能够clone这个response或者reques参考文章
使用某些webpack插件,例如offline-plugin或者webpack-workbox-plugin
博客GitHub地址(欢迎star)
我的公众号