📌

Azure Notification Hubsで学ぶWeb Push通知入門

2024/12/09に公開

はじめに

普段は専用線接続の設計をしたり、NW関連のアサインが多いゴリゴリのインフラエンジニアです。
たまたまAzure Notification Hubs × Web Push通知の処理を実装する機会があったので、備忘がてら記事を執筆してみました。

Web Push通知の仕組み

まずは動作イメージをつかむ

百聞は一見に如かずということで、今回簡単なデモアプリを作成しました。
フロントエンドから、ファイルをBlobにアップロードすると、Event GridトリガーでバックエンドのFunctionsからNotification Hubsを使ってPush通知を送信しています。
(詳細なシーケンスやアーキテクチャはこちら)で解説しています。

以降、この動作を交えて解説していきたいと思います。
今回はこのデモアプリの実装内容を確認しながら、Notification Hubsを利用したWeb Push通知について理解を深めていきます。

Web Push通知の基本概念

Web Push通知は以下の要素で構成されています。

  1. Service Worker

    • ブラウザのバックグラウンドで動作
    • Push通知の受信と表示を担当
    • HTTPSに対応しているサーバーが必要(例外あり)
  2. Push Service

    • ブラウザベンダーが提供するサービス
    • FirefoxではMozilla Push Service、ChromeやEdgeではFCM(Firebase Cloud Messaging)
  3. VAPID認証

    • Voluntary Application Server Identification(VAPID)は、プッシュサービスに対してアプリケーションサーバーを識別・認証する仕組み
    • 公開鍵と秘密鍵のペアを使用
    • 公開鍵(applicationServerKey)はブラウザでの購読時に使用(送信元を識別するために使用)
    • 秘密鍵はプッシュサービスへの通知送信時に使用

通知の流れ

Push通知はざっくりと以下のような流れで行われます。

  • 初期登録フェーズではService Workerが登録され、
  • 通知有効化フェーズではユーザから通知許可を受け取ったのちに、Push Serviceに登録を行います。
  • 通知受信フェーズではWebアプリ側からPush Serviceを経由し、Push通知を行います。

シーケンスに起こすと以下のような流れです。

Azure Notification Hubsについて

Azure Notification Hubsとは?

サービスの概要については公式ドキュメント[1]から引用しました。

Azure Notification Hubs は、任意のバックエンド (クラウドまたはオンプレミス) から任意のプラットフォーム (iOS、Android、Windows など) に通知を送信できる、使いやすく、かつスケールアウトされたプッシュ エンジンを提供します。

Push通知を実現するために利用するサービスということがわかりますが、「Push ServiceさえあればNotification Hubsは要らないのでは…?」と思った方もいらっしゃるのではないでしょうか。
次節でメリットについてご紹介します。

Notification Hubsを利用するメリット

マルチプラットフォーム対応が容易

Notification Hubsを利用しない場合

先に説明したNotification Hubsを利用しない場合にPush通知を送信する場合の構成は以下の通りです。

[1:1]MS Learn 「Azure Notification Hubs とは」 「Push通知のしくみ」から引用

上記の図ではBackendが直接Push Serviceとやり取りをする必要があるため、iOSとAndoroid,Webアプリ等複数のプラットフォームを利用する場合、Push Serviceの分だけ開発を行う必要があります。

Notification Hubsを利用する場合

続いてNotification Hubsを利用するパターンを見ていきましょう。
開発者が実装するBackendからはNotification Hubsだけを見ればよくなるため、マルチプラットフォーム対応が容易となります。

[1:2]MS Learn 「Azure Notification Hubs とは」 「Azure Notification Hubs を使用する理由」から引用

豊富な配信パターン

加えて、Notification Hubsではタグやスケジューリングされた通知など、様々な方法でPush通知を行うことができます。

監視やモニタリングをAzureと統合できる

Push通知関連のメトリクスやログをAzure MonitorやLog Analytics等と連携してモニタリングできることもNotification Hubsを利用するメリットの1つといえるでしょう。

デモアプリの実装

ここからはデモアプリの実装を通して、Notification Hubsを用いたPush通知について理解を深めていきます。

1. 全体構成

必要なAzureリソースと構成

  • Static Web Apps(フロントエンド)
  • Functions(バックエンドAPI)
  • Notification Hubs
  • Storage Account(ファイルアップロード用)
  • Event Grid(Event Trriger用)

処理シーケンス

今回のデモアプリが目指す処理シーケンスは以下の通りです。
(フェーズの背景色と、↑で記載している構成図の線の色は揃えています。)

プロジェクト構成

Frontend資材とBackend資材の構成は以下の通りです。

project/
├── frontend/          // React アプリケーション
│   ├── src/
│   │   ├── components/
│   │   └── service-worker.js
│   └── public/
└── api/              // Azure Functions
    ├── shared_code/
    │   └── notification_hub.py
    └── function_app.py

2. フロントエンド実装

Service Workerの実装

まずはService Workerの実装です。
コード中にコメントで解説を載せています。

service-worker
public/service-worker.js

// 通知を受け取るイベントハンドラです。
self.addEventListener('push', function(event) {
    console.log('Push event received');
    
    try {
      // event.dataにプッシュメッセージのペイロードが含まれます。
        const data = event.data.json();
        // 通知オプションを設定しています。本文やアイコン等を定義しています。
        const options = {
            body: data.body,
            icon: '/icon.png',
            badge: '/badge.png'
        };

        // 通知表示が完了するまでService Workerを活性状態に保ちます。
        event.waitUntil(
            // showNotificationは通知を表示するAPIです。
            self.registration.showNotification(data.title, options)
        );
    } catch (error) {
        console.error('Error processing push event:', error);
    }
});

self.addEventListener('notificationclick', function(event) {
    console.log('Notification clicked');
    event.notification.close();
});

addEventListener[2]でService Worker上でイベントを待ち受け、showNotification[3]で通知を表示します。

通知時にペイロードから特定のフィールドを取得して、表示する通知に組み込むこともできます。

Reactコンポーネントの実装

続いて、フロントエンドについて通知関連部分に絞って解説していきます。

Reactコンポーネント
const NotificationComponent: React.FC = () => {
  const [isSubscribed, setIsSubscribed] = useState(false);

  const subscribeToPushNotifications = async () => {
    try {
      const registration = await navigator.serviceWorker.ready;
      
      // 通知の許可を要求します。
      // 2-2 通知許可ダイアログ表示
      const permission = await Notification.requestPermission();
      if (permission !== 'granted') {
        throw new Error('通知の許可が必要です');
      }

      // Push通知の購読
      // 2-4 Push Service登録
      const subscription = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: process.env.REACT_APP_VAPID_PUBLIC_KEY
      });

      // バックエンドに購読情報を送信
      // 2-6 POST /api/notifications/subscribe
      const response = await fetch('/api/notifications/subscribe', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(subscription)
      });

      if (!response.ok) {
        throw new Error('購読に失敗しました');
      }

      setIsSubscribed(true);
    } catch (error) {
      console.error('Subscription error:', error);
    }
  };

  return (
    <div>
      <button 
        onClick={subscribeToPushNotifications}
        disabled={isSubscribed}
      >
        {isSubscribed ? '通知購読済み' : '通知を有効にする'}
      </button>
    </div>
  );
};

まず、Notification.requestPermission[4]で通知の許可を求めるポップアップを起動します。
続いてregistration.pushManager.subscribe[5]で「2-4 Push Service登録」を行い、ブラウザ購読情報を取得しています。
この際、REACT_APP_VAPID_PUBLIC_KEYから取得したVAPID公開鍵を利用します。
ここで取得した購読情報を「2-6 POST /api/notifications/subscribe」でFunctionsに渡して、Notification Hubsにデバイス登録を行います。

3. バックエンド実装

続いてバックエンドの実装について確認していきます。
今回のデモアプリのバックエンドは、フロントエンドやEvent Gridから呼ばれるAPIです。

Azure Functions実装

初めにfunction_app.pyを確認していきます。
後に説明するNotification Hubsクライアントを呼び出すことで、Notification Hubs周りの操作を行います。

function_app.py
function_app.py

// Notification Hubsにデバイスを登録するAPIです。
@app.function_name(name="subscribeNotification")
// APIのパスやメソッド、認証関連の設定を行います。
@app.route(route="notifications/subscribe", methods=["POST", "OPTIONS"], auth_level=func.AuthLevel.ANONYMOUS)

def subscribe_notification(req: func.HttpRequest) -> func.HttpResponse:
    if req.method == "OPTIONS":
        return func.HttpResponse(
            status_code=200,
            headers={
                'Access-Control-Allow-Credentials': 'true',
                'Access-Control-Allow-Origin': '*',
                'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
                'Access-Control-Allow-Headers': 'Content-Type, Authorization',
                'Access-Control-Max-Age': '86400'
            }
        )

    try:
        connection_string = os.environ.get("NOTIFICATION_HUB_CONNECTION_STRING")
        hub_name = os.environ.get("NOTIFICATION_HUB_NAME")

        // ブラウザ購読情報を検査し、subscriptionに格納します。
        subscription = req.get_json()
        if not subscription or 'endpoint' not in subscription:
            return func.HttpResponse(
                json.dumps({"error": "Invalid subscription data"}),
                mimetype="application/json",
                status_code=400
            )

        // Notification Hubsクライアントを作成します。
        hub = NotificationHub(connection_string, hub_name)
        // create_registrationメソッドを呼び出します。
        // 2-7 デバイス登録
        location = hub.create_registration(subscription)
        
        return func.HttpResponse(
            json.dumps({"registrationId": location}),
            mimetype="application/json"
        )
        
    except Exception as e:
        logging.error(f'Error: {str(e)}')
        return func.HttpResponse(
            json.dumps({"error": str(e)}),
            status_code=500
        )

// Event Gridトリガーで通知を送信する関数です。
// 4-2 通知送信リクエスト
@app.event_grid_trigger(arg_name="event")
def main_notification(event: func.Event GridEvent):
    try:
        connection_string = os.environ["NOTIFICATION_HUB_CONNECTION_STRING"]
        hub_name = os.environ["NOTIFICATION_HUB_NAME"]

        // Notification Hubsクライアントを作成します。
        hub = NotificationHub(connection_string, hub_name)
        // send_notificationメソッドを呼び出します。
        // この際、引数でタグ情報を渡しています。このタグで登録されているデバイスに通知を送信します。
        // 4-2 通知送信リクエスト
        status_code = hub.send_notification("browser")
        
        logging.info(f'Notification sent with status code: {status_code}')
        
    except Exception as e:
        logging.error(f'Error: {str(e)}')

ここではAPIのパスの定義や、Notification Hubsクライアントのメソッド呼び出しなどを行っています。

Notification Hubsクライアント

Notification Hubsクライアントは、Functionsのfunction_app.pyから呼び出されるNotification Hubs周りの処理を担います。

notification_hub.py
shared_code/notification_hub.py
class NotificationHub:
    def __init__(self, connection_string, hub_name):
        self.hub_name = hub_name
        self.endpoint = None
        self.sas_key_name = None
        self.sas_key_value = None
        self.parse_connection_string(connection_string)

    // 2-7 デバイス登録
    def create_registration(self, subscription):
        try:
            installation_id = base64.b64encode(
                subscription['endpoint'].encode('utf-8')
            ).decode('utf-8')
            
            url = f"{self.endpoint}/{self.hub_name}/installations/{installation_id}"
            
            installation = {
                'installationId': installation_id,
                'platform': 'browser',
                'pushChannel': subscription,
                "tags": ['browser'],
            }
            
            headers = {
                'Authorization': self._generate_sas_token(),
                'Content-Type': 'application/json'
            }
            
            response = requests.put(url, json=installation, headers=headers)
            
            if not response.ok:
                logging.error(f"Registration error: {response.text}")
                response.raise_for_status()
            
            return response.headers.get('Location') or installation_id
            
        except Exception as e:
            logging.error(f"Error in create_registration: {str(e)}")
            raise

    // 4-2 通知送信リクエスト
    def send_notification(self, tags):
        try:
            url = f"{self.endpoint}/{self.hub_name}/messages?api-version=2015-04"
            // 認証トークン、フォーマット、通知先のタグを設定
            headers = {
                'Authorization': self._generate_sas_token(),
                'Content-Type': 'application/json',
                "ServiceBusNotification-Format": "browser",
                "ServiceBusNotification-Tags": tags,
            }

            // Push通知時に表示される中身をbody用のpayloadに設定
            payload = {
                "title": f"Web Push通知だよ",
                "body": f"Notification HubsからのWeb Push通知だよ"
            }
            
            response = requests.post(
                url, 
                headers=headers,
                data=json.dumps(payload), 
            )
            
            if not response.ok:
                logging.error(f"Send notification error: {response.text}")
                response.raise_for_status()
            
            return response.status_code
            
        except Exception as e:
            logging.error(f"Error in send_notification: {str(e)}")
            raise

create_registration

Notification Hubsはあらかじめ登録されたデバイスに対して通知を送ります。
create_registrationメソッドはデバイスを登録する処理を担っています。
内部ではNotification HubsのREST API[6]を利用しています。

今回はWeb Push通知なのでplatformにはbrowserを、tagには任意の値を設定できるのですが今回は一律browserを設定しています。
tagはユーザの種別等を登録しておくことで、特定の種別のユーザにまとめてPush通知を送ることができます。
subscriptionにはフロントエンドで取得したブラウザ購読情報を格納しています。

send_notification

登録されたデバイスに対して通知を送るメソッドです。
こちらも内部でdirect-sendというNotification HubsのREST API[7]を利用しています。

ServiceBusNotification-DeviceHandleをヘッダに設定することで、デバイスを指定して通知を送ることもできますが、今回は簡易的に実装することを優先したため、ServiceBusNotification-Tagsであらかじめ決めておいたタグを指定して送信しています。

4.Azure Notification Hubsの構築

Web Push通知の設定

Notification Hubsでプッシュ通知を利用するためには、以下の設定が必要です

  1. Notification HubsでのWeb Push設定
    • Azure portalで該当のNotification Hubを選択
    • 左メニューから「Browser (Web Push)」を選択
    • 「Generate VAPID Keys」をクリック
    • Subject Nameにプッシュサービスからの連絡用メールアドレスを設定

設定前

設定後

ここで設定したVapid Public Keyは先の説明の通り、フロントエンド側でPush Service登録時に利用します。

まとめ

Azure Notification Hubsを使用したWeb Push通知の実装について解説しました。
Notification HubsのWeb Push通知関連情報が少なかったため、実装に苦戦しましたが誰かの役に立てたら嬉しいです。

参考資料

脚注
  1. https://learn.microsoft.com/ja-jp/azure/notification-hubs/notification-hubs-push-notification-overview ↩︎ ↩︎ ↩︎

  2. https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener ↩︎

  3. https://developer.mozilla.org/ja/docs/Web/API/ServiceWorkerRegistration/showNotification ↩︎

  4. https://developer.mozilla.org/ja/docs/Web/API/Notification/requestPermission_static ↩︎

  5. https://developer.mozilla.org/ja/docs/Web/API/PushManager/subscribe ↩︎

  6. https://learn.microsoft.com/ja-jp/rest/api/notificationhubs/create-overwrite-installation ↩︎

  7. https://learn.microsoft.com/ja-jp/rest/api/notificationhubs/direct-send ↩︎

Discussion