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

Firebase Cloud Messaging(FCM)を利用したWebPush通知の実装について

More than 1 year has passed since last update.

最近FCMを利用したWebPush通知を実装する機会があったため、
その備忘録として簡単にまとめておきます。

WebPushについて

FacebookやGmail、SlackやChatworkなどを利用されているとブラウザの画面右上に飛んでくる通知。
(※ 画像はサンプルで開発したWebPush通知のスクショです)

FCMを利用したWebPush.png

これをFCMを利用して実装してみました。

FCMとは

FCMとは、Firebase Cloud Messagingの略で、
W3CをIETFで標準化が進むWebPushをFCMを利用して通知を送信できるようになりました。

これまでは、Firefoxでは事前のキーやIDが必要なくWebPushを送信出来ていましたが、
GoogleChromeではAndroidのアプリケーションと同様に
GoogleCloudMessaging(GCM)の登録やキーとIDの取得が必要な状態でした。

FCMでは、VAPIDによる認証を利用することで、
GCMのような登録を不要としたWebPushを実現することが可能になっています。

WebPushの流れ

Push送信までの流れを簡単に説明すると

  1. アプリケーションサーバで鍵を生成する
  2. ブラウザは、アプリケーションサーバから公開鍵を受け取る
  3. ブラウザは、取得した公開鍵を利用して、プッシュサーバにプッシュ通知の購読を登録する
  4. アプリケーションサーバから、プッシュサーバに対して通知を送信する
    • この際にブラウザ側で取得したEndpointや認証情報は、アプリケーションサーバに渡しておく
  5. プッシュサーバから対象のブラウザに対して通知を送信する
  6. ブラウザは、ServiceWorkerでpushイベントを購読し、受信すれば何かしらの動作を行う

詳細については下記記事が分かりやすくまとまっていました。

プログラム

Node.js ver7.6.0を利用して実装してみました。

認証鍵の生成と交換(アプリケーションサーバ)

下記のようにアプリケーションサーバ側で鍵を生成し、
ブラウザからAPIが呼ばれたときに公開鍵を返すようにしています。

Node.js
const webpush = require('web-push');
const vapidKeys = webpush.generateVAPIDKeys();

app.get('/getpush', (req, res) => {
  return res.json({
    publicKey: vapidKeys.publicKey
  });
});

プッシュ通知購読処理(ブラウザ)

webpush.js
/**
 * npm web-push パッケージ サイトを参考
 *    https://www.npmjs.com/package/web-push
 */
function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);

  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

/**
 * serviceWorkerからSubscriptionを取得する
 */
async function getsubscription() {
  var reg = await navigator.serviceWorker.ready;
  var sub = await reg.pushManager.getSubscription();
  return sub;
}

/**
 * Subscriptionを取得するためにサーバ側で生成された
 * WebPush送信のための公開鍵をAPI経由で取得する
 */
async function getPublicKey() {
  var res = await fetch('getpush', {
                    method: 'GET',
                    headers: { 'Content-Type': 'application/json' }
                  }).then((res) => res.json());
  console.log('APIのレスポンス');
  console.log(res.publicKey);
  return res.publicKey;
}

/**
 * サーバから取得した公開鍵を元に
 * ServiceWorkerからSubscriptionを取得する
 */
async function subscribe(option) {
  var reg = await navigator.serviceWorker.ready;
  var sub = await reg.pushManager.subscribe(option);
  return sub;
}

/**
 * サーバから公開鍵を取得し、
 * ServiceWorkerからSubscriptionを取得する
 */
async function initSubscribe() {
  var vapidPublicKey = await getPublicKey();
  let option = {
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
  }
  return await subscribe(option);
}

/**
 * ページの読み込みが完了すれば、
 * WebPushを受け取るための準備を行う
 */
window.addEventListener('load', async () => {
  navigator.serviceWorker.register('./serviceworker.js');
  var sub = await getsubscription();
  if (!sub) {
    // ブラウザに通知許可を要求する
    var permission = await Notification.requestPermission();
    new Notification('WebPushの設定をしました');
    if (permission === 'denied') {
      return alert('ブラウザの通知設定をONにしてください');
    } else {
      sub = await initSubscribe();
    }
  }
  console.log(sub);
});

Push送信処理(アプリケーションサーバ)

Node.js
app.post('/send/webpush', (req, res) => {
  // ブラウザ側でプッシュサーバから取得した、Endpointとp256dh、authの認証情報をセットする
  var pushSubscription = {
      endpoint: req.body.endpoint,
      keys: {
          p256dh: req.body.p256dh,
          auth: req.body.auth
      }
  };
  // 送信するメッセージは、Json形式で送る必要がある
  var message = JSON.stringify({
    title: req.body.title,
    body: req.body.body,
    icon: req.body.icon,
    link: req.body.link,
  });
  var options = {
    TTL: 10000,
    vapidDetails: {
      subject: req.body.link,
      // 先ほど生成したVAPIDの鍵情報をセットする
      publicKey: vapidKeys.publicKey,
      privateKey: vapidKeys.privateKey
    }
  }
  // npmのweb-pushライブラリを利用して、通知を送信する
  webpush.sendNotification(pushSubscription, message, options).then((response)=>{
    return res.json({
      statusCode: response.statusCode || -1,
      message: response.message || '',
    });
  // 通知送信時にフォーマットエラーや必須パラメータの欠如、送信先が不明な場合などにエラーを検知する
  }).catch((error) => {
    console.log(error);
    return res.json({
      statusCode: error.statusCode || -1,
      message: error.message || '',
    });
  });
});

通知受信設定(ブラウザ)

ServiceWorkerを利用して、通知イベントを検知したときの処理を記載しておきます。

serviceworker.js
// ブラウザに対して通知イベントを検知した際に実行される処理を定義
self.addEventListener('push', (e) => {
  var data = e.data.json();
  var title = data.title;
  var options = {
    body : data.body,
    icon: data.icon,
    data: {
      link_to: data.link
    }
  };
  e.waitUntil(self.registration.showNotification(title, options));
});
// 通知バーがクリックされた際に実行される処理を定義
self.addEventListener('notificationclick', (e) => {
  e.notification.close();
  e.waitUntil(clients.openWindow(e.notification.data.link_to));
});

参考

megadreams14
平成元年生,兵庫県出身,スタートアップ企業(介護×IT)でCTOやってます。 AWS Summit 2014Tokyo,Jenkins Conference 2015, Developers Summit 2015で発表!! 介護×ITという分野に興味ある方、お気軽にご連絡下さい!!
https://brightvie.me/
brightvie
「あなたの“困った・できたらいいな“をカタチに」 ブライト・ヴィーは手作りのICTシステムをお届けするエンジニアチームです。
https://brightvie.me/
Why not register and get more from Qiita?
  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
No 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
ユーザーは見つかりませんでした