技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

Firebase で push 通知

目標

Push 通知機能を Firebase を利用して実装してみるテストプログラム。
本当は ServiceWorker を時前実装でやろうとしてたのだけど、そのサンプルで多様されてる Firebase が気づいたら独自フレームワークになってたらしいので、それで試してみた。

Firebase にアカウントを作ろう!

まずはしれっと Firebase でアカウントを作成する。
アカウント自体は無料で作成できる。

基本は Google アカウントでログイン出来るので、それを自分は行った。

ログインしたら Firebase プロジェクトの作成を行っていく。

Push 通知さえ試せればいいので、Analytics とか要らん…欲しい人はどうぞ

プロジェクトを作ったら、その設定を開く。

マイアプリではアプリを追加。

Web アイコンを選んで

適当な名前で登録しよう。

するとこんな感じになるはず。

次に「Clowd Messaging」のタブを開いて、サービスアカウントを作成します。
特に「ウェブプッシュ証明書」は必須。

ここまできたらコンソールに進みましょう。

ローカルアプリの準備

Firebase SDK の準備

Firebase コマンドツール群をインストールしてしまいます。

firebase.google.com

具体的には nodejs が入っていれば npm install -g firebase-tools でOKです。

Firebase プロジェクトの初期化

firebase init と叩きます。
色々出てきますが「Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys」にチェックを入れておきます。

オプションのセレクトは、Use an existing project で、既存のプロジェクトに紐づけます。
各種オプションはこんな感じで

出来上がったプロジェクトはこんな感じ。

firebase serve と叩くとテストサーバが起動します。

Push 通知に対応する

index.html を次のように改変。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Welcome to Firebase Hosting</title>

    <!-- update the version number as needed -->
    <script src="/__/firebase/9.18.0/firebase-app-compat.js"></script>
    <script src="/__/firebase/9.18.0/firebase-messaging-compat.js"></script>
    <script src="/__/firebase/init.js"></script>

    <link rel="manifest" href="/manifest.json">
  </head>
  <body>
    <div class="demo-layout mdl-layout mdl-js-layout mdl-layout--fixed-header">
    
      <main class="mdl-layout__content mdl-color--grey-100">
        <div class="mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-grid">
    
          <!-- Container for the Table of content -->
          <div class="mdl-card mdl-shadow--2dp mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-cell--12-col-desktop">
            <div class="mdl-card__supporting-text mdl-color-text--grey-600">
              <!-- div to display the generated registration token -->
              <div id="token_div" style="display: none;">
                <h4>Registration Token</h4>
                <p id="token" style="word-break: break-all;"></p>
                <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored"
                        onclick="deleteToken()">Delete Token</button>
              </div>
              <!-- div to display the UI to allow the request for permission to
                   notify the user. This is shown if the app has not yet been
                   granted permission to notify. -->
              <div id="permission_div" style="display: none;">
                <h4>Needs Permission</h4>
                <p id="token"></p>
                <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored"
                        onclick="requestPermission()">Request Permission</button>
              </div>
              <!-- div to display messages received by this app. -->
              <div id="messages"></div>
            </div>
          </div>
    
        </div>
      </main>
    </div>

    <script>
      // Retrieve Firebase Messaging object.
      const messaging = firebase.messaging();
    
      // IDs of divs that display registration token UI or request permission UI.
      const tokenDivId = 'token_div';
      const permissionDivId = 'permission_div';
    
      // Handle incoming messages. Called when:
      // - a message is received while the app has focus
      // - the user clicks on an app notification created by a service worker
      //   `messaging.onBackgroundMessage` handler.
      messaging.onMessage((payload) => {
        console.log('Message received. ', payload);
        // Update the UI to include the received message.
        appendMessage(payload);
      });
    
      function resetUI() {
        clearMessages();
        showToken('loading...');
        // Get registration token. Initially this makes a network call, once retrieved
        // subsequent calls to getToken will return from cache.
        messaging.getToken({vapidKey: 'BKQMXfEsYlpUr2Eszc0Ojg2xhvA9QfwlbIlu5YSmSFjF-y7yhm7vbkQihpbGllRtkCV60WPwQ-38Ndl-wDqdukM'}).then((currentToken) => {
          if (currentToken) {
            sendTokenToServer(currentToken);
            updateUIForPushEnabled(currentToken);
          } else {
            // Show permission request.
            console.log('No registration token available. Request permission to generate one.');
            // Show permission UI.
            updateUIForPushPermissionRequired();
            setTokenSentToServer(false);
          }
        }).catch((err) => {
          console.log('An error occurred while retrieving token. ', err);
          showToken('Error retrieving registration token. ', err);
          setTokenSentToServer(false);
        });
      }
    
    
      function showToken(currentToken) {
        // Show token in console and UI.
        const tokenElement = document.querySelector('#token');
        tokenElement.textContent = currentToken;
      }
    
      // Send the registration token your application server, so that it can:
      // - send messages back to this app
      // - subscribe/unsubscribe the token from topics
      function sendTokenToServer(currentToken) {
        if (!isTokenSentToServer()) {
          console.log('Sending token to server...');
          // TODO(developer): Send the current token to your server.
          setTokenSentToServer(true);
        } else {
          console.log('Token already sent to server so won\'t send it again ' +
              'unless it changes');
        }
      }
    
      function isTokenSentToServer() {
        return window.localStorage.getItem('sentToServer') === '1';
      }
    
      function setTokenSentToServer(sent) {
        window.localStorage.setItem('sentToServer', sent ? '1' : '0');
      }
    
      function showHideDiv(divId, show) {
        const div = document.querySelector('#' + divId);
        if (show) {
          div.style = 'display: visible';
        } else {
          div.style = 'display: none';
        }
      }
    
      function requestPermission() {
        console.log('Requesting permission...');
        Notification.requestPermission().then((permission) => {
          if (permission === 'granted') {
            console.log('Notification permission granted.');
            // TODO(developer): Retrieve a registration token for use with FCM.
            // In many cases once an app has been granted notification permission,
            // it should update its UI reflecting this.
            resetUI();
          } else {
            console.log('Unable to get permission to notify.');
          }
        });
      }
    
      function deleteToken() {
        // Delete registration token.
        messaging.getToken().then((currentToken) => {
          messaging.deleteToken(currentToken).then(() => {
            console.log('Token deleted.');
            setTokenSentToServer(false);
            // Once token is deleted update UI.
            resetUI();
          }).catch((err) => {
            console.log('Unable to delete token. ', err);
          });
        }).catch((err) => {
          console.log('Error retrieving registration token. ', err);
          showToken('Error retrieving registration token. ', err);
        });
      }
    
      // Add a message to the messages element.
      function appendMessage(payload) {
        const messagesElement = document.querySelector('#messages');
        const dataHeaderElement = document.createElement('h5');
        const dataElement = document.createElement('pre');
        dataElement.style = 'overflow-x:hidden;';
        dataHeaderElement.textContent = 'Received message:';
        dataElement.textContent = JSON.stringify(payload, null, 2);
        messagesElement.appendChild(dataHeaderElement);
        messagesElement.appendChild(dataElement);
      }
    
      // Clear the messages element of all children.
      function clearMessages() {
        const messagesElement = document.querySelector('#messages');
        while (messagesElement.hasChildNodes()) {
          messagesElement.removeChild(messagesElement.lastChild);
        }
      }
    
      function updateUIForPushEnabled(currentToken) {
        showHideDiv(tokenDivId, true);
        showHideDiv(permissionDivId, false);
        showToken(currentToken);
      }
    
      function updateUIForPushPermissionRequired() {
        showHideDiv(tokenDivId, false);
        showHideDiv(permissionDivId, true);
      }
    
      resetUI();
    </script>
  </body>
</html>

で、「<!-- keypair -->」をFirebase console のこれに置き換え。

manifest.json はコレ。
ぶっちゃけコレだけ。(値も固定値で)

{
    "gcm_sender_id": "103953800507"
}

firebase-messaging-sw.js も作成します。

importScripts('/__/firebase/9.18.0/firebase-app-compat.js');
importScripts('/__/firebase/9.18.0/firebase-messaging-compat.js');
importScripts('/__/firebase/init.js');

const messaging = firebase.messaging();

importScripts('https://www.gstatic.com/firebasejs/9.18.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/9.18.0/firebase-messaging-compat.js');

firebase.initializeApp({
    apiKey: "",
    authDomain: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: "",
});

firebase.initializeApp の中身はFirebaseの赤線で囲った部分。

これで準備完了。

サンプルを実行してみる

起動するとこんな画面になります。

そしたら、Firebase console の Messaging から「新しいキャンペーンを作成」「通知」を選ぶ

通知の作成でメッセージを書いたら「テストメッセージを送信」する。
「FCMトークン」なるものは、さっきの長ったらしい文字列を貼り付けよう。

そしてメッセージを送信すると、Windows 上にこんなメッセージが飛ぶようになる。

一旦飛ぶようになれば、ブラウザを閉じていても受け取るようになる。

最後に

本当は ServiceWorker の第二弾としてやりたかったんだけどなぁ(-_-;)
いつの間にか仕様が変わっておった…。

仕方ないので、今度は NodeJS サーバを立ててやってみることにしよう…