ソリッドシード株式会社

PWAで行うPush通知

2021年11月30日

以前、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

⼿順リスト

  1. 1. Push通知ライブラリのインストール
  2. 2. Pushサーバ認証⽤鍵の作成
  3. 3. Service Workerの準備と登録
  4. 4. Push準備
  5. 5. Push通知実⾏
1. Push通知ライブラリのインストール
composer require minishlink/web-push 

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


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

今回は.envファイルに転記しておきます。

 
$ 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


3. 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); 
         }); 
 } 
}); 
 
4. 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); 
       } 
     ); 
   }); 
} 
 
5. 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
      }
    }
  }
   

以上です。

投稿者:Komine