立即注册 登录
彼岸网 返回首页

天香公主的个人空间 http://www.bian-wang.com/discuz/?10005 [收藏] [复制] [分享] [RSS] txgz999@yahoo.com

日志

如何向用户推送通知

热度 3已有 160 次阅读2017-6-24 01:46 PM |个人分类:Discuz| 文章, 如何, 用户

本文旨在讨论网友a1980提出的问题:
希望如果有新的文章,只要開啟瀏覽器,不論有沒有進到我們的網站,他都會在右下角通知讀者有新文章
http://www.75271.com/3375.html

网友介绍的文章用的是HTML5里的网络通知功能 (Web Notifications API),参见( 链接链接 )。 其实Discuz的代码里就使用过这个功能:在 template/default/common/footer.htm 里有下面这段代码,它的作用是当用户有新提醒或新消息时,在屏幕右下角就会出现一个通知窗口:


但是至少我从没在用Discuz造的网站里看到过这个窗口。其原因是实现这个功能的代码(在文件 static/js/html5notification.js 里)用的是早期由谷歌自创的网络通知API,语法和现在的国际标准有些不同,较新的服务器已经不支持它了。但是不难把这个文件用当前的标准改写,这样我们就能在较新的Firefox和Chrome浏览器里看到这个功能了(下载链接)。

但是这种做法只在用户接触站内的某个网页时才会触发,它没法在网站有需要时(如网站里发了一篇新文章),主动向用户发送通知。要达到那样的效果,需要另外两个HTML5里的功能:网络推送API(Push API),以及服务工人(Service Worker),参见( 链接链接 )。

要实现这个功能,对网站有些要求。首先网站必须使用HTTPS,这是Service Worker的要求。其次网站用的PHP版本至少得是 5.6 而且要支持gmp,这是我用的一个第三方类库 web-push-php的要求。同时用户的浏览器不能是IE,而需要是较新版本的Firefox,Chrome,Safari,或Edge (我的测试环境是 Firefox54和Chrome59),这是Push API和Service Worker的要求。因此这个功能在现阶段不能用来完全替代Discuz里传统的消息传递方式,但可以成为一个有益的补充。

要实现网站向用户推送通知,光靠网站服务器和用户浏览器的力量还不够,还需要一个第三方提供推送服务(push service)。这是因为网站和浏览器的联系是由浏览器向网站发请求而开始,又由网站发回复给浏览器而结束。网站无法主动和某个浏览器联系。所以网站向浏览器推送通知的方法是它把通知的内容发给由浏览器决定的提供push service的服务器,再由这个服务器将内容转给浏览器。每种浏览器都使用一个提供push service的服务器。浏览器在启动那刻起就和提供push service的服务器保持着联系,直到浏览器停止运行为止。所以提供push service的服务器可以随时送通知给浏览器,而不需要用户去打开发通知的那个网站的网页。如果提供push service的服务器收到要传送的通知时,要送达的那个浏览器没在运行或不在线上,它会将通知保存,等浏览器上线后和它联系时再送达通知。

Service Worker是在浏览器里运行的不同于UI线程(thread)的单独的线程。它由浏览器依据网站的需要产生,运行网站提供的代码,完成网站要做的事。它的最常见的用途是在浏览器离线状态时,将浏览器对网站的请求用本地资源来满足,从而让用户在离线时继续使用该网站。在网络推送里它起的作用是当浏览器收到推送通知后,它会发给和该网站对应的Service Worker,由Service Worker运行网站提供的代码来显示通知。

要实现推送,需要两部分的工作。第一部分是一次性的准备工作
1)要征得用户对显示通知窗口的同意。用户会看到一个征得同意的窗口
2)网站要准备一个service worker要做的事的js代码文件,并向浏览器注册一个service worker来做这些事
3)由该service worker向浏览器使用的push service订阅推送,这样网站才能把要推送的内容发给push service传递。网站需要提供一个代表它的应用服务器的 VAPID public key,push service会回给一个专用的url,称为endpoint,来供网站发给它要传送的通知
4)将订阅成功后得到的信息包括push service提供的 endpoint,还有浏览器提供的代表它的客户 public key和token,送到服务器去保存。当网站要发通知时,需要这些信息
第二部分是发送和接受通知
1)网站在要发通知时,找到所有要发通知的浏览器的订阅信息
2)给每个订阅信息提供的endpoint发经过加密的通知,发的格式由web push protocol决定的。这部分protocol比较复杂,所以我用了第三方提供的类库 web-push-php
3)浏览器收到push service传送的通知后,会交给和网站对应的service worker来处理
4)该service worker会运行网站提供的处理代码来显示通知窗口
要进一步了解web push的原理和流程,建议阅读Matthew Gaunt的书(链接) 和他的示范代码(链接)。上面说的两部分工作也可以用他书中的两副插图来说明:



下面介绍下我在Discuz网站里使用web push的尝试。目的不在于给Discuz提供一个完整的新的通知机制,而在于摸索和了解它的可行性。

界面的变化
1)用户在登录后,如果浏览器支持web push的话,在首页的左上方会出现一个"不收推送通知"链接,代表当前用户还没有同意接收通知。点击后这个链接会变成"接收推送通知",这时用户就可以收到网站推送的通知了。如果不想收到推送通知了,就再次点击这个链接。这个toggle过程类似于网站上已有的隐身/在线功能。
2)当浏览器收到通知后会在右下方的窗口里显示。作为测试,加了两类通知
a)当网站发布新文章时发通知给所有订阅通知的用户
b)当博主发新日志时,发通知给所有订阅通知的好友
用户点击通知窗口后就进入了该文章/日志页


数据库的修改
要添加一个新的数表来记载推送的订阅信息
CREATE TABLE `ext_push_subscription` ( `id` smallint(6) unsigned NOT NULL AUTO_INCREMENT, `uid` mediumint(8) unsigned NOT NULL DEFAULT '0', `subscription` mediumtext NOT NULL, `dateline` int(10) unsigned NOT NULL, PRIMARY KEY (`id`), KEY `uid` (`uid`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
代码的修改
主干部分是下面这些新文件和文件夹:
push-setup.js在浏览器里运行负责为浏览器订阅推送
push-service-worker.js记载service-worker要做的时,包括如何显示收到的通知
push-subscription.php接受浏览器递交的推送订阅信息
push-message.php在网站上负责发通知的函数
source/class/table/ table_ext_push_subscription.php负责管理保存推送订阅的数表
web-push-php网上下载的帮助将通知发给 push service 的类库 (链接)
push service 用 VAPID keys 来确认要向用户推送通知的网站就是让用户订阅推送的网站。每个网站应该使用自己的VAPID keys。访问网页 https://web-push-codelab.appspot.com 就能得到自己的VAPID keys。然后将它们替换文件pub-message.php里的 VAPID_PUBLIC_KEY 和 VAPID_PRIVATE_KEY,以及文件 push-setup.js 里的 VAPID_PUBLIC_KEY

其次需要修改现有的文件
1。在语言包里添加要用的汉字字符串 修改文件 source/language/lang_template.php,在其中加入 'allow_push' => '接收推送通知', 'forbid_push' => '不收推送通知', 'newarticle_push_title' => '快去看看新到的文章吧', 'newblog_push_title' => '您的好友{author}刚发了新日志,快去看看吧',
2。修改界面使得用户可以接收推送通知 修改文件 template/default/common/header.htm, 在下面这句话 <!--{hook/global_cpnav_extra1}--> 后加入 < a id="subsriptionswitch" style="cursor: pointer;" onclick='toggleSubscription(this, $_G[uid]);'></a> <script type="text/javascript"> var FORM_HASH = "{FORMHASH}"; var ALLOW_PUSH_LABEL = "{lang allow_push}"; var FORBID_PUSH_LABEL = "{lang forbid_push}"; </script> <script type="text/javascript" src="push-setup.js"></script> <script type="text/javascript"> setUpSubscriptionSwitch($("subsriptionswitch"), $_G[uid]); </script> 注意在上面代码里<和a间的空格要去掉

3。在网站有新文章时通知用户
修改文件 source/include/portalcp/portalcp_article.php, 在下面这句话 C::t('portal_article_count')->insert(array('aid'=>$aid, 'catid'=>$setarr['catid'], 'viewnum'=>1)); 后加入 include_once libfile('function/home'); $pic = $setarr['pic']; if ($pic) $pic = pic_get($pic, '', $setarr['thumb'], $setarr['remote'], 1, 1); include_once(DISCUZ_ROOT.'push-message.php'); sendPushMessage('new-article', lang('template', 'newarticle_push_title'), $setarr['title'], $summary, $pic, 'portal.php?mod=view&aid='.$aid, null);
4。当发新日志时,给日志作者的好友发通知
修改文件 source/function/function_blog.php, 在下面这句话 C::t('common_member_field_home')->update($_G['uid'], array('recentnote'=>$POST['subject'])); 后加入 $summary = blog_bbcode($message); $summary = cutstr(strip_tags($summary), 140); require_once libfile('function/friend'); $frienduids = array(); if ($blogarr['friend']==0 || $blogarr['friend']==1) { $friendarray = friend_list($_G['uid'], 100); if($friendarray && is_array($friendarray)) { foreach($friendarray as $friend) { $frienduids[] = $friend['fuid']; } } } else if ($blogarr['friend']==2 && !empty($POST['target_ids'])) { $frienduids = explode(',', $POST['target_ids']); } if (!empty($frienduids)) { include_once(DISCUZ_ROOT.'push-message.php'); sendPushMessage('new-blog', lang('template', 'newblog_push_title', array('author' => $blogarr['username'])), $blogarr['subject'], $summary, $picurl, 'home.php?mod=space&uid='.$blogarr['uid'].'&do=blog&id='.$blogid, $frienduids); } }
测试网站: https://ngcorner.com/dz32wp,用户 test1/123451, test2/123451, test3/123451。可以用一个ID发日志,再用另一个好友ID看通知

下载链接: http://www.bian-wang.com/discuz/data/userupload/10005/webpush_discuz.zip

发表评论 评论 (11 个评论)

回复 a1980 2017-6-29 02:16 AM
天香公主真的很厲害,我研究的半天還是一知半解,天香馬上就做出來了,小弟真的很佩服,真的很謝謝你的協助和解答。
回复 天香公主 2017-6-26 12:02 AM
ladyff: 其实更需要的是可以对短消息进行推送。而且现在有一些网站是访问的时候,FF直接弹出窗口问要不要接受通知,这个能做到么 ...
这很容易做到。就相当于我文末3和4,两者的套路都是在新数据储存后将相关数据交给sendPushMessage就行了。不过我的目的是proof of concept
回复 ladyff 2017-6-25 11:41 PM
其实更需要的是可以对短消息进行推送。而且现在有一些网站是访问的时候,FF直接弹出窗口问要不要接受通知,这个能做到么
回复 天香公主 2017-6-25 09:34 AM
carry0987: 測試成功~收到了~
哦,原来如此
回复 carry0987 2017-6-25 09:20 AM
天香公主: 做了一个小改动,使得链接的含义与网站已有的隐身/在线的含义相仿:

链接的字样是"不收推送通知"时,代表当前用户还没有同意接收
链接的字样是"接收推送通 ...
測試成功~收到了~
回复 天香公主 2017-6-25 09:15 AM
做了一个小改动,使得链接的含义与网站已有的隐身/在线的含义相仿:

链接的字样是"不收推送通知"时,代表当前用户还没有同意接收
链接的字样是"接收推送通知"时,代表当前用户同意接收
回复 carry0987 2017-6-25 09:14 AM
天香公主: 谢谢,我等会在mac上试试。

还有我发现我的toggle过程和隐身/在线反了。用户看到'隐身'字样时是处于隐身状态下,所以看到接收网站通知'字样应该是在允许接收状 ...
OK~
回复 天香公主 2017-6-25 08:13 AM
carry0987: 我是在Mac Chrome 59.0.3071.109 (正式版本) (64 位元)中測試的,我用test1發了一篇日誌,然後再以test2登入論壇,發現依然沒有收到通知 ...
谢谢,我等会在mac上试试。

还有我发现我的toggle过程和隐身/在线反了。用户看到'隐身'字样时是处于隐身状态下,所以看到接收网站通知'字样应该是在允许接收状态下,我最好反了,等会我会改。
回复 carry0987 2017-6-25 07:51 AM
我是在Mac Chrome 59.0.3071.109 (正式版本) (64 位元)中測試的,我用test1發了一篇日誌,然後再以test2登入論壇,發現依然沒有收到通知
回复 天香公主 2017-6-25 07:39 AM
carry0987: 沒有顯示呢...會不會是這個的問題?

這段代碼在
/api/manyou/Service/DiscuzTips.php
這個文件裡面 ...
谢谢测试,这个错误信息不该有影响。能不能具体讲讲你的测试步骤,还有你的浏览器的版本?
回复 carry0987 2017-6-25 06:33 AM
沒有顯示呢...會不會是這個的問題?

這段代碼在
/api/manyou/Service/DiscuzTips.php
這個文件裡面

facelist doodle 涂鸦板

您需要登录后才可以评论 登录 | 立即注册

小黑屋|Archiver|彼岸网  

Powered by Discuz! X3.1 © 2001-2014 Comsenz Inc.
GMT-4, 2017-7-25 06:42 AM , Processed in 0.058728 second(s), 19 queries.

返回顶部