PWA使用

189 阅读3分钟

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

Web Push Protocol

 +-------+           +--------------+       +-------------+
 |  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没有任何数据');
    }
});