ブラウザでユーザに通知を送るWebプッシュ通知をやってみた!

どうも、りょすけです。

 

本日は、 自分が開発しているWebサービスにWebプッシュ通知機能を追加したので、プログラミングのTipsとして記事を書こうと思います。

 

Webプッシュ通知って?

 

みなさんがよく使っているスマホのLINEアプリでは、メッセージが届くと通知が表示されますね。

これと同じようにChromeやFirefoxといったブラウザにWebサイトから通知を送る技術をWebプッシュ通知っていいます。

スマートフォンはもちろんですが、PCにも表示させることができます。

Webプッシュ通知はスマホアプリと違ってユーザはアプリをインストールする必要がありません。

Webサイトを訪れたときにポップアップ表示される通知の許諾に許可をするだけで、通知を受信することができるようになります。

Webプッシュ通知ができるようになると、ユーザに新しいお知らせを通知したりとコミュニケーションが深まります。

 

 

Webプッシュ通知のプログラミング

 

今回は、Ruby on Railsで開発したWebサイトにWebプッシュ通知を追加します。

新しい技術ってすべて自分でしらべて、イチからつくっていたら時間がかかりすぎます。

今回はWebプッシュ通知に必要な機能をまとめた「web_push」というgemライブラリを利用します。

Gemのインストール

Gemfileに下記2つのgemを追加し、bundle installコマンドでgemをインストールします。

・Gemfile

# for webpush
gem 'serviceworker-rails'
gem 'webpush'
$ bundle install

 

VAPID公開鍵、秘密鍵設定

Webプッシュ通知はVAPIDと呼ばれる認証の仕組みを使います。

web_push gemを使ってVAPIDの公開鍵と秘密鍵を生成するために下記のrakeタスクを実装します。

・lib/tasks/webpush.rake

namespace :webpush do
  task :generate_key do
    vapid_key = Webpush.generate_key
    File.open(Rails.root.join('tmp/vapid_public_key.txt'), mode = "w"){|f|
      f.write("WEB_PUSH_VAPID_PUBLIC_KEY=#{vapid_key.public_key}")# 公開鍵
      f.write("\n")
      f.write("WEB_PUSH_VAPID_PRIVATE_KEY=#{vapid_key.private_key}")# 秘密鍵
    }
  end
end

rakeコマンドでタスクを実行して公開鍵と秘密鍵を生成します。

$ rake webpush:generate_key

tmp/vapid_public_key.txtにVAPIDの公開鍵と秘密鍵が出力されます。

公開鍵と秘密鍵は環境変数に設定してRailsに読み込ませましょう。

・.env

WEB_PUSH_VAPID_PUBLIC_KEY=BFeNxJwfaizp2pa1alWfMQy5igld3HbXDQeNldJeq5MRXgVdTsGFom_vbfQcdq6_YGWMWWmrHVfy-EgG6PaZd2Y=
WEB_PUSH_VAPID_PRIVATE_KEY=ZG9cR3TJaFzoiaQIu-z-e-vjtE4paQNKXumH-1lx5aM=

 

ServiceWorkerを準備する

Webサイト上でWebプッシュ通知のやりとりはServiceWorkerと呼ばれるJavascriptの処理領域で行われます。

 

manifest.json設定

まず、Webサイトを訪問した際にサイトの情報を提供する「manifest.json」が必要になります。

重要な点としてはstart_urlに定義したパス以下のみServiceWorkerが実行される点です。

・public/manifest.json

{
  "name": "WEB APP NAME",
  "short_name": "WEB APP",
  "icons": [
    {
      "src": "/images/icon.png",
      "sizes": "144x144",
      "type": "image/png"
    }
  ],
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#e91e63"
}

Webサイトにアクセスしたタイミングでブラウザにmanifest.jsonを読み込ませる必要があります。

app/views/layouts以下の共通ファイルでHTMLのheadタグ内に設定します。

・app/views/layout/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <!-- headタグ内に追加 -->
    <link rel="manifest" href="/manifest.json">

これでWebサイトにアクセスした際に利用者のブラウザにmanifest.jsonが読み込まれます。

ServiceWorkerの作成とルーティング

次にServiceWorkerを実装します。

・app/assets/javascripts/service_worker.js

var CACHE_VERSION = 'v1';
var CACHE_NAME = CACHE_VERSION + ':sw-cache-';

function onSWInstall(event) {
    return event.waitUntil(
        caches.open(CACHE_NAME).then(function prefill(cache) {
            return cache.addAll([]);
        })
    );
}

function onSWActivate(event) {
    return event.waitUntil(
        caches.keys().then(function(cacheNames) {
            return Promise.all(
                cacheNames.filter(function(cacheName) {
                    // Return true if you want to remove this cache,
                    // but remember that caches are shared across
                    // the whole origin
                    return cacheName.indexOf(CACHE_VERSION) !== 0;
                }).map(function(cacheName) {
                    return caches.delete(cacheName);
                })
            );
        })
    );
}

function onSWPush(event) {
    // event.data are null when Firefox's debugger push
    // event.data are PushMessageData object and cannot exec event.data.json, event.data.text => 'Test push message from DevTools.'
    var json = event.data ? event.data.json() : {"title" : "DEBUG TITLE", "body" : "DEBUG BODY"};
    return event.waitUntil(
        self.registration.showNotification(json.title, {
            body: json.body,
            //icon: json.icon,
            data: {
                target_url: json.target_url
            }
        })
    );
}

function onSWNotificationClick(event) {
    event.notification.close();
    return event.waitUntil(
        clients.openWindow(event.notification.data != null ? event.notification.data.target_url : '/')
    );
}

self.addEventListener('install', onSWInstall);
self.addEventListener('activate', onSWActivate);
self.addEventListener("push", onSWPush);
self.addEventListener("notificationclick", onSWNotificationClick);

Webプッシュ通知の許諾を得るためのJavascriptを実装します。このJavascriptで先ほど生成したVAPID公開鍵を使う必要があります。

そのためRails側でJavascriptにVAPID公開鍵を埋め込むようにします。

・app/assets/javascripts/user/serviceworker/serviceworker.js

let subscribed = false;
const isEnableWebPushBrowser = function() {
    const ua = navigator.userAgent.toLowerCase();
    if (!navigator.serviceWorker) {
        return false;
    }

    // Edge & Operaは対象外
    if (ua.indexOf("edge") >= 0 || ua.indexOf("opr") >= 0) {
        return false;
    }
    // Chrome 52- OK
    if (ua.match(/chrom(e|ium)/)) {
        var raw = ua.match(/chrom(e|ium)\/([0-9]+)\./);
        if (raw && parseInt(raw[2], 10) >= 52) {
            return true;
        }
    }
    // Firefox 48- OK
    if (ua.indexOf("firefox") >= 0) {
        var raw = ua.match(/firefox\/([0-9]+)\./);
        if (raw && parseInt(raw[1], 10) >= 48) {
            return true;
        }
    }
    return false;
};

if (isEnableWebPushBrowser()) {
    var convertWebPushSubscriptionJson = function(subscription) {
        var jsonData = {};
        if (subscription) {
            jsonData = {
                endpoint: subscription.endpoint,
                key: btoa(String.fromCharCode.apply(null, new Uint8Array(subscription.getKey('p256dh')))).replace(/\+/g, '-').replace(/\//g, '_'),
                auth: btoa(String.fromCharCode.apply(null, new Uint8Array(subscription.getKey('auth')))).replace(/\+/g, '-').replace(/\//g, '_')
            };
        }
        return jsonData;
    };

    // Webプッシュ通知を有効にする
    var webPushSubscribe = function() {
        return navigator.serviceWorker.getRegistration().then(function(registration) {
            if(!registration || !registration.pushManager) return;

            // Webプッシュ通知用VAPID
            var vapidPublicKey = new Uint8Array(<%= Base64.urlsafe_decode64(ENV['WEB_PUSH_VAPID_PUBLIC_KEY']).bytes %>);
            return registration.pushManager.subscribe({
                userVisibleOnly: true,
                applicationServerKey: vapidPublicKey
            }).then(function(subscription) {
                // Webプッシュ通知に登録した情報をサーバーに送信してDBに保存する
                $.ajax({
                    url:"任意のURL",
                    type:"POST",
                    data: convertWebPushSubscriptionJson(subscription),
                    dataType:"json",
                    timespan:1000,
                    async : false,
                    beforeSend: function(xhr) {
                        xhr.setRequestHeader("X-CSRF-Token", $('meta[name="csrf-token"]').attr('content'))
                    }
                }).done(function(data1,textStatus,jqXHR) {
                    subscribed = true;
                }).fail(function(jqXHR, textStatus, errorThrown ) {
                    subscribed = false;
                });
                return true;
            })["catch"](function(err) {
                return false
            });
        });
    };

    // Webプッシュ通知を無効にする
    var webPushUnSubscribe = function() {
        return navigator.serviceWorker.getRegistration().then(function(registration) {
            return registration.pushManager.getSubscription().then(function(subscription) {
                if (!subscription) {
                    return;
                }
                return subscription.unsubscribe().then(function(success) {
                    // API叩いて無効にするなどの処理
                    return;
                })["catch"](function(error) {
                    return console.log(error);
                });
            });
        });
    };

    // ServiceWorkerのインストール
    navigator.serviceWorker.register('/serviceworker-user.js', { scope: '/' });

ServiceWorkerはmanifest.jsonのstart_urlで定義したパス以下でないと動作しません。

VAPID公開鍵を埋め込んだJavascriptのURLを任意に決められるようにルーティングを定義します。

・config/initializers/serviceworker.rb

Rails.application.configure do
  config.serviceworker.routes.draw do
    match "/serviceworker-user.js" => "user/serviceworker/serviceworker.js"
  end
end

最後にWebプッシュ通知を実行するJavascriptを作成します。今回はリクエストパラメータに指定された値がある場合のみ実行するようにします。

注意点としてChromeは通知の許諾と通知を送信を一回の処理で実現できますが、Firefoxの場合は通知の許諾を得る処理をした後、もう一度通知の送信処理をする必要があることです。

・app/assets/javascripts/webpush_subscriber.js

function getParam(name, url) {
    if (!url) url = window.location.href;
    name = name.replace(/[\[\]]/g, "\\$&");
    var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
        results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return '';
    return decodeURIComponent(results[2].replace(/\+/g, " "));
}

if(getParam('subscribe') && Notification.permission != "denied") {
    let try_subscribe = 0;
    const id = setInterval(function() {
        webPushSubscribe().then(function(subscription) {
            try_subscribe += 1
            if(subscribed || try_subscribe > 3){
                clearInterval(id);
            }
        });
    }, 2000);
}

 

サーバー側でWebプッシュ通知の登録情報を受け取る

通知の許諾を得ると通知メッセージを送信するための認証情報が取得できます。

サーバー側で認証情報を保存しておくと、任意のタイミングでWebプッシュ通知を送信することができるようになります。

メッセージの送信はweb_push gemを利用して下記のように記述します。

payload = {
    endpoint: params[:endpoint], # ブラウザ側のregistration.pushManager.getSubscription()で取得したsubscriptionのendpoint
    p256dh: params[:key], # 同じくsubscriptionのp256dh
    auth: params[:auth], # 同じくsubscriptionのauth
    vapid: {
        subject: "タイトル", // 環境変数に設定した値を取得
        public_key: ENV['WEB_PUSH_VAPID_PUBLIC_KEY'], // 環境変数に設定した公開鍵を取得
        private_key: ENV['WEB_PUSH_VAPID_PRIVATE_KEY'] // 環境変数に設定した秘密鍵を取得
    },
    message: {
        icon: '',
        title: 'タイトル',
        body: 'メッセージ',
        target_url: '/'
    }.to_json
}
Webpush.payload_send(payload)

 

今回は会員登録直後のあいさつをWebプッシュ通知で送るようにしました。

これで会員登録をすると直後にWebプッシュの許可をとり、あいさつのメッセージを遅れるようになりました。

 

 

 

 

 

まとめ

 

一般的なWebサイトは一方的な情報発信になりがちです

Webプッシュ通知を使うことでユーザとのコミュニケーションが深まり、ユーザ体験が向上しますね

 

ここ数年で国内外で成功事例も増えてWebサイトのみでスマホアプリに並ぶリッチなユーザ体験を提供できる様になってきています。

Webプッシュ通知を使うことでコンバージョンが向上したという事例もあります。

 

このように新しいWebマーケティングの技術を取り入れることで、サービスの質が向上していきますね。

これからも継続してサービスを磨きまくります!

 

ありがとうございました〜〜〜

藤沢瞭介(Ryosuke Hujisawa)
  • りょすけと申します。18歳からプログラミングをはじめ、今はフロントエンドでReactを書いたり、AIの勉強を頑張っています。off.tokyoでは、ハイテクやガジェット、それからプログラミングに関する情報まで、エンジニアに役立つ情報を日々発信しています!

未整理記事