Vue2之PWA

PWA (Progressive Web Apps)

webapp用户体验差(不能离线访问),用户粘性低(无法保存入口),pwa就是为了解决这一系列问题(Progressive Web Apps),让webapp具有快速,可靠,安全等特点.

PWA一系列用到的技术

  • Web App Manifest (让网站再桌面上生成桌面图标)
  • Service Worker(离线缓存使用)
  • Push Api & Notification Api(服务端推送)
  • App Shell & App Skeleton(未加载完成的壳、正在加载的骨架)

Web App Manifest

将网站添加到桌面、更类似native的体验

Web App Manifest设置

<link rel="manifest" href="/manifest.json">
{
    "name": "PWA效果展示",  // 应用名称 
    "short_name": "PWA", // 桌面应用的名称  ✓
    "display": "standalone", // fullScreen (standalone) minimal-ui browser ✓
    "start_url": "/", // 打开时的网址  ✓
    "icons": [{ // 设置桌面图片 icon图标
        "src": "/icon.png",
        "sizes": "144x144",
        "type": "image/png"
    }],
    "background_color": "#aaa", // 启动画面颜色
    "theme_color": "#aaa" // 状态栏的颜色
}

ios 不支持 manifest文件,可以通过 meta/link 私有属性进行设置

<!-- 图标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

Service Worker特点:

  • 不能访问/操作dom

  • 会自动休眠,不会随浏览器关闭所失效(必须手动卸载)

  • 离线缓存内容开发者可控

  • 必须在https或者localhost下使用

  • 所有的api都基于promise

生命周期

  • 安装( installing ):这个状态发生在 Service Worker 注册之后,表示开始安装,触发 install 事件回调指定一些静态资源进行离线缓存。
  • 安装后( installed ):Service Worker 已经完成了安装,并且等待其他的 Service Worker 线程被关闭。
  • 激活( activating ):在这个状态下没有被其他的 Service Worker 控制的客户端,允许当前的 worker 完成安装,并且清除了其他的 worker 以及关联缓存的旧缓存资源,等待新的 Service Worker 线程被激活。
  • 激活后( activated ):在这个状态会处理 activate 事件回调 (提供了更新缓存策略的机会)。并可以处理功能性的事件 fetch (请求)、sync (后台同步)、push (推送)。
  • 废弃状态 ( redundant ):这个状态表示一个 Service Worker 的生命周期结束。

关键方法

  • self.skipWaiting():表示强制当前处在 waiting 状态的 Service Worker 进入 activate 状态
  • event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。
  • self.clients.claim():在 activate 事件回调中执行该方法表示取得页面的控制权, 这样之后打开页面都会使用版本更新的缓存。旧的 Service Worker 脚本不再控制着页面,之后会被停止。

index.html

  • index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <link rel="stylesheet" href="/index.css">
        <link rel="manifest" href="/manifest.json">
    </head>
    <body>
        <div>
            <h1>PWA 效果展示</h1>
            <ul></ul>
        </div>
        <script src="/main.js"></script>
    </body>
    </html>
    

    页面中分别引入 css、js文件 及引入manifest.json

main.js

  • main.js

    const ul = document.querySelector('ul');
    fetch('/api/list').then(res => res.json()).then(data => {
        data.forEach(item => {
            let li = document.createElement('li');
            let img = document.createElement('img');
            img.src = item;
            li.appendChild(img);
            ul.appendChild(li);
        });
    })
    

    main.js中会调用服务端接口获取数据进行页面渲染。

    核心实现:实现离线访问功能、并离线缓存接口数据

在index.html中注册serviceWorker

  • index.html

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <link rel="manifest" href="/manifest.json">
    
        <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" />
        <link rel="stylesheet" href="./index.css">
    </head>
    
    <body>
        <h1>PWA 网站</h1>
        <ul></ul>
        <script src="/main.js"></script>
        <script>
            // 给网站注册serviceWorker, 注册时机  等待资源加载完毕后 ,在开线程
            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 convertedVapidKey = urlBase64ToUint8Array('BKn9WZWSFKaRlWfxwg32xV5M_IYr_nUFRQnS8tb_fR_1X1Ga_xP2TGfObHtKZzDeVBSJfoNasD_-N5qnYyg5enc');
    
    
            window.addEventListener('load', async () => {
                if ('serviceWorker' in navigator) {
                    // web worker
                    let registration = await navigator.serviceWorker.register('/sw.js');
    
                    // 等待serviceWorker 激活成功成 订阅
                    await navigator.serviceWorker.ready;
                    let pushSubscription = await registration.pushManager.subscribe({
                        userVisibleOnly: true,
                        applicationServerKey: convertedVapidKey
                    });
    
                    // 拿到pushSubscription 可以推送消息, 我将他转交给我们的服务器,我们的服务器可以用他来通知pushService -> 浏览器
                    fetch('/add-sub',{
                        headers:{
                            'Content-Type':'application/json'
                        },
                        method:'post',
                        body:JSON.stringify(pushSubscription)
                    })
                }
            })
        </script>
    
    </body>
    
    </html>
    
sw.js
  • sw.js

    • 注册监听函数
    • 缓存静态资源
      • 安装时将缓存列表进行缓存操作
      • 激活后删除无用的缓存
    • 离线使用缓存
    // 对资源进行离线缓存  seriveWorker 可以自定义缓存的内容
    const CACHE_NAME = 'cache_v' + 2;
    const CAHCE_LIST = [ // 列表越长 越容易出问题
        '/',
        '/index.html',
        '/main.js',
        '/index.css',
        '/api/list',
        '/manifest.json',
        '/icon.png'
    ];
    // 当断网时 我需要拦截请求, 使用缓存的结果 
    
    // 核心就是拦截请求
    async function fetchAndSave(request){
        let res = await fetch(request); // 数据流
        let cloneRes = res.clone(); // 为了保证不破坏原有的响应结果
        let cache = await caches.open(CACHE_NAME);
        await cache.put(request,cloneRes); // 用响应结果更新缓存
        return res;
    }
    self.addEventListener('fetch', (e) => {
        // 如果是静态资源 不做拦截
        // serviceWorker中不支持ajax,但是支持fetch
        let 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(res => {
                     return caches.match(e.request);
                })
            )
        }
    
        // 如果断网了, 抛出异常
        e.respondWith(
            fetch(e.request).catch(res => {
                 return caches.match(e.request);
            })
        )
    });
    
    // 当serviceWorker 安装时 需要跳过等待
    // 预先将缓存列表的数据缓存起来
    async function preCache() {
        let cache = await caches.open(CACHE_NAME); // 创建一个缓存空间
        await cache.addAll(CAHCE_LIST);
        await self.skipWaiting()
    }
    self.addEventListener('install', (e) => {
        // e.waitUtil表示等待promise执行完成
        // 预先将缓存列表的数据缓存起来
        e.waitUntil(preCache())
    })
    async function clearCache() {
        let keys = await caches.keys();
        return Promise.all(keys.map(key => {
            if (key !== CACHE_NAME) {
                return caches.delete(key)
            }
        }))
    }
    // serviceWorker 不是立即生效,需要在下一次访问的时候才生效
    self.addEventListener('activate', (e) => {
        e.waitUntil(Promise.all([clearCache(), clients.claim()])); // 激活后立刻让serviceWorker拥有控制权
    })
    
    // workbox ->workbox-webpack-plugin
    
    self.addEventListener('push',function (e) {
        self.registration.showNotification(e.data.text())
    })
    

缓存策略

基于workbox 缓存类型

  • cachefirst 缓存优先
  • cacheonly 仅缓存
  • networkfirst 网络优先
  • networkonly 仅网络
  • StaleWhileRevalidate 从缓存取,用网络数据更新缓存

Push Api

Web Push Protocol

 +-------+           +--------------+       +-------------+
 |  UA   |           | Push Service |       | Application |
 +-------+           +--------------+       |   Server    |
     |                      |               +-------------+
     |      Subscribe       |                      |
     |--------------------->|                      |
     |       Monitor        |                      |
     |<====================>|                      |
     |                      |                      |
     |          Distribute Push Resource           |
     |-------------------------------------------->|
     |                      |                      |
     :                      :                      :
     |                      |     Push Message     |
     |    Push Message      |<---------------------|
     |<---------------------|                      |
     |                      |                      |

核心实现流程

  • Subscribe: 向Push Service发起订阅,获取PushSubscription
  • Monitor: 实现浏览器和PushService通信 (用户离线,PushSevice会维护消息列表)
  • Distribute Push Resource: 将PushSubscription 交给服务器,用于通信
  • Push Message: 服务端将消息推送给 Push ServicePush Service在推送给对应的客户端

实现服务端推送

server.js

const express = require('express');
const fs = require('fs');
const path = require('path');
const bodyParser = require('body-parser');
const webpush = require('web-push');
const app = express();
let list = require('./data.json');

const vapidKeys = {
    publicKey: 'BKn9WZWSFKaRlWfxwg32xV5M_IYr_nUFRQnS8tb_fR_1X1Ga_xP2TGfObHtKZzDeVBSJfoNasD_-N5qnYyg5enc',
    privateKey: 'bmsKpg6rE-K-LgU_DAIPynBdD8AK8hal8IMfYo3IyVc'
}
const subs = [];
app.use(bodyParser.json())
app.use(express.static(path.join(__dirname)));
app.use(express.static(path.join(__dirname, 'public')))
app.get('/api/list', (req, res) => {
    let start = Math.floor(Math.random() * (list.length - 20));
    res.json(list.slice(start, start + 20));
});
app.post('/add-sub', (req, res) => {
    subs.push(req.body);
    res.json({ data: 'ok' })
});
webpush.setVapidDetails(
    'mailto:1035465284@qq.com',
    vapidKeys.publicKey,
    vapidKeys.privateKey
)
app.get('/server-push', (req, res) => {
    subs.forEach(sub => webpush.sendNotification(sub, JSON.stringify('hello zf')));
    res.end();
});
app.listen(3000,()=>{
    console.log(`pwa server port:3000`)
});

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="manifest" href="/manifest.json">

    <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" />
    <link rel="stylesheet" href="./index.css">
</head>

<body>
    <h1>PWA 网站</h1>
    <ul></ul>
    <script src="/main.js"></script>
    <script>
        // 给网站注册serviceWorker, 注册时机  等待资源加载完毕后 ,在开线程
        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 convertedVapidKey = urlBase64ToUint8Array('BKn9WZWSFKaRlWfxwg32xV5M_IYr_nUFRQnS8tb_fR_1X1Ga_xP2TGfObHtKZzDeVBSJfoNasD_-N5qnYyg5enc');


        window.addEventListener('load', async () => {
            if ('serviceWorker' in navigator) {
                // web worker
                let registration = await navigator.serviceWorker.register('/sw.js');

                // 等待serviceWorker 激活成功成 订阅
                await navigator.serviceWorker.ready;
                let pushSubscription = await registration.pushManager.subscribe({
                    userVisibleOnly: true,
                    applicationServerKey: convertedVapidKey
                });

                // 拿到pushSubscription 可以推送消息, 我将他转交给我们的服务器,我们的服务器可以用他来通知pushService -> 浏览器
                fetch('/add-sub',{
                    headers:{
                        'Content-Type':'application/json'
                    },
                    method:'post',
                    body:JSON.stringify(pushSubscription)
                })
            }
        })
    </script>

</body>

</html>

Notification

ServieWorker中监听服务端推送的消息

self.addEventListener('push', function (e) {
    var data = e.data;
    if (e.data) {
        self.registration.showNotification(data.text());        
    }  else {
        console.log('push没有任何数据');
    }
});

可以实现APP中信息推送的功能,当然可以直接使用Notification API ,但是大部分情况,我们还是会配合Push Api进行使用

Workbox资料

  • https://blog.csdn.net/haosicx/article/details/132025426
  • https://cloud.tencent.com/developer/article/2169293
  • https://www.cnblogs.com/lcosima/p/12342611.html
  • https://webpack.docschina.org/guides/progressive-web-application/
  • https://www.jianshu.com/p/54cc04190252
  • workbox-webpack-plugin
  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值