PWA学习笔记

PWA(Progressive Web Apps)学习笔记

什么是PWA

PWA = 普通的网站 + manifest + Service Workers

manifest文件包含网站相关的信息,包括图标,背景屏幕,颜色和默认方向。

Service Workers为网站提供了更好的体验(渐进增强),允许将网站添加到设备的主屏幕,离线缓存。

PWA应该具备的特性:

  • 响应式的 - 它适应较小的屏幕尺寸
  • 连接无关 - 由于 Service Worker 缓存,它可以离线工作
  • 应用式的交互 - 它使用应用外壳架构进行构建
  • 始终保持最新 - 感谢 Service Worker 的更新过程
  • 安全的 - 它通过 HTTPS 进行工作
  • 可发现的 - 搜索引擎可以找到它
  • 可安装的 - 使用清单文件
  • 可链接的 - 可以简单的通过 URL 来共享

何为Service Workers

Service Workers由JavaScript编写,运行在浏览器后台,基于事件驱动。如果用户浏览器不支持Service Workers的话,并不会造成影响,网站还可以作为普通网站进行浏览,因此做到了“渐进增强”。

通过Service Workers,可以缓存 UI 外壳(用户界面所必需的最小化的 HTML、CSS 和 JavaScript),动态内容在UI外壳加载后再加载,为用户提供类似原生app的体验。

  • Service Workers运行在自己的全局脚本上下文中
  • 不绑定到具体的网页
  • 无法修改网页中的元素,因为它无法访问 DOM
  • 只能使用 HTTPS(localhost本地开发除外)
  • Service Workser运行在不同的线程中,不会被阻塞

Service Workers生命周期

从生命周期图中可以看出,当第一次加载页面时,并不会有激活的 Service Worker 来控制页面。只有当 Service Worker 安装完成并且用户刷新了页面或跳转至网站的其他页面,Service Worker 才会激活并开始拦截请求。
如果需要在第一次加载时,就希望Service Workers激活并开始拦截请求,可以通过如下方式立即激活Service Workers。

self.addEventListener('install', function(event) {
  //使 Service Worker 解雇当前活动的worker, 并且一旦进入等待阶段就会激活自身,触发activate事件
  event.waitUntil(self.skipWaiting());
});

结合self.clients.claim() 一起使用,以确保底层 Service Worker 的更新立即生效。

self.addEventListener('activate', function(event) {
  e.waitUntil(
        caches.keys().then(function(keyList) {
            return Promise.all(keyList.map(function(key) {
                if (key !== cacheName) {
                    console.log('[ServiceWorker] Removing old cache', key);
                    return caches.delete(key);
                }
            }));
        })
    );
    return self.clients.claim(); //确保底层 Service Worker 的更新立即生效
});

Service Workers缓存

var cacheKey = "first-pwa";  //缓存的key,可以添加多个不同的缓存

var cacheList = [   //需要缓存的文件列表
    '/',
    'index.html',
    'icon.png',
    'main.css'
];

//在安装过程中缓存已知的资源
self.addEventListener('install', event => {  //监听install事件
    event.waitUntil(  //install完成后
        caches.open(cacheKey)  //打开cache
            .then(cache => cache.addAll(cacheList))  //将需要缓存的文件加入cache列表
            .then(() => self.skipWaiting())  //使 Service Worker 解雇当前活动的worker,
                                            // 并且一旦进入等待阶段就会激活自身,触发activate事件
                                            //无需等待用户跳转或刷新页面
    );
});


//拦截fetch请求
self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request).then(response => { //如果请求的资源在缓存中
            if (response != null) return response;  //返回缓存资源

            //通过网络获取资源,并缓存
            var requestToCache = event.request.clone(); //克隆当前请求
            return fetch(requestToCache.url).then(response => {
                if (!response || response.status !== 200) {
                    return response;  //返回错误的响应
                }
                var responseToCache = response.clone(); //克隆响应
                caches.open(cacheKey)
                    .then(cache => {
                        cache.put(requestToCache, responseToCache);  //将响应添加到缓存中
                    });
                return response;  //返回响应
            });
        })
    );
});

属于Service Workers作用域范围内的所有http请求都将触发fetch事件,包括html、css、js、图片等。

拦截包含save-data的http请求头部的实例

如果用户在浏览器中启用了节省数据的功能,浏览器在每个http请求头部中会加入save-data请求头。

this.addEventListener('fetch', function (event) {
 
  if(event.request.headers.get('save-data')){
    // 我们想要节省数据,所以限制了图标和字体
    if (event.request.url.includes('fonts.googleapis.com')) {
        // 不返回任何内容
        event.respondWith(new Promise(resolve => resolve(new Response('', {
            status: 417,
            statusText: 'Ignore fonts to save data.'
            })))
        );
    }
  }
});

如何保证Service Workers能获取到最新的文件

  • 更新存储缓存的名称。
  • 缓存破坏,每次发布时更新文件的名称,如增加一个版本号等。

Web应用清单(mainifest.json)

mainifest.json需要在网页head标签中引用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato">
    <link rel="stylesheet" href="main.css">
    <link rel="manifest" href="manifest.json"/>
    <title>PWA</title>
</head>
<body>
<h1>Hello PWA!</h1>
<script type="text/javascript">
    if (navigator.serviceWorker != null) {
        navigator.serviceWorker.register('sw.js').then(registration => {
            console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }).catch(function (err) {
            console.log('ServiceWorker registration failed: ', err);
        });
    } else {
        //serviceWorker is not supported
    }
</script>
</body>
</html>

manifest.json中包含的字段主要包括:

{
  "name": "First PWA",
  "short_name": "pwa",
  "display": "standalone",
  "start_url": "/index.html",
  "theme_color": "#FFDF00",
  "background_color": "#FFDF00",
  "orientation": "landscape",
  "scope": "/",
  "icons": [
    {
      "src": "icon.png",
      "sizes": "144x144",
      "type": "image/png"
    }
  ]
}
  • name:当用户被提示安装应用时出现的文本。
  • short_name:当应用安装后出现在用户主屏幕上的文本。
  • display:显示模式,默认为browser。包括fullscreen、standalone、minimal-ui 或 browser 。

    • fullscreen:应用占用整个可用的显示区域。
    • standalone:应用以看起来像一个独立的原生应用。此模式下,用户代理将排除诸如 URL 栏等标准浏览器 UI 元素,但可以包括诸如状态栏和系统返回按钮的其他系统 UI 元素。
    • minimal-ui:此模式类似于 fullscreen,但为终端用户提供了可访问的最小 UI 元素集合,例如,后退按钮、前进按钮、重载按钮以及查看网页地址的一些方式。
    • browser:使用操作系统内置的标准浏览器来打开 Web 应用。
  • start_url:应用启动时的第一个页面。
  • theme_color:可以对浏览器的地址栏进行着色,以符合网站的主色调。
  • background_color:启动时的背景色。
  • orientation: 屏幕方向。
  • icons:当应用被添加到设备主屏幕时所显示的图标。

参考 https://developer.mozilla.org/en-US/docs/Web/Manifest

监听添加到主屏幕事件

    //监听添加到主屏幕事件
    window.addEventListener('beforeinstallprompt', function (event) {
        // //取消添加
        // e.preventDefault();
        // return false;

        event.userChoice.then(function (result) {
            console.log(result.outcome);
            if (result.outcome == 'dismissed') {

            } else {

            }
        });
    });

推送通知

目前FireFox、Chrome、Edge 已经支持 Push API。推送的过程主要分为三个步骤:

  • 客户端订阅
  • 发送需要推送的消息到push service
  • push service推送到客户端

客户端订阅

在订阅前,需要先生成VAPID, VAPID是“自主应用服务器标识” ( Voluntary Application Server Identification ) 的简称。它是一个规范,定义了应用服务器和推送服务之间的握手。

1.客户端订阅消息,此时浏览器会询问用户是否允许消息推送通知。
2.从浏览器获取PushSubscription对象,其中包含了客户端的信息,可以理解为标示设备的id。

var vapidPublicKey = 'BF0eSi4ANvVKr017Gr_Xzb-bN9l8-c3qRUHqVU6C-vFy_i3xgrKDY-13BPF5BVx93IVObJwnwrt5vjX-ltM6Uuo';

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;
    }

    function subscribeForPushNotification(registration) {
        return registration.pushManager.getSubscription()
            .then(function (subscription) {
                if (subscription) {
                    return;
                }
                return registration.pushManager.subscribe({
                    userVisibleOnly: true,
                    applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
                })
                    .then(function (subscription) {
                        var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
                        var key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
                        var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
                        var authSecret = rawAuthSecret ?
                            btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';
                        var endpoint = subscription.endpoint;
                        return fetch('http://localhost:3001/api/register', {
                            method: 'post',
                            headers: new Headers({
                                'content-type': 'application/json'
                            }),
                            body: JSON.stringify({
                                endpoint: subscription.endpoint,
                                key: key,
                                authSecret: authSecret,
                            }),
                        });
                    });
            });
    }

3.将PushSubscription发送到服务端保存。

服务端示例:

this.post('/register', 'register', async (req, res, next) => {
            try {
                let {endpoint, authSecret, key} = req.body;
                let subscriber = {
                    endpoint,
                    keys: {
                        auth: authSecret,
                        p256dh: key
                    }
                };
                subscribers.push(subscriber);
                res.apiSuccess({});
            }
            catch (err) {
                next(err);
            }
        });

发送消息到push service

通过Web Push协议将需要推送的消息发送到push service。

使用web-push的发送示例:

this.post('/send', 'send', async (req, res, next) => {
            try {
                let message = req.body.message;
                for (let subscriber of subscribers) {
                    webpush.sendNotification(
                        subscriber,
                        JSON.stringify({
                            msg:message,
                            url:'http://localhost:3001',
                            icon:''
                        })
                    );
                }
                res.apiSuccess({});
            }
            catch (err) {
                next(err);
            }
        });

push service推送到客户端

当push service收到消息后,会将消息保存起来,直到目标设备上线后将消息推送到客户端,或者消息超时不再发送。

Servicer Worker toolbox

sw-toolbox

一些工具

参考

https://github.com/SangKa/PWA-Book-CN

https://developers.google.com/web/fundamentals/push-notifications/how-push-works

https://codelabs.developers.google.com/codelabs/your-first-pwapp/#0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值