将Rails Application 武装成 Progressive web Application

我们经常用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 keyprivate key 作为环境变量保存。

接下来,将webpushserviceworker-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)就可以完成一次发送。

实装时候的一些困惑

  1. 我们肿么知道要发送的目标的endpoint,p256dh这些东西?

这些东西其实来自订阅成功之后浏览器返回给我们的结果,我们不需要自己指定,只需要把结果持久化,等到要发送时候取出来,填入发送方法的相关参数就好了。

暂时以上。