PWA (Progressive Web Apps)
webapp用户体验差(不能离线访问),用户粘性低(无法保存入口),pwa就是为了解决这一系列问题,让webapp具有快速,可靠,安全等特点
pwa一系列用到的技术
- Web App Manifest
- Service Worker
- Push Api & Notification Api
- App Shell & App Skeleton(骨架屏)
Web App Manifest
将网站添加到桌面
android配置
<link rel="manifest" href="/manifest.json">
{
"name": "应用的名称 ", // 应用名称
"short_name": "应用的名称 ", // 桌面应用的名称 ✓
"display": "standalone", // fullScreen (standalone) minimal-ui browser ✓
"start_url": "/", // 打开时的网址 ✓
"icons": [{ // 设置桌面图片 icon图标
"src": "/icon.png",
"sizes": "500x500",
"type": "image/png"
}],
"background_color": "#aaa", // 启动画面颜色
"theme_color": "#aaa" // 状态栏的颜色
}
ios配置
<!-- 图标icon -->
<link rel="apple-touch-icon" href="/icon.png" />
<!-- 添加到主屏后的标题 和 short_name一致 -->
<meta name="apple-mobile-web-app-title" content="标题">
<!-- 隐藏safari地址栏 standalone模式下默认隐藏 -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<!-- 设置状态栏颜色 -->
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
Service Worker
特点:
- 不能访问/操作DOM
- 会自动休眠,不会随浏览器关闭所失效(必须手动卸载)
- 离线缓存内容开发者可控
- 必须在https或localhost下使用
- 所有的api都基于promise
生命周期:
- 安装(installing):这个状态发生在
service work
注册之后,表示开始安装,触发install
事件回调指定一些静态资源进行离线缓存 - 安装后(installed):
service work
已经完成了安装,并且等待其他的service worker
线程被关闭。 - 激活(activating):在这个状态下没有被其他的
service worker
控制的客户端,允许当前的worker完成安装,并且清除了其他worker以及关联缓存的旧缓存资源,等待新的service worker
线程被激活。 - 激活后(activated):在这个状态会处理activate事件回调(提供了更新缓存策略机会)。并可以处理功能性的事件
fetch
(请求),sync
(后台同步),push
(推送)。 - 废弃状态(redundant):这个状态表示一个
service worker
的生命周期结束。
关键Api方法
self.skipWaiting()
: 表示强制当前处在waiting
状态的service worker
进入active
状态event.waitUntil()
: 传入一个Promise为参数,等到该Promise为resolve状态为止。self.clients.claim()
: 在active事件回到中执行该方法表示取得页面的控制权,这样之后打开页面都会使用版本更新的缓存,旧的service worker
脚本不再控制着页面,之后会被停止。
注册serviceWorker
需要等待主线程加载完毕,在注册
serviceWorker
。
window.addEventListener('load', async () => {
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.register('/sw.js')
}
})
注册监听函数
//请求拦截
self.addEventListener('fetch', e => {
})
self.addEventListener('install', e => {
//直接激活
e.waitUntil(skipWaiting());
})
缓存静态资源
安装时将缓存列表进行缓存
const CACHE_NAME = 'cache_v' + 1;
const CACHE_LIST = [
'/',
'/index.html',
'/main.js',
'/index.css',
'/api/list',
'/manifest.json',
'/icon.png'
];
async function preCache() {
//打开缓存空间
const cache = await caches.open(CACHE_NAME);
//缓存静态资源
await cache.addAll(CACHE_LIST);
await self.skipWaiting();
}
self.addEventListener('install', e => {
e.waitUnitl(preCache()); //跳转等待,直接激活
})
激活后删除无用的缓存
async function clearCache() {
const keys = await caches.keys();
await Promise.all(keys.map(key => {
if (key !== CACHE_NAME) {
return caches.delete(key);
}
}))
}
self.addEventListener('activate', e => {
//让serviceWorker拥有控制权
e.waitUnitl(Promise.all([self.clients.claim(), clearCache()]))
})
离线使用缓存
- cachefirst: 缓存优先
- cacheonly: 仅缓存
- networkfirst: 网络优先
- networkonly: 仅网络
- staleWhileRevalidate: 从缓存取,用网络数据更新缓存
使用staleWhileRevalidate
async fetchAndSave(request) {
//serviceWorker不支持ajax,但是支持fetch
const result = await fetch(request);
//为了保证不破坏原有数据
const cloneRest = res.clone();
//缓存数据
const cache = await caches.open(CACHE_NAME);
await cache.put(request, cloneRest);
return result;
}
self.addEventListener('fetch', e => {
const url = new URL(e.request.url);
if (url.origin !== self.origin) return; //静态资源走浏览器缓存
//从缓存取,用网络数据更新缓存
if (e.request.url.includes('/api')) {
return e.respondWith(
fetchAndSave(e.request).catch(_ => {
return caches.match(e.request);
})
)
}
e.respondWith(fetch(e.request).catch(_ => {
return caches.match(e.request);
}))
})
Push API
+-------+ +--------------+ +-------------+
| UA | | Push Service | | Application |
+-------+ +--------------+ | Server |
| | +-------------+
| Subscribe | |
|--------------------->| |
| Monitor | |
|<====================>| |
| | |
| Distribute Push Resource |
|-------------------------------------------->|
| | |
: : :
| | Push Message |
| Push Message |<---------------------|
|<---------------------| |
| | |
核心实现流程
Subscribe
: 向Push Service
发起订阅,获取PushSubscription
Monitor
: 实现浏览器和Push Service
通信Distribute Push Resource
: 将PushSubscription
交给服务器,用于通信Push Message
: 服务器将消息推送给Push Service
.Push Service
在推送给对应的客户端
实现推送
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
const publicKey = 'BKn9WZWSFKaRlWfxwg32xV5M_IYr_nUFRQnS8tb_fR_1X1Ga_xP2TGfObHtKZzDeVBSJfoNasD_-N5qnYyg5enc';
const convertedVapidKey = urlBase64ToUint8Array(publicKey); // 通过公钥通信确保安全, 类型要求是ArrayBuffer
window.addEventListener('load', async () => {
if ('serviceWorker' in navigator) {
let registration = await navigator.serviceWorker.register('/sw.js');
// 等待serviceWorker激活后
await navigator.serviceWorker.ready;
let pushSubsription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidKey
});
// 服务器这是我的通信对象
fetch('/add-sub', {
headers: {
"Content-Type": 'application/json'
},
method: 'post',
body: JSON.stringify(pushSubsription)
})
}
});
Notification
serviceWorker中监听服务端推送的消息
self.addEventListener('push', function(e) {
var data = e.data;
if (e.data) {
self.registration.showNotification(data.text());
} else {
console.log('push没有任何数据');
}
});