フリーランスエンジニア向けIT系求人・仕事・案件情報サイト
更新日

PWAのプッシュ通知実装、notification apiとの違いを解説!

PWAで行うPush通知

以前、PWAについて紹介しましたが、
今回はPWAでのPush通知(WebPush)にフォーカスしてお伝えしたいと思います。

WebPushのメリット・デメリット

  • メリット
    • 手軽に既存サイトでPush通知機能を追加できる(低コストで試せる)
    • ユーザーがアプリをインストールする必要がない(通知許可だけ)
  • デメリット
    • 挙動がブラウザに依存する
    • 非対応ブラウザ、OSがある

PushAPIとNotificationAPI

PWAでPush通知を行う場合、PushAPIとNotificationAPIを使用します。
PushAPIはPushサーバからの通知を受け取り、NotificationAPIはユーザーへ通知を行います。

Push API は、ウェブアプリケーションがサーバーからメッセージ (プッシュ通知) を受信できるようにします。ウェブアプリケーションがフォアグランド状態かどうか、読み込まれているかどうかに関わらず利用できます。開発者は、オプトインしたユーザーへ非同期の通知と更新を届けることができ、タイムリーな新着コンテンツによってユーザーの関心を得られるでしょう。
出典元:MDN Web Docs – PushAPI

NotificationNotifications API のインターフェイスで、ユーザーへのデスクトップ通知の設定と表示に使われます。これらの通知の表示方法や機能はプラットフォームによって異なりますが、一般にユーザーに対して非同期に情報を提供する方法を提供します。
出典元:MDN Web Docs – Notification

どちらもアプリにService Workerがインストールされている必要があります。

配信サーバ(Pushサーバ)

Push通知を行う為には配信サーバで認証を行う必要があります。
認証方式は主に

  • FCM (Firebase Cloud Messaging)
  • VAPID (Voluntary Application Server Identification for Web Push)

の2つがあります。
この記事ではFirebaseにプロジェクトを準備しなくて良い等、手間がかからない利点があるVAPIDを利用します。

Push通知を実装してみる

構成

laravel   
┣ app   
┃ ┣ Http   
┃ ┃ ┗ Controllers   
┃ ┃  ┗ NotificationController.php    
┃ ┗ Models   
┃  ┗ Notification.php   
┣ public   
┃ ┣ pwa_asset   
┃ ┃ ┣ push.js  //起点となるjsファイル 
┃ ┃ ┗ manifest.json 
┃ ┗ sw.js  //Service Worker 
┗ resources   
  ┗ views   
   ┗ header.blade.php   

バージョン情報

  • PHP 7.4.21
  • Laravel 6.20.30
  • web-push-libs/web-push-php ^6.0

⼿順リスト

  • Push通知ライブラリのインストール
  • Pushサーバ認証⽤鍵の作成
  • Service Workerの準備と登録
  • Push準備
  • Push通知実⾏

Push通知ライブラリのインストール

composer require minishlink/web-push 

引用元:github – web-push-libs/web-push-php


アプリケーションサーバ識別⽤の鍵を作成

$ openssl ecparam -genkey -name prime256v1 -out private_key.pem
$ openssl ec -in private_key.pem -pubout -outform DER|tail -c 65|base64|tr -d '=' |tr '/+' '_-' >> public_key.txt
$ openssl ec -in private_key.pem -outform DER|tail -c +8|head -c 32|base64|tr -d '=' |tr '/+' '_-' >> private_key.txt

引用元:github – web-push-libs/web-push-php


Service Workerの準備と登録

JavaScriptでServiceWorkerを登録します。
まずServiceWorkerのJavaScriptファイルを作成します。

[sw.js]
// プッシュイベント
self.addEventListener("push", (event) => {
  let data = event.data.text();
  data = JSON.parse(data);
  const options = {
      body: data.body,
      icon: data.icon,
      actions: [
          { action: "yes", title: "yes" },
          { action: "no", title: "no" },
      ],
  };
  event.waitUntil(self.registration.showNotification(data.title, options));
});
// プッシュ通知をクリックしたときのイベント
self.addEventListener("notificationclick", (event) => {
    event.notification.close();
    if (event.action === "yes") {
     console.log('clicked yes');
    } else if (event.action === "no") {
     console.log('clicked no');
    } else {
      console.log('something else');
    }
});
self.addEventListener("fetch", (event)=> {
  console.log('fetched');
});
self.addEventListener("install", (event) => {
  console.log("service worker install ...");
});
self.addEventListener('activate', (event) => {
    console.log("activated");
});

次にServiceWorkerを登録する為のJavaScriptを作成します。
この部分が処理の起点になります。

[push.js] 
self.addEventListener("load", async () => { 
 if ("serviceWorker" in navigator) { 
     window.sw = await navigator.serviceWorker 
         // 作成したServiceWorkerのJavaScriptファイルを指定
         .register("/sw.js", { scope: "/" }) 
         .then((reg) => { 
           reg.onupdatefound = function() { 
             reg.update(); 
           } 
         }).catch(function (err) { 
           console.log("Failed ! Error: ", err); 
         }); 
 } 
}); 

Push準備

ユーザー通知許可を得た後で、
Push通知に必要な情報(エンドポイント、公開鍵、トークン)の取得とDB保存を行います。

[push.js] 
function allowPushNotification(appServerKey) { 
  if ("Notification" in window) {
      let permission = Notification.permission;
      if (permission === "denied") {
          alert("Push通知が拒否されています。ブラウザのPush通知を許可してください");
          return false;
      }
  }
  // 公開鍵
  const applicationServerKey = urlB64ToUint8Array(appServerKey); 
  // 公開鍵とパラメータを渡し、戻り値で取得したエンドポイント、公開鍵、トークンをDB登録する。
  navigator.serviceWorker.ready.then( 
   function(serviceWorkerRegistration) { 
     let options = {
       userVisibleOnly: true, 
       applicationServerKey: applicationServerKey 
     }; 
     // ユーザーからの通知許可が出ると以下が実行される
     serviceWorkerRegistration.pushManager.subscribe(options).then( 
       function(pushSubscription) {
         const key = pushSubscription.getKey("p256dh"); 
         const token = pushSubscription.getKey("auth"); 
         let data = new FormData() 
         data.append('endpoint', pushSubscription.endpoint) 
         data.append('userPublicKey', 
                    key ? btoa( String.fromCharCode.apply(null, new Uint8Array(key)) ) : null), 
         data.append('userAuthToken', 
                    token ? btoa(String.fromCharCode.apply(null, new Uint8Array(token))) : null)            
         let csrf_token = document.getElementsByName('csrf-token')[0].getAttribute('content');
         // DB登録
         fetch('/subscription', { 
           method: 'POST', 
           body: data, 
           headers: { 
             'X-CSRF-TOKEN': csrf_token
           }, 
         }).then(() => console.log('Subscription ended')) 
       }, function(error) { 
         console.log(error); 
       } 
     ); 
   }); 
}

Push通知実⾏

Pushサーバに通知を依頼します。
その際には先程DBに保存したエンドポイント、公開鍵、トークンを用います。

[Notification.php ]
 // Push通知実行
  public function execPushNotification()
  {
    // 環境変数から取得
    $auth = [
      'VAPID' => [
        'subject' => env('APP_VAPID_SUBJECT');
        'publicKey' => env('PWA_PUBLIC_KEY');
        'privateKey' => env('PWA_PRIVATE_KEY');
      ]
    ];
    // 認証する
    $webPush = new WebPush($auth);
    // 通知対象とぺーロードを設定
    $notifications[] = [
      'subscription' => Subscription::create([
        'endpoint' => 'DBに保存したエンドポイント',
        'publicKey' => 'DBに保存した公開鍵',
        'authToken' => 'DBに保存したトークン'
      ]),
      'payload' => '{
        "body":"this is body",
        "title":"this is title",
        "url":"https://example.com",
        "icon":"pwa_asset/logo.png"
      }'
    ],
    [
      'subscription' => Subscription::create([
        'endpoint' => 'DBに保存したエンドポイント',
        'publicKey' => 'DBに保存した公開鍵',
        'authToken' => 'DBに保存したトークン'
      ]),
      'payload' => '{
        "body":"this is body2",
        "title":"this is title2",
        "url":"https://example.com",
        "icon":"pwa_asset/logo2.png"
      }'
    ];
    // キューに入れる
    foreach ($notifications as $notification) {
      $webPush->queueNotification(
        $notification['subscription'],
        $notification['payload'] 
      );
    }
    // Push通知を依頼
    foreach ($webPush->flush() as $report) {
      $endpoint = $report->getRequest()->getUri()->__toString();
      if ($report->isSuccess()) {
        // sent successfully
      }
    }
  }

以上がPWAを用いたPush通知の概要となります。
興味のある方は是非触ってみてください。

\ ログインしなくても検討機能が使える♪ /
JavaScriptの案件を見てみる