ラボ > PHP:Firebase、Javascript関連:Firebase、Google関連:Firebase

FirebaseでPCにプッシュ通知

スマホじゃなくてデスクトップ(PC)にプッシュ通知を出したい

作成日:2017-11-10, 更新日:2017-11-20

基本

色々な人が色々な時期にコピペした情報をサイトに載せているから異常なくらい面倒だった。

「Google Cloud Messaging」と「Firebase」

「Google Cloud Messaging」も「Firebase」もGoogleが提供元。

細かいことは知らない。
「Google Cloud Messaging」は昔。「Firebase」が今。
だから「Firebase」で情報を調べる必要あり。

前提条件

・Win(Xampp)環境
・「PC+Firebase+プッシュ通知」は「https」もしくは「localhost」じゃないとダメ。
・ブラウザも関係あり・・・らしい(ひとまず最新のChromeで試す)
・「Firebase」でプロジェクトを作成する必要あり。

内容

・「Firebase」でプロジェクトを作成
・PCでプッシュ通知の許可を求める
・サーバでプッシュ通知を送る(PCにプッシュ通知を受け取る)

「Firebase」でプロジェクトを作成

Firebaseにログインしてプロジェクトを作成する。

作成したプロジェクトから「プロジェクトの設定」を探して「ウェブアプリにFirebaseを追加」をクリックして、jsの記述をコピる。

下記のようなヤツ。

<script src="https://www.gstatic.com/firebasejs/4.6.2/firebase.js"></script>
<script>
  // Initialize Firebase
  var config = {
    apiKey: "〇〇〇〇",
    authDomain: "〇〇〇〇.firebaseapp.com",
    databaseURL: "https://〇〇〇〇.firebaseio.com",
    projectId: "〇〇〇〇",
    storageBucket: "〇〇〇〇.appspot.com",
    messagingSenderId: "〇〇〇〇"
  };
  firebase.initializeApp(config);
</script>

▼後述の各ソースで使われる値

「/index.php」 上記
「/manifest.json」の「gcm_sender_id」 上記の「messagingSenderId」の値
「/pushSender.php」の「GOOGLE_API_KEY」 Firebaseにログインして、プロジェクトの設定からサーバーキーを探す

サンプルソース

jsがらみの読み込みが・・・キャッシュの問題で2日近く調べるハメに。それを考慮したソース。

必要なファイル

・空のテキストファイルでファイル名は「subscriptions.data」
・プッシュ通知で使う画像ファイル「icon.jpg」

PCでプッシュ通知の許可を求める

ファイル用の読み込み関連は太字にしています。

/index.php

<?php $cache = '?'.time(); ?><!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>プッシュ通知のサンプル</title>
  <meta name="mobile-web-app-capable" content="yes">
  <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
  
  <script src="https://www.gstatic.com/firebasejs/4.6.2/firebase.js"></script>
  <script>
    // Initialize Firebase
    var config = {
      apiKey: "〇〇〇〇",
      authDomain: "〇〇〇〇.firebaseapp.com",
      databaseURL: "https://〇〇〇〇.firebaseio.com",
      projectId: "〇〇〇〇",
      storageBucket: "〇〇〇〇.appspot.com",
      messagingSenderId: "〇〇〇〇"
    };
    firebase.initializeApp(config);
  </script>
  
  <link rel="manifest" href="./manifest.json<?php echo $cache; ?>">
  <script src="./index.js<?php echo $cache; ?>"></script>
</head>

<body>
  <div>
    <h1>プッシュ通知のサンプル</h1>
    <p>
      <a href="#" id="push_regist">通知登録して!!!!</a><br>
      <a href="#" id="push_delete">通知登録消して!!!!</a><br>
    </p>
  </div>
  
  <script>
    var nowPath = location.protocol + '//' + location.host + location.pathname.replace('index.php', '');
    
    var basePath = nowPath;
    var tmpPrms = {
       'isLocalhost': true,
       'pathRecieve': basePath + 'recieve.php',
       'pathSwJs':    basePath + 'sw.js<?php echo $cache; ?>',
    };
    
    askForPushNotification(tmpPrms);
  </script>
</body>
</html>

/index.js

/** Push通知の許可をもらう **/
var askForPushNotification = function(tmpOpt) {
  var self = this;
  var isLocalhost,pathRecieve,pathSwJs,idBtnAddPush,idBtnDelPush;
  
  // 通知登録の追加・削除の切り替え
  var switchBtn = function(isAdd) {
    if ( isAdd ) {
      $(idBtnAddPush).show();
      $(idBtnDelPush).hide();
    }
    else {
      $(idBtnAddPush).hide();
      $(idBtnDelPush).show();
    }
  }
  
  /** プッシュ通知の許可 **/
  var initialiseState = function() {
    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);
        
        if (!subscription || subscription==null) {
          switchBtn(true);
          return;
        }
        else {
          switchBtn(false);
        }
      }).catch(function(err){
        console.warn("Error during getSubscription()", err);
      });
    });
  }
  
  /** プッシュ通知を有効化 **/
  var subscribe = function() {
    // 発行したサブスクリプションをサーバー側(/recieve.php)に送信します。
    var saveSubscriptionOfServer  = function(subscription) {
      console.log('sending to server for regist:', subscription);
      
      var data = {
         "subscription" : subscription.endpoint
      };
      
      $.ajax({
        type:     'POST',
        url:       pathRecieve,
        dataType: 'json',
        cache:     false,
        data:      data
      });
    }
    
    navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
      serviceWorkerRegistration.pushManager.subscribe({
        userVisibleOnly: true
      }).then(function(subscription) {
        // ボタンの表示切替
        switchBtn(false);
        
        // サーバにも登録
        return saveSubscriptionOfServer(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);
        }
      })
    });
  }
  
  /** プッシュ通知を無効化 **/
  var unsubscribled = function() {
    // サブスクリプションをサーバーから削除する処理。テストなので実装していません。
    var deleteSubscriptionOfServer = function(subscrption) {
      console.log('sending to server for delete:', subscrption);
      
      //var data = {
      //   "subscription" : subscription.endpoint
      //};
      //
      //$.ajax({
      //  type:     'POST',
      //  url:       pathRecieve,
      //  dataType: 'json',
      //  cache:     false,
      //  data:      data
      //});
    }
    
    navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
      serviceWorkerRegistration.pushManager.getSubscription().then( function(subscription) {
        if (!subscription ) {
          switchBtn(true);
          return;
        }
        
        // サーバからも削除
        deleteSubscriptionOfServer (subscription);
        
        subscription.unsubscribe().then(function(successful) {
          switchBtn(true);
        }).catch(function(e) {
          console.error("Unsubscription error: ", e);
          switchBtn(true);
        });
      }).catch(
        function(e) {
          console.error("Error thrown while unsubscribing from push messaging.", e);
        }
      )
    });
  }
  
  var setEvent = function() {
    // サブスクリプションを発行します
    $(idBtnAddPush).on("click", function(){
      Notification.requestPermission(function(permission) {
        if(permission !== "denied") {
          subscribe();
        }
        else {
          alert ("プッシュ通知を有効にできません。ブラウザの設定を確認して下さい。");
        }
      });
    });
    
    // サブスクリプションをサーバ、ブラウザ共に削除します
    $(idBtnDelPush).on("click", function(){
      unsubscribled();
    });
  }
  
  var init = function(tmpOpt) {
    isLocalhost  = (tmpOpt.isLocalhost!=null)?  tmpOpt.isLocalhost:   false;
    pathRecieve  = (tmpOpt.pathRecieve!=null)?  tmpOpt.pathRecieve:  '/recieve.php';
    pathSwJs     = (tmpOpt.pathSwJs!=null)?     tmpOpt.pathSwJs:     './sw.js';
    idBtnAddPush = (tmpOpt.idBtnAddPush!=null)? tmpOpt.idBtnAddPush: '#push_regist';
    idBtnDelPush = (tmpOpt.idBtnDelPush!=null)? tmpOpt.idBtnDelPush: '#push_delete';
    
    // プッシュ通知の許可
    if ("serviceWorker" in navigator && (window.location.protocol === "https:" || isLocalhost)) {
      navigator.serviceWorker.register(pathSwJs).then( function (registration) {
        if (typeof registration.update == "function") {
          registration.update();
        }
        
        initialiseState();
      }).catch(function (error) {
        console.error("Service Worker registration failed: ", error);
      });
    }
    
    setEvent();
  }
  init(tmpOpt);
}

/manifest.json

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

/recieve.php

<?php
  // ファイルに格納する:DBに格納するならそういう処理を行う
  file_put_contents("subscriptions.data", $_POST['subscription']. PHP_EOL, FILE_APPEND);

/sw.js

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

/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) {
  var pathNotification = '/notifications.php';
  
  console.log("Push message received", event);
  event.waitUntil(getEndpoint().then(function(endpoint) {
    //通知内容をサーバに取得しに行きます。
    return fetch(pathNotification+"?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");
  });
}

サーバでプッシュ通知を送る(PCでプッシュ通知を受け取る)

/pushSender.php

<?php
define('GOOGLE_API_URL','https://android.googleapis.com/gcm/send');
define('GOOGLE_API_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);
    }
    
    if ( count($registration_ids) == 0 ) {
      return 'no registration_ids';
    }
    
    $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();
echo $test->sendData('hello');

/notifications.php

<?php
$tmpTtl = 'タイトル' . date("Y/m/d H:i:s");
$tmpIcn = 'icon.jpg';
$tmpBdy = 'ボディ。' . $_GET['endpoint'];
$tmpUrl = 'http://' . $_SERVER["HTTP_HOST"];

$echo = '{'
      . '"title":"' . $tmpTtl . '",'
      . '"icon":"'  . $tmpIcn . '",'
      . '"body":"'  . $tmpBdy . '",'
      . '"url":"'   . $tmpUrl . '"'
      . '}';
echo $echo;

確認方法

・index.phpにアクセス→「通知登録して」っていうリンクをクリック(初クリックだとプッシュ通知の許可を求められるはず)
・subscriptions.dataにデータが書き込まれているか確認
・pushSender.phpにアクセス→プッシュ通知される・・・はず。

"results":[{"error":"MismatchSenderId"}]}」というエラーが出て約2日調査に費やした・・・。結局はJSのキャッシュ絡みってトコに落ち着いた。

注意

・jsの読み込みはキャッシュの絡みがあるので「〇〇.js?日時」「〇〇.json?日時」とかにしておく

・「/sw.js(service-worker.js)」はアプリのあるトップディレクトリに置かないとダメなときもある。

・「manifest.json」はheadタグ内にいれておかないとエラーがでるかもしれない(プログラム絡みで何も気にせずにbodyタグ内の下部にいれていたら下記のようなエラーがでた)

Unable to subscribe to push. DOMException: Registration failed - missing applicationServerKey, and manifest empty or missing

関連

Chromeでプッシュ通知の許可・拒否の取り消し

参考

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