Notification with Service Workers push events

系列文章:

  1. Service Workers 和离线缓存
  2. Notification with Service Workers push events (本文)
  3. PWA:添加应用至桌面及分享

Notification

HTML5 Notification 已经推出挺久了,它可以用来给用户发送通知提示。

一直想试一试给自己的博客用上这个功能。上一篇成功升级 https 之后,终于可以来捣鼓一下了。捣鼓之前,还是先来看一下浏览器支持情况。

Notification 浏览器支持情况

Can I use Notification
Can I use Notification

从上图中可以看到,除了我行我素的 IE 之外,其他桌面浏览器都已经支持 Notification;与之相反,移动端一片血红,几乎全军覆没。自己玩就不用在意这些了,而且 Notification 已加入标准,移动端浏览器最终也会响应号召的🙃。

So, JUST DO IT.

虽然,桌面浏览器已经基本支持 Notification,但 Notification 之中还有很多配置项。之前,有看到过大神写的一篇关于 Notification 的文章,上面列举了 Notification 的属性,比如,sound, vibrate, image 等。于是,上 MDN 看了下它们的支持情况,

Notification API support
Notification API support

同样也是一大片血红,普遍也就只支持最基础的功能。

小试牛刀

想要尝试 Notification 非常方便,打开浏览器的 console 就可以了。

首先,申请推送的权限,在 console 中输入

Notification.requestPermission();

就可以看到浏览器左上角弹出提示问你是否允许推送。

权限有 3 种状态:granted(同意), denied(拒绝)和 default,默认是 default。默认权限浏览器行为和拒绝相同,不会发起推送,只有在获得用户同意后,浏览器才会发起推送。

requestPermission 会返回一个 Promise,当用户选择后,会将用户所做的决定(即granted, denied, default)作为参数传递给 then 方法。

获得了用户同意的授权之后,就可以发起推送了。发起推送也很简单,只需创建一个 Notification 对象。

// new Notification(title[, options]);
new Notification('Hello world.');

通常情况下,这时你就能看到屏幕右上角会弹出个小框。不过总会遇到一些特例:mac 下 chrome 满屏状态下 Notification 不会实时弹出,只有切换到桌面状态下才可能弹出。然而,它就像个磨人的小妖精,你不知道它会在什么时候弹出,可能是下一秒,可能是一个小时以后,也可能是明天...(firefox 和 safiri 满屏下没有这个问题)

其他具体的一些 API 就不细讲了,有兴趣的可以看 MDN 的文档,或者之前提到的那篇文章。

试过了最基本的 Hello world,那么,再进一步试着搞到项目中看看。

当今,人们都睡得比较晚,有的是工作原因,有的是因为有晚睡强迫症,也有时是专注于什么一下子忘了时间。

这时就可以用 Notification 来做个提示,提醒自己早点休息。

const NOTIFICATION_API = 'Notification';
const PERMISSION_GRANTED = 'granted';
const NOTIFICATION_START_TIME = 23;
const NOTIFICATION_END_TIME = 6;
const DELAY_MINUTES = 5;
const NOTIFICATION = {
    title: '夜深了',
    delay: DELAY_MINUTES * 60 * 1000, // 5 minutes
    options: {
        body: '亲,工作之余,也要注意身体噢...',
        icon: '/favicon.ico'
    }
};

const isSupportNotification = () => NOTIFICATION_API in window;
const getPermission = () => Notification.permission;
const isPermissionGranted = permission => permission === PERMISSION_GRANTED;

const registerNotification = () => {
    const now = new Date();
    const nowHour = now.getHours();
    // Time in the notification time block
    if (nowHour <= NOTIFICATION_END_TIME || nowHour >= NOTIFICATION_START_TIME) {
        // Show notification 5 minutes later
        setTimeout(() => new Notification(NOTIFICATION.title, NOTIFICATION.options), NOTIFICATION.delay);
    } else {
        // Show notification at 11 o'clock.
        const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), NOTIFICATION_START_TIME, DELAY_MINUTES);
        setTimeout(() => new Notification(NOTIFICATION.title, NOTIFICATION.options), start.valueOf() - now.valueOf());
    }
};

if (isSupportNotification()) {
    if (isPermissionGranted(getPermission())) {
        registerNotification();
    } else {
        Notification
            .requestPermission()
            .then(isPermissionGranted)
            .then(granted => granted && registerNotification());
    }
} else {
    console.info('Browser not support Notification.');
}

有兴趣的话,你还可以多捣鼓几个。但是,这些提示说起来都是程序写死的,当页面加载之后就决定了它显示的时间,而不是动态产生的。如果,想要发送动态提示,这就需要客户端与服务器端的配合,还是先来看客户端。

通过 service workers push events 来接收消息

上一篇中已经成功地在客户端注册了 service workers,通过它来获取服务端发送的消息就很简单了。

监听 Service workers 中的 push 事件,就能获取来自推送服务器的消息,再通过 registration.showNotification 方法就能发出 Notification 了。

// service-worker.js
// ...
const onPush = function(event) {
    event.waitUntil(_self.registration.showNotification('New Post Arrival', {
        icon: '/logo.png'
    }));
};

_self.addEventListener('push', onPush);

现在就可以打开 firefox 试一试了,打开 service workers 调试页,点击推送就可以预览效果了。(为什么不用 chrome?这个问题后面会说...)

是不是以为这样就完成了?那就错了,这才刚刚完成了一半,服务器怎么知道是给你发推送,而不是隔壁老王?

客户端订阅

这就需要客户端将自己与其他客户端区分的信息告诉服务器,而这个信息就是订阅信息,在 service workers 注册时可以拿到。我们再修改一下之前的代码...

// ServiceWorkerService.js
// ...
sw.register(SERVICE_WORKER_FILE_PATH)
        .catch(() => console.error('Load service worker fail'))
        .then(registration =>
            registration
                .pushManager
                .getSubscription()
                .then(subscription => subscription || registration.pushManager.subscribe({ userVisibleOnly: true })))
        .then(subscription => {
            const endpoint = subscription.endpoint;

            const options = {
                method: 'post',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ endpoint })
            };

            return httpFetch(SUBSCRIBE_API, options);
        })
        .catch(error => console.error('Subscribe Failure: ', error.message))
        .then(() => sendMessageToSW('Hello, service worker.'))
        .catch(() => console.error('Send message error.'));

在注册 service worker 时,先通过 pushManager.getSubscription 方法获取当前客户端是否已经订阅过了,没有订阅则通过 pushManager.subsribe 方法来获取一个订阅;接着就将订阅信息发送给后端,交由后端储存起来,服务端的接口这里就不贴了,有兴趣的看 Github 上的代码吧。

订阅信息是最重要的资料,需要妥善保存,一旦泄露别人就能轻易冒充你了。

订阅信息会过期,所以不要忘了在 servier worker 中监听 pushsubscriptionchange 事件,当订阅过期后自动重新订阅。

拿到了订阅信息,接着就可以来推送消息了。不过得先说明一点,这里所说的服务器推送与 http2 的 server push 没有任何关系(虽然,之前我一直是这么认为的...彡(-_-;)彡)。

打个岔

说到 http2,就顺便说一个 nginx 升级 http2 时遇到的问题。ubuntu 14.04 下需要将 OpenSSL 升级至 1.0.2,nginx 才能开启 http2。

Note that accepting HTTP/2 connections over TLS requires the “Application-Layer Protocol Negotiation” (ALPN) TLS extension support, which is available only since OpenSSL version 1.0.2. Using the “Next Protocol Negotiation” (NPN) TLS extension for this purpose (available since OpenSSL version 1.0.1) is not guaranteed.

但如果,和我一样使用 nginx docker 镜像的话,使用 alpine 版本就能开启 http2,而不必操心上面所提的了。

服务器推送

言归正传,这里的服务器推送是基于发布/订阅模式构成的一套体系,通过客户端的订阅行为向服务器注册,当服务器广播消息时,将消息传递给推送服务,再由推送服务器给客户端推送消息。

你可能会像我一样纳闷,推送服务是什么鬼?自己的服务器支持 http2,可以 server push 那是不是可以直接推送消息,而不通过推送服务哪?答案是,No way。这里的推送服务(Push Service)指的是 google 的 fcm (以前叫 gcm),或者 apple 的 APNs(苹果现在还不支持 webpush)等。这点可以从上面 firefox 截图中的推送服务后的字符串看出端倪 https://updates.push.services.mozilla.com/wpush/...,同时,它也是客户端提交给服务器的订阅信息。

知道了这些就能理解规范上的 webpush 架构了。

Webpush Architecture
Webpush Architecture

要发送通知时,服务器端取出之前客户端上传的订阅信息,即刚提到的 url 地址,往这个地址发一个 post 请求就可以了,剩下的事推送服务会替你完成。

// publish.js
// ...
    .post('/broadcast', async ctx => {
        await readEndpoints()
            .then(endpoints => {
                ctx.status = 200;
                ctx.body = {};

                endpoints.forEach(endpoint => {
                    webPush.sendNotification({ endpoint })
                        .catch(console.error);
                });
            })
            .catch(err => {
                ctx.status = 500;
                ctx.body = err;
            });
    });

web-push & payload

给客户端发送推送内容时,需要对推送内容进行加密,这里使用了 web-push 这个库来帮加密内容,并将消息传递给推送服务器。实现了推送服务后,就不用再通过控制台去模拟推送服务了。

上面是最基础的用法,如果要带上 message,就需要在客户端注册时向后端传递 p256dhauth。服务器发送消息时,通过这两个值来给 message 加密,当然加密的过程都交给 web-push 来做。通过 Postman 发个消息👀

同之前测试一样,在系统右上角弹出了提示,通常通知都可以被点击,这点 web notification 也可以做到...

响应点击事件

在 service workers 中可以监听 notification 的 click 事件,再通过 clients 操作,就能达到一些诸如打开一个新页面等类似的效果。

// service-worker.js
// ...
const onNotificationClick = function(event) {
    event.notification.close();

    event.waitUntil(clients.openWindow(event.notification.data));
};

_self.addEventListener('notificationclick', onNotificationClick);

现在点击推送,就会打开我的网站啦~😁

可惜的是,当浏览器关闭时,推送就接收不到了。

搞完了 firefox,但也不能忘了老朋友 chrome,之前有提到现在这套代码在 chrome 下无法成功订阅,如果想要 chrome 支持,那么还得用上古哥服务。

配置古哥服务

重要提示:使用 google 服务需要科学上网...

想要 chrome 下 service workers 能够发出 Notification 并不复杂,只需以下几步:

  1. 由于,GCM 已经被 FCM(Firebase Cloud Messaging) 替代,所以,先要先开通 firebase
  2. 创建一个项目
  3. 查看 setting(⚙)中的 cloud messaging 信息(Server keySender ID)
  4. 客户端根目录下添加 manifest.json,并设置 gcm_sender_idapplicationServerKey,分别对应项目的Sender IDServer key
  5. 服务器端,在使用 web-push 调用 sendNotification API 时添加 gcmAPIKey(填 Server key

通过这几步,chrome 就和 firefox 一样可以接受通知消息了。因为,项目之前没有使用 Firebase,所以,个人没有直接使用它所提供的 API 来发送通知。如果,你的项目中已经用到了 Firebase,那么,你可以根据手册直接使用 firebase 封装后的 API 来接收消息,那样可能会更简单一点。

配置成功之后,就可以试试 Notification 在移动端 chrome 下的效果。

notification on mobile
notification on mobile

服务器发出消息后,notification 就会出现在系统的消息提示栏里,点击通知也会打开新的页面。(和桌面端一样,浏览器彻底关闭后就无法接收到消息了)

是不是很酷~

再次提示:使用 google 服务需要科学上网...

摊手
摊手

(google 翻译摊手竟然是 tanshou...�😂)

最后

至此,从客户端订阅,到服务器发送推送消息,再到客户端接收推送消息一整套的功能就完成了。尽管,无论是桌面端还是移动端在浏览器关闭的情况下,Service Workers 都无法接收到推送消息,但这个功能还是能够极大得增加用户的粘性,尤其在桌面端(大多情况都会打开着浏览器)。

Tips: 开发时,记得勾选 Application -> Service Workers 下的 Update on reloadBypass for network,这样 service worker 的更新会被立即应用。

同时,推荐 mozilla 的 Service Workers Cookbook 真的很棒!论文档、Demo,M 家优势明显。

如果,你喜欢我的文章,欢迎来我的博客并开启通知,这样每当有新的文章,你就会第一时间收到通知啦~相信有些小伙伴已经收到了😎

有了 SW 和 Notification 还要啥 R(自)S(行)S(车)...[手动滑稽]

内容如有不妥之处,请指出,谢谢...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容