PWA 渐进式网络应用 - 4 - 推送通知

PWA 渐进式网络应用 - 4 - 推送通知

说明

推送通知可以很好的与用户进行互动,尤其是当他们关闭了标签页或访问其他网页的时候,体验类似于原生应用,要给用户发送通知,需要首先通过用户授权

  • Notification h5 中用来给用户发送通知
  • push API 获取服务器推送,然后通过 Service Workers 调用 Notification 发送通知

发送通知步骤

  • 浏览器会显示一个提示以询问用户是否愿意接受通知
  • 如果接受,就可以将用户的订阅详细信息保存在服务器上
  • 需要的时候使用订阅信息来发送通知,不同用户、不用设备、不同浏览器订阅信息都不一样

注意:如果用户拒绝订阅,那么不但没法向其推送消息,而且下次用户访问网站的时候也不会被提示

notification 使用

1. 没有推送的情况下显示通知

// 注册 serviceWorker
function registerServiceWorker() {
    return navigator.serviceWorker
        .register('sw.js')
        .then(registration => {
            console.log('success registe serviceworker');
            return registration;
        })
        .catch(err => console.error('Unable to register service worker.', err));
}

// 注册后显示通知
function execute() {
    registerServiceWorker().then(registration => {
        registration.showNotification('notification');
    });
}

window.addEventListener('load', function() {
    // 测试兼容性
    // 支持 serviceWorker
    if (!('serviceWorker' in navigator)) return;
    // 支持 push API
    if (!('PushManager' in window)) return;

    // 获取通知权限
    let promiseChain = new Promise((resolve, reject) => {
        return Notification.requestPermission(result => resolve(result));
    }).then(result => {
        if (result === 'granted') {
            // 有权限就直接执行,弹出通知
            execute();
        } else {
            console.log('no permission');
        }
    });
});

2. `showNotification(title, options) 参数说明

一些配置项不是所有浏览器都支持

// 注册后显示通知
function execute() {
    registerServiceWorker().then(registration => {
        registration.showNotification('notification', {
            body: 'this is body', // 内容
            icon: '/center.png', // 图标
            badge: '/min.png', // 小图标,手机上展现通知缩略信息时使用
            image: '/max.jpg', // 给用户的预览图
            vibrate: [100, 200, 200, 300], // 设置震动
            requireInteraction: true, // 显示通知直到用户交互
            actions: [
                // 给通知增加一些按钮
                {
                    action: 'f-action',
                    title: 'first-action',
                    icon: '/f.png'
                },
                {
                    action: 's-action',
                    title: 'second-action',
                    icon: '/s.png'
                }
            ]
        });
    });
}

3. 通知的行为控制

1. 点击通知行为

self.addEventListener('notificationclick', event => {
    if (!event.action) {
        // 没有点击在按钮上
        console.log('Notification click.');
        return;
    }

    // 对应 registration.showNotification(title, options) 中 options 中设置的按钮
    switch (event.action) {
        case 'f-action':
            console.log('f-action click');
            break;
        case 's-action':
            console.log('s-action click');
            break;
        default:
            console.log('Unknow action click', event.action);
    }
});

2. 监控关闭通知

// 点击关闭通知后触发
self.addEventListener('notificationclose', event => {
    console.log('notificationclose');
});

3. 通知事件的数据传递

在主程序代码中发送通知 showNotification 并传递数据,而在 service-worker 中监听事件处理,并根据数据做出响应

// index.js 主程序中发送通知
registration.showNotification('notification', {
    body: 'this is body', // 内容
    icon: '/center.png', // 图标
    data: {
        // 要发送的数据
        time: Date.now(),
        msg: 'this is index.html data'
    }
});

// service worker 通过 event.notification.data 中接收数据然后根据数据做出响应
self.addEventListener('notificationclick', event => {
    console.log(event.notification.data); //  { time: 1520928976193, msg: "this is index.html data" }
});

4. service-worker 中打开指定页面

self.addEventListener('notificationclick', event => {
    const data = event.notification.data;
    let promiseChain = Promise.resolve();
    if (!event.action) {
        // 没有点击在按钮上
        console.log('Notification click.');
        // data 中有跳转页面信息则安排跳转
        if (data && data.page) {
            promiseChain = clients.openWindow(data.page);
        }

        return;
    }

    event.waitUntil(promiseChain);
});

常用案例

1. 服务器配置推送

以 node 为例

const express = require('express');
const bodyParser = require('body-parser');

// 引用一个 node 上消息推送的模块
const webpush = require('web-push');

const app = express();

// 设置 VAPID
webpush.setVapidDetails(
    'mailto:[email protected]',
    'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY',
    'p6YVD7t8HkABoez1CvVJ5bl7BnEdKUu5bSyVjyxMBh0'
);

app.post('/register', (req, res) => {
    let endpoint = req.body.endpoint;
    // saveRegistrationDetails(endpoint, key, authSecret); 保存到服务器
    /*
      真实情况可以是从服务器批量获取 pushSubscription 订阅信息,然后推送,
      下边这个对象时模拟获取的订阅信息
   */

    const pushSubscription = {
        endpoint: req.body.endpoint,
        keys: {
            auth: req.body.authSecret,
            p256dh: req.body.key
        }
    };
    const body = 'tttt for registering';
    const iconUrl = 'http://localhost:3323/icon.png';
    // 消息推送
    webpush
        .sendNotification(
            pushSubscription,
            JSON.stringify({
                msg: body,
                url: 'http://localhost:3323/',
                icon: iconUrl
            })
        )
        .then(result => res.sendStatus(201))
        .catch(err => console.log(err));
});

2. 客户端订阅通知

客户端需要将自己与其他客户端区分的信息告诉服务器,这样服务器就知道要把信息推送给谁了,这个用于区分的信息就是订阅信息

>

// 订阅通知
let endpoint;
let key;
let authSecret;
let vapidPublicKey =
    'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY';

// 一个将 VAPID 钥从 base64 字符串转换成 Uint8 数组的函数
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;
}

// 注册 serviceWorker
if ('serviceWorker' in navigator) {
    navigator.serviceWorker
        .register('/sw.js')
        .then(registration => {
            console.log('successful', registration);
            // 查看是否已经存在订阅权限
            return registration.pushManager
                .getSubscription()
                .then(subscription => {
                    console.log(subscription);
                    if (subscription) {
                        return;
                    }
                    // 不存在就去申请一下
                    return registration.pushManager
                        .subscribe({
                            userVisibleOnly: true,
                            applicationServerKey: urlBase64ToUint8Array(
                                vapidPublicKey
                            )
                        })
                        .then(subscription => {
                            // 申请成功后处理一下秘钥
                            const rawKey = subscription.getKey
                                ? subscription.getKey('p256dh')
                                : '';
                            key = rawKey
                                ? btoa(
                                      String.fromCharCode.apply(
                                          null,
                                          new Uint8Array(rawKey)
                                      )
                                  )
                                : '';
                            const rawAuthSecret = subscription.getKey
                                ? subscription.getKey('auth')
                                : '';
                            authSecret = rawAuthSecret
                                ? btoa(
                                      String.fromCharCode.apply(
                                          null,
                                          new Uint8Array(rawAuthSecret)
                                      )
                                  )
                                : '';
                            // 将订阅信息传给服务器
                            return fetch('./register', {
                                method: 'post',
                                headers: new Headers({
                                    'content-type': 'application/json'
                                }),
                                body: JSON.stringify({
                                    endpoint: subscription.endpoint,
                                    key: key,
                                    authSecret: authSecret
                                })
                            });
                        });
                });
        })
        .catch(err => {
            throw err;
        });
}

3. service worker 中接收通知并作出响应

  • 监听 Service Workers 的 push 事件,就能获取来自推送服务器的消息
  • 接受服务器推送后,再通过 registration.showNotification() 方法就可以发出 Notification 了
// 接收通知并与之互动
self.addEventListener('push', function(event) {
    console.log('get push');
    var payload = event.data ? JSON.parse(event.data.text()) : 'no payload';
    var title = '测试通知!!';

    event.waitUntil(
        // 接收到通知后,显示
        self.registration.showNotification(title, {
            body: payload.msg,
            url: payload.url,
            icon: payload.icon
        })
    );
});

// 处理通知的点击事件
self.addEventListener('notificationclick', function(event) {
    console.log('notificationclick');
    event.waitUntil(
        clients.matchAll({ type: 'window' }).then(clientList => {
            for (var i = 0; i < clientList.length; i++) {
                var client = clientList[i];
                if (client.url == '/' && 'focus' in client)
                    return client.focus();
            }
            if (clients.openWindow) {
                return clients.openWindow('http://localhost:3323');
            }
        })
    );
    event.notification.close();
});

注意:经过测试 在 http:localhost:3323 这类的地址下(非 https)firefox 上可以正常使用、Chrome 上 pushManager.subscribe() 不能使用(但也不会报错),根据这个网站 了解到,有些功能国内不可用,这个 API 的支持度很低

移动端目前不好使

相关文章

  • PWA 渐进式网络应用 - 1 - 简介及调试方法
  • PWA 渐进式网络应用 - 2 - 使用 Service Workers
  • PWA 渐进式网络应用 - 3 - manifest.json 添加应用至桌面
  • PWA 渐进式网络应用 - 4 - 推送通知

参考

简单了解 HTML5 中的 Web Notification 桌面通知
PWA 文档

你可能感兴趣的:(PWA)