我们经常用websocket来实现后台消息推送。比如提醒用户新的留言,发帖,评论之类。
但是这种方式的局限性也很明显,就是当我们在浏览其他网页的时候,我们并不能知道在另一个网页上发生的这些通知。
为了在我们浏览其他网站的时候,也能在浏览器上收到我们在其他网站上的通知消息,就有了 webpush 这个东西。
有了webpush这种东西,我们就可以说,我们的web application是渐进式的为application了。
浏览器支持
主流的pc chrome 和firefox,android chrome。
什么,你不用这些浏览器? 食💩吧。
Rails的场合
下面我们就来给Rails Application添加webpush。
首先我们要生成和push service用于通信的公钥和私钥。
安装webpush
这个gem。
1 2 3 4 5 6 7
| # gem install webpush # One-time, on the server vapid_key = Webpush.generate_key # Save these in your application server settings vapid_key.public_key vapid_key.private_key
|
把生成的public key
和 private key
作为环境变量保存。
接下来,将webpush
和 serviceworker-rails
这两个gem添加到Gemfile中。
1 2
| gem 'webpush' gem 'serviceworker-rails'
|
bundle install,运行generator
1
| rails g serviceworker:install
|
这样就会生成以下文件。
1 2 3 4 5
| config/initializers/serviceworker.rb - 配置service worker的文件 app/assets/javascripts/serviceworker.js.erb - 一个带有一些例子的serviceworker文件 app/assets/javascripts/serviceworker-companion.js - 用来把你的serviceworker注册到浏览器中 app/assets/javascripts/manifest.json.erb - 上面提到的manifest public/offline.html - 这个暂时我也没管是啥
|
因为要嵌入我们之前的公钥,所以先把app/assets/javascripts/serviceworker-companion.js
重命名成 app/assets/javascripts/serviceworker-companion.js.erb
添加如下内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| if ('serviceWorker' in navigator) { console.log('Service Worker is supported'); var vapidPublicKey = new Uint8Array(<%= Base64.urlsafe_decode64(ENV["WEB_PUSH_VAPID_PUBLIC_KEY"]).bytes %>); navigator.serviceWorker.register('/serviceworker.js') .then(function(registration) { console.log('Successfully registered!', ':^)', registration); registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: vapidPublicKey }) .then(function(subscription) { $.ajax({ url: "/sessions/subscribe", type: "post", headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')}, data: { subscription: subscription.toJSON() }, success: function(data){ } }); console.log('endpoint:', subscription.endpoint); }); }).catch(function(error) { console.log('Registration failed', ':^(', error); }); }
|
这里我们注册我们的serviceworker,注册成功之后,我们得把浏览器返回给我们的关于push 服务器的一些信息持久化保存起来,利用我们保存的信息,我们就可以主动推送信息了。
这里先看一下serviceworker.js
的内容。
打开 serviceworker.js
添加如下内容
1 2 3 4 5 6 7 8 9 10 11 12 13
| function onPush(event) { var json = event.data ? event.data.json() : {"title" : "DEBUG TITLE", "body" : "DEBUG BODY"}; event.waitUntil( self.registration.showNotification(json.title, { body: json.body, icon: json.icon, data: { target_url: json.target_url } }) ) } self.addEventListener("push", onPush);
|
这样我们就会在收到push消息的时候在浏览器的右上角显示消息框。至于传过来的event里面的内容是什么,现在暂时无视好了。
接下来我们再看一下持久化subscription
的部分。
也就是这个js的具体实现。
1 2 3 4 5 6 7 8 9 10
| $.ajax({ url: "/sessions/subscribe", type: "post", headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')}, data: { subscription: subscription.toJSON() }, success: function(data){ }
|
注意在Rails中,ajax要带上csrf
的header。
如何持久化因需求而异,这里简单地塞到数据库的表中而已。如果没有用户登陆的情况下,我们则塞到session里面。
1 2 3 4 5 6 7 8 9 10 11 12
| class SessionsController < ApplicationController def subscribe subscription = JSON.dump(params[:subscription].permit!.to_hash) if current_user current_user.update(subscription: subscription) end session[:subscription] = subscription head :ok end end
|
接下来就是激动人心的发送部分了。在发送之前我们得做一些准备工作,比如先把我们要发送的subscription
从数据库或者session里面取出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| def fetch_subscription(user) if user && user.subscription encoded_subscription = user.subscription else raise "Cannot create notification: no :subscription for user" end JSON.parse(encoded_subscription).with_indifferent_access end ## message format #message: { # icon: 'https://example.com/images/demos/icon-512x512.png', # title: title, # body: body, # target_url: target_url #} def webpush_params(user,message) subscription_params = fetch_subscription(user) message = message.to_json endpoint = subscription_params[:endpoint] p256dh = subscription_params.dig(:keys, :p256dh) auth = subscription_params.dig(:keys, :auth) vapid = { subject: 'gyorou@tjjtds.me', public_key: ENV['WEB_PUSH_VAPID_PUBLIC_KEY'], private_key: ENV['WEB_PUSH_VAPID_PRIVATE_KEY'] } { message: message, endpoint: endpoint, p256dh: p256dh, auth: auth, vapid: vapid } end
|
{ message: message, endpoint: endpoint, p256dh: p256dh, auth: auth, vapid: vapid }
就是我们要喂给
webpush的全部内容了。
我们调用Webpush.payload_send webpush_params(user, message)
就可以完成一次发送。
实装时候的一些困惑
- 我们肿么知道要发送的目标的endpoint,p256dh这些东西?
这些东西其实来自订阅成功之后浏览器返回给我们的结果,我们不需要自己指定,只需要把结果持久化,等到要发送时候取出来,填入发送方法的相关参数就好了。
暂时以上。