ニフクラ mobile backend(mBaaS)お役立ちブログ

スマホアプリ開発にニフクラ mobile backend(mBaaS)。アプリ開発に役立つ情報をおとどけ!

mBaaSからWeb Pushを送ってみよう

f:id:mbaasdevrel:20180703175531p:plain

最近、PWAに注目が集まっています。PWAはProgressive Web Appの略で、一つの技術ではなく幾つかの技術が組み合わさった、スマートフォンWebアプリのベストプラクティスといったキーワードになります。

PWAを構成する技術の一つにプッシュ通知があります。最近、メディアサイトなどでWebサイトを訪問した際にプッシュ通知を受け取りますかと言った確認が出ることがあるでしょう。そうしたメッセージを出す技術になります。

現状のmBaaSではWebプッシュには対応していないのですが、Webブラウザから取得したトークンを保存しておく場所としてmBaaSを利用することは可能です。そこで今回はmBaaSと組み合わせた、サーバレスでのWebプッシュ通知実装を紹介します。

Webプッシュの3つのパターン

一言でWebプッシュと言っても3つのパターンがあります。

一つはローカルプッシュで、これはWebページを開いているユーザに対して使えるものです。タブを閉じてしまうと表示されません。サーバレスで使えますが、Webページを開いていなければならないという問題があるので用途も限られます。チャットなどで新着を伝えたりするのに使えるでしょう。

もう一つはGoogle ChromeやSafariなどが独自で実装しているプッシュ通知です。他のブラウザと互換性がありませんので、実装を個々に行う必要があります。例えばGoogle ChromeであればFirebaseのプロジェクト登録が必要だったり、Safariであれば証明書の作成などが必要になります。

最後はMozillaが採用していた方法で、VAPIDという仕組みになります。これは現在、EdgeやGoogle Chromeでも対応しています。VAPIDはVoluntary Application Server Identification for Web Pushの略で、Firebaseプロジェクトの登録が不要になるだけでなく、各ブラウザの実装が共通化されます。将来的にはSafariもこの方式に沿うのではないでしょうか。

今回はこのVAPIDを使った方法を紹介します。

利用する技術

  • Service Worker
  • Push API with VAPID
  • Node.js

今回はNode.js/JavaScriptで実装します。Node.jsではVAPIDに対応した web-push というライブラリがありますので、これを使います。

鍵の生成

まず公開鍵、秘密鍵を生成します。これは web-push にて可能です。以下のようなコードを実行します。

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

そうすると公開鍵(publicKey)と秘密鍵(secretKey)を出力します。公開鍵はWebブラウザ側でも使いますので、以下のような形で保存しておきます(ファイル名は keys.json とします)。秘密鍵は privateKey.json として保存しています。アプリケーションキー(applicationKey)とクライアントキー(clientKey)はmBaaSのものになります。

{
  "publicKey": "BOO...YEk",
  "applicationKey": "aed...dfd",
  "clientKey": "30e...7cd"
}

privateKey.jsonの内容は以下のようになります。

{
  "privateKey": "IW...o1c"
}

HTMLの作成

今回のHTMLは必要最低限です。 js/bundle.js はこれから作成します。

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title></title>
    <meta charset="utf-8" />
    <meta name="description" content="" />
    <meta name="author" content="" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="" />
    <!--[if lt IE 9]>
    <script src="//cdn.jsdelivr.net/html5shiv/3.7.2/html5shiv.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/respond.js/1.4.2/respond.min.js"></script>
    <![endif]-->
    <link rel="shortcut icon" href="" />
  </head>
  <body>
    <!-- Example: <script src="//ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> -->
    <div id="subscribe">購読する</div>
    <script src="js/bundle.js"></script>
  </body>
</html>

クライアントのJavaScript

Webブラウザ向けのJavaScriptを作成します。今回はgulpを使っています。

初期化

まずライブラリを読み込みます。

const NCMB = require('ncmb');
const config = require('../keys.json');
const ncmb = new NCMB(config.applicationKey, config.clientKey);
const WebPush = ncmb.DataStore('WebPush');

Service Workerの登録

次にService Workerの登録を行います。Service Workerがある場合は、すでに購読済みかチェックします。 findOrcreate は後ほど作成します。

document.addEventListener('DOMContentLoaded', async e => {
  // Service Worker非対応
  if (!('serviceWorker' in navigator)) return;
  if (!('showNotification' in ServiceWorkerRegistration.prototype)) return;
  
  await navigator.serviceWorker.register('/serviceworker.js')
  // プッシュ通知を拒否された場合
  if (Notification.permission === 'denied') return;
  // PushManagerが存在しない場合
  if (!('PushManager' in window)) return;
  const serviceWorkerRegistration = await navigator.serviceWorker.ready;
  const subscription = await serviceWorkerRegistration.pushManager.getSubscription();
  if (subscription) {
    // すでに購読中
    const json = subscription.toJSON();
    await findOrcreate(json.endpoint, json);
  } else {
    // 未購読
    console.log('未購読です');
  }
});

購読処理

購読するというラベルをクリックした時のイベントは以下の通りです。 config.publicKey に生成した公開鍵が入ります。これをUnit8の配列に変換して、 applicationServerKey に指定します。

document.querySelector('#subscribe').onclick = async (e) => {
  const vapidPublicKey = config.publicKey;
  const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
  const serviceWorkerRegistration = await navigator.serviceWorker.ready
  try {
    const subscription = await serviceWorkerRegistration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: convertedVapidKey
    });
    if (subscription) {
      const json = subscription.toJSON();
      await findOrcreate(json.endpoint, json);
    }
  } catch (e) {
    console.log('購読拒否しました');
  }
};

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;
}

購読データの保存

これで取得できる購読データ(subscription)は下記のような形式になっています。

{
  "endpoint":"https://fcm.googleapis.com/fcm/send/e7K...Hag",
  "expirationTime":null,
  "keys":{
    "p256dh":"BK7...e5U",
    "auth":"DGt...D8g"
  }
}

このデータをすべてmBaaSに保存します。その際、endpointをキーとして重複しないようにします。

const findOrcreate = async (endpoint, json) => {
  const result = await WebPush
    .equalTo('endpoint', endpoint)
    .count()
    .fetchAll();
  if (result.count == 0) {
    const webPush = new WebPush();
    await webPush
      .set('endpoint', json.endpoint)
      .set('json', json)
      .save();
  }
}

これで購読データの保存が完成です。

プッシュ通知を送信する

プッシュ通知の送信は、mBaaSから上記の購読データをダウンロードし、実行するだけです。注意点としては、秘密鍵(privateKey)も渡すことです。タイトル、メッセージ、アイコンそしてバッジなどがカスタマイズできる情報になります。

const NCMB = require('ncmb');
const config = require('./keys.json');
const sec = require('./privateKey.json');
const webpush = require('web-push');

const ncmb = new NCMB(config.applicationKey, config.clientKey);
const WebPush = ncmb.DataStore('WebPush');
webpush.setVapidDetails(
  "mailto:admin@moongift.jp",
  config.publicKey,
  sec.privateKey
);

(async () => {
  const icon = `app.png`;
  const params = {
    title: "プッシュ通知です!",
    msg: `これはサーバから送っています. 今は ${new Date().toLocaleString()} です。 メッセージとアイコンも送っています `,
    icon:  icon
  };
  
  const results = await WebPush
    .fetchAll();
  const subscriptions = []
  for (const webPush of results) {
    await webpush.sendNotification(webPush.json, JSON.stringify(params), {});
  }
})();

このようにすることで、プッシュ通知が送信できます。プッシュ通知の送信内容もmBaaSから取得すれば、自由にカスタマイズしたメッセージで送れるようになります。

f:id:mbaasdevrel:20180703175531p:plain

まとめ

プッシュ通知を送信する仕組みは別途必要ですが、それはローカルのコンピュータからでも可能です。そうすれば静的サイト(Amazon S3やGitHub Pagesなど)でもプッシュ通知の購読を取得し、さらにメッセージの送信もできるようになります。ぜひお試しください。

中津川 篤司

中津川 篤司

NCMBエヴァンジェリスト。プログラマ、エンジニアとしていくつかの企業で働き、28歳のときに独立。 2004年、まだ情報が少なかったオープンソースソフトの技術ブログ「MOONGIFT」を開設し、毎日情報を発信している。2013年に法人化、ビジネスとエンジニアを結ぶDXエージェンシー「DevRel」活動をスタート。