Help us understand the problem. What is going on with this article?

最近流行りのWeb Push(プッシュ通知)を試してみる

More than 1 year has passed since last update.

Web Push(プッシュ通知)を試してみる

勉強がてら最近、facebookなどで見かける専用アプリを必要とせずにプッシュ通知が可能な仕組みを試してみました。
簡単に言うとchromeやfirefoxを用いて通知をしてみようといった取り組みです。
(サファリはまだ対応していませんが、将来的には対応されると思います。)

仕組み

ServiceWorkerという仕組みを利用した物になります。
下記の流れになります。
1、ドメイン独自のServiceWorkerをブラウザにインストール
2、サブスクリプションを発行
3、2で発行したサブスクリプションをアプリ製作者のサーバーなどに送付
4、アプリ制作者は受け取ったサブスクリプションを管理する。
5、通知を流したい時にサブスクリプションを使用してfirebase宛に送信
6、通知される側のServiceWorkerはfirebaseに常時接続しているので
命令を受け取る。
7、命令に従ってインストールされているServiceWorkerの「push」ハンドリングを起動
8、pushハンドリングに通知内容を書いておくとそれが実行される。
かなり大まかに言えば上記のようなフローです。

このようにServiceWorkerは今までのブラウザの常識(リクエストを投げてレスポンスを表示する)を覆す物となります。
通知以外にも色々と使えそうです!

次からの例ではインストールボタンを用意し、そのボタンを押下した時にインストールを発火しています。
アクションを発火させるだけなので、勿論ボタンだけではなく、ページを開くなどのアクション時でもインストールは可能です。

firebaseへの登録

まずfirebaseに登録します。
https://firebase.google.com/
にログインし、プロジェクトを作成します。
作成後、「設定」に移動します。(歯車のリンク)
まず「全般」タブより
「ウェブアプリに Firebase を追加」というリンクがあると思うので表示します。
jsが表示されるのでこの内容をコピーしておきます。
コピー後、更に「クラウドメッセージング」タブに移動します。
「Server key」
もメモしておきましょう。

はじめに

自分のメモの為に例を貼り付けてます。
デバッグなど仕込みまくりですので使用時は消してください(^^;
ソースの整理もしていません。

デバッグ

ServiceWorkerのデバッグはchromeの開発者ツール「Application」タグの
ServiceWorkerで可能です。

実例(クライアント側)

ここでの例はindex.htmlにボタンを設置し、その押下アクションを拾ってます。
表示時などにイベントを発火する事も出来ます。よしなに。
はじめのfirebaseからのスクリプトはfirebaseで生成したjsになります。
任意の物を貼り付けてください。(例では全てhogehogehogehogeに置き換えてます)

このページを開いて「通知登録して!!!!」リンクを押下すると通知を受け取る事が出来るようになります。

/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <link rel="manifest" href="./manifest.json">
    <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>

    <script src="https://www.gstatic.com/firebasejs/3.7.0/firebase.js"></script>
    <script>
      // Initialize Firebase
      var config = {
          apiKey: "hogehogehogehoge",
          authDomain: "hogehogehogehoge",
          databaseURL: "hogehogehogehoge",
          storageBucket: "hogehogehogehoge",
          messagingSenderId: "hogehogehogehoge"
      };
      firebase.initializeApp(config);
    </script>
    <script src="./index.js"></script>
  </head>
  <body>
    <h1>Web Push Test</h1>
    <a href="#" id="push_regist"  style="display:none">通知登録して!!!!</a><br>
    <a href="#" id="push_delete"  style="display:none">通知登録消して!!!!</a><br>
  </body>
</html>

ServiceWorkerのインストールやサブスクリプションIDをサーバーに送ったりする処理です。

index.js
function initialiseState() {
    if (!("showNotification" in ServiceWorkerRegistration.prototype)) {
        console.warn("プッシュ通知が対応されておりません");
        return;
    }

    if (Notification.permission === "denied") {
        console.warn("通知をブロックしております");
        return;
    }

    if (!("PushManager" in window)) {
        console.warn("プッシュ通知が対応されておりません");
        return;
    }

    //既に過去に登録されている場合は登録するボタンではなく、削除ボタンを表示します
    navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
        serviceWorkerRegistration.pushManager.getSubscription().then(
                function (subscription) {
                    console.log(subscription);
                    $("#push_regist").hide();
                    $("#push_delete").hide();

                    if (!subscription) {
                        $("#push_regist").show();
                        return;
                    }
                    $("#push_delete").show();
                }).catch(function(err){
                    console.warn("Error during getSubscription()", err);
                });
    });
}

$(document).ready(function(){
    if ("serviceWorker" in navigator &&
            (window.location.protocol === "https:" || isLocalhost)) {
        navigator.serviceWorker.register("./sw.js").then(
            function (registration) {
                if (typeof registration.update == "function") {
                    registration.update();
                }

                initialiseState();
            }).catch(function (error) {
                console.error("Service Worker registration failed: ", error);
            });
    }

    //サブスクリプションを発行します
    $("#push_regist").on("click", function(){
        Notification.requestPermission(function(permission) {
            if(permission !== "denied") {
                subscribe();
            } else {
                alert ("プッシュ通知を有効にできません。ブラウザの設定を確認して下さい。");
            }
        });
    });

    //サブスクリプションをサーバ、ブラウザ共に削除します
    $("#push_delete").on("click", function(){
        unsubscribled();
    });

    function subscribe() {
        navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
            serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true }).then(
                function(subscription) {
                    $("#push_regist").hide();
                    $("#push_delete").show();

                    return sendSubscriptionToServer(subscription);
                }
            ).catch(function (e) {
                if (Notification.permission == "denied") {
                    console.warn("Permission for Notifications was denied");
                } else {
                    console.error("Unable to subscribe to push.", e);
                    window.alert(e);
                }
            })
        });
    }

    function unsubscribled() {
        navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
            serviceWorkerRegistration.pushManager.getSubscription().then(
                function(subscription) {
                    if (!subscription ) {
                        $("#push_regist").show();
                        $("#push_delete").hide();
                        return;
                    }

                    sendSubscriptionToServerForDelete(subscription);

                    subscription.unsubscribe().then(function(successful) {
                        $("#push_regist").show();
                        $("#push_delete").hide();
                    }).catch(function(e) {
                        console.error("Unsubscription error: ", e);
                        $("#push_regist").show();
                        $("#push_delete").hide();
                    });
                }
            ).catch(
                function(e) {
                    console.error("Error thrown while unsubscribing from push messaging.", e);
                }
            )
        });
    }

    function sendSubscriptionToServer(subscription) {
        //発行したサブスクリプションをサーバー側に送信します。
        //ここではサブスクリプションを/recieve.phpに送信しています。
        console.log('sending to server for regist:',subscription);
        var data = {"subscription" : subscription.endpoint};
        $.ajax({
            type: "POST",
            url: "/recieve.php",
            dataType: "json",
            cache: false,
            data: data
        });
    }

    function sendSubscriptionToServerForDelete(subscrption) {
        //TODO サブスクリプションをサーバーから削除する処理。テストなので実装していません。
        console.log('sending to server for delete:', subscrption);
    }
});

ServiceWorkerはjsが配置されているディレクトリ配下の操作が出来るようになっています。
なのでjsディレクトリに本体を入れてしまうとjsディレクトリ以下しか操作できません。
その回避策です。
ただ本体を更新してもこちら変化しないので
キャッシュが効いてしまう。?日付のようにクエリ入れるのも面倒くさい。
いっその事/sw.jsを本体にしてしまって良いかもしれません。

/sw.js
importScripts("./js/sw.js");

ServiceWorker本体です。
ご覧のように現在はpushハンドラ内にてサーバーへメッセージを取得しにいっている。
メッセージを暗号化するとメッセージ込みで送信する事も可能らしい。要調査。

/js/sw.js
//ServiceWorkerにインストールされるスクリプト
//プッシュ通知が行われると「push」イベントが起動する
self.addEventListener("install", function(event) {
    self.skipWaiting();
    console.log("Installed", event);
});

self.addEventListener("activate", function(event) {
    console.log("Activated", event);
});

self.addEventListener("push", function(event) {
    console.log("Push message received", event);
    event.waitUntil(getEndpoint().then(function(endpoint) {
        //通知内容をサーバに取得しに行きます。
        return fetch("/notifications.php?endpoint=" + endpoint);
            }).then(function(response) {
                if (response.status === 200) {
                    return response.json();
                }
                throw new Error("notification api response error")
                    }).then(function(response) {
                        //TODO デザインやボタンの有無などの調整が必要
                        return self.registration.showNotification(response.title, {
                            icon: response.icon,
                            body: response.body,
                            tag: "push-test",
                            actions: [{
                                action: "act1",
                                title: "ボタン1"
                            }, {
                                action: "act2",
                                title: "ボタン2"
                            }],
                            vibrate: [200, 100, 200, 100, 200, 100, 200],
                            data: {
                                url: response.url
                            }
                        })
                    })
    );
});
//押したaction名はnotificationclickのevent.actionで取得できます。

self.addEventListener("notificationclick", function(event) {
    console.log("notification clicked:" + event);
    console.log("action:" + event.action);
    event.notification.close();

    var url = "/";
    if (event.notification.data.url) {
        url = event.notification.data.url
    }

    event.waitUntil(
            clients.matchAll({type: "window"}).then(function() {
            if(clients.openWindow) {
              return clients.openWindow(url)
            }
        })
    );
});

function getEndpoint() {
    return self.registration.pushManager.getSubscription().then(function(subscription) {
        if (subscription) {
            return subscription.endpoint;
        }
        throw new Error("User not subscribed");
    });
}

プッシュを許可しますか?という定型メッセージのデザインです。
アイコンや色などを変更する事ができます。

manifest.json
{
  "name": "Web Push Test",
  "short_name": "WebPush",
  "icons": [{  
    "src": "/image/icon.png",  
    "sizes": "256x256",
    "type": "image/png" 
  }],  
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000040",
  "gcm_sender_id": "hogehogehoge"
}

実例(サーバーサイド側)

テスト用の超簡易版です。チェックも何もしてないです。
本来ならサブスクリプションをユーザと紐づけてDB管理したりするべきだと思います。
ここではテストしやすいように
subscriptions.data
にサブスクリプションを保存して全てに送信するといった簡単な検証をしています。

サブスクリプションIDを格納します。

/recieve.php
<?php
    file_put_contents("subscriptions.data", $_POST['subscription']. PHP_EOL, FILE_APPEND);
?>

firebaseにサブスクリプションを送付します。
それによって通知がされる仕組みです。
ここでは簡易的にこのphpにアクセスがあると「subscriptions.data」ファイルの中のサブスクリプション全てに通知を送るような仕組みです。

/pushSender.php
<?php
define('GOOGLE_API_URL','https://android.googleapis.com/gcm/send');
define('GOOGLE_API_KEY','firebaseへの登録でメモしたServer key');


class ClassGoogleCloudMessaging{

    public function sendData($message){

        $registration_ids = array();
        //TODO 本来はDB等から取り出した送信先レジストレーションIDを格納
        $registration_ids_tmp = @file("subscriptions.data", FILE_IGNORE_NEW_LINES);

        foreach ($registration_ids_tmp as $value) {
            $registration_ids[] = str_replace(GOOGLE_API_URL, "", $value);
        }

        $data = array(
            'message'  => $message
        );

        $header = "Content-Type:application/json"."\r\n"
            ."Authorization:key=".GOOGLE_API_KEY."\r\n";

        $contents = array(
            'data'             => $data,
            'registration_ids' => $registration_ids,
            'collapse_key'     => 'gcmtest'
        );

        $options_array = array();
        $options_array["http"]["method"]  = "POST";
        $options_array["http"]["header"]  = $header;
        $options_array["http"]["content"] = json_encode($contents);

        $context = stream_context_create();
        stream_context_set_option(
            $context,
            $options_array
        );

        try{
            $response = file_get_contents(
                GOOGLE_API_URL,
                false,
                $context
            );

        }catch(Exception $e){
            $response = $e;

        }
        return $response;
    }
}

$test = new ClassGoogleCloudMessaging();
$test->sendData("test");
?>

通知を受け取ったServiceWorkerがアクセスしにくるphpです。
今は固定ですが、動的にするなどの対策が必要です。
※または暗号化でメッセージ自体を送ることも可能なようです。TODO

/notifications.php
<?php
echo('{
    "title": "通知テスト' . date("Y/m/d H:i:s") . '",
    "icon": "/image/icon.png",
    "body": "通知本文になります。' . $_GET['endpoint'] . '",
    "url": " https://' . $_SERVER["HTTP_HOST"] . '"
}');
?>

参考

下記を参考にしました。
https://developers.google.com/web/updates/2016/03/web-push-encryption
http://tech.pjin.jp/blog/2015/10/20/android%E3%81%A7%E3%83%97%E3%83%83%E3%82%B7%E3%83%A5%E9%80%9A%E7%9F%A5%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B%E3%80%90%E7%AC%AC6%E5%9B%9E-app%E3%82%B5%E3%83%BC%E3%83%90%E5%81%B4%E5%AE%9F/
http://qiita.com/narikei/items/0f26b30d347ae19d9559

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away