【PWA】ServiceWorker を登録して push通知 を実装する!

サムネイルLinux
この記事は約15分で読めます。

PWAの代表機能の1つである push通知 の実装手順について、無駄をすべて取っ払った記事を書いていきます!
push通知関連の記事でよく見かけるのが、それって push通知 をする上で必要なの?という処理が記述されていて、Service Worker という仕組みにあまり詳しくない人が混乱する(というか自分がめちゃくちゃ混乱した)と思ったのでまとめました!

わい
わい

PWA(Progressive Web Application)
= 進歩したwebアプリケーション 的な感じです!
代表機能には以下があります。
1. キャッシュ戦略
2. push通知
3. iPhone等でホーム画面追加時にモバイルアプリっぽくなる設定

前提

  • ↓でアクセスできる環境
    http://localhost

    or http://127.0.0.1(ループバックアドレスならなんでも)

    or https://xxx.xx(SSL環境)
  • 使用言語は HTML・JavaScript・PHP
  • Composer を使用
  • 通知を許可するダイアログが出たら「許可する」をクリック。

必要なファイル(計4個)

  • index.html (ドキュメントルートでアクセスした際に表示されるview)
  • index.js (Service Worker をブラウザに登録する)
  • service-worker.js (登録する Service Worker のイベント処理を記述)
  • send.php (通知を web-push を用いて送る)

ファイル名は何でもいいです。任意の名前をつけてください!
計5つのファイルについて詳細にセクションごとに解説していきます。

わい
わい

以上です!これ以上必要なものはありません!

クロたん
クロたん

記事によっては FCM(Firebase Cloud Messaging) というpushサーバーを提供するサービスとかmanifest.json を設置してるのをよく見かけるけど、不要にゃにょ?

わい
わい

確かにpush通知はPWAの機能だからmanifest.json を置く必要があるかと思いますが、
push通知 を送るという処理においては必須ではないよ!

読み飛ばしてください!

↓Component図 で表すとこうなります。
この記事を読んで関係性をつかめるようになったらGoodです!
いまはわからなくても全く問題なし!

↓シーケンス図にしたらこんな感じ。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <!-- 文字化けしないようにね -->
    <meta charset="UTF-8">
    <!-- Service Worker を登録するjsファイルを読み込む -->
    <script src="index.js"></script>
</head>
<body>
    <!-- このボタンを押すと通知が送信される -->
    <button id="notify">通知を送信する</button>
</body>
</html>

index.js

わい
わい

長いですが、このファイルは非常に大事です。
特に Service Worker の登録 からは慎重に読み進めてください。
理解の補助のためにコメントアウトを細かく入れています。

window.addEventListener('DOMContentLoaded', () => {
    /** 通知を送るボタンがクリックされたら発火 */
    const $notify_btn = document.getElementById('notify');
    $notify_btn.addEventListener('click', async function () {
        // 通知を送るphpファイルにリクエストを投げる
        const result = await fetch('/send.php')
            .then((response) => response.json())
            .then((result) => result)
            .catch(() => console.error('通知の送信に失敗しました。'))
        console.log(result.body);
    });
});
/** Service Worker の登録 */
if ('serviceWorker' in navigator) {
    // Service Workerをファイルを読み込んでブラウザに登録する
    // service-worker.js は必ずドキュメントルート直下に置くことと、scope はめちゃくちゃ重要です!
    navigator.serviceWorker.register('service-worker.js', {
        scope: '/',
    }).then(() => console.log('serviceWorker を登録しました。'))
} else {
    console.log('serviceWorker に対応していません。')
}
/** Service Worker の準備が出来たら発火 */
navigator.serviceWorker.ready.then((registration) => {
    // pushサーバーに当ブラウザの存在を知らせ、認証を受ける
    registration.pushManager.getSubscription().then((subscription) => {
        // ↓後述で生成する公開鍵の文字列を入力する
        let server_public_key = "xxxxxxxxxxxxxxxx";
        // 呪文のような関数を用いて、公開鍵をpushサーバーが要求する認証用キーにフォーマットする
        server_public_key = urlBase64ToUint8Array(server_public_key);
        // pushサーバーに認証リクエストを送る
        registration.pushManager.subscribe({
            userVisibleOnly: true,
            applicationServerKey: server_public_key
        }).then((subscription) => {
            // ブラウザに割り当てられているtokenを含んだpushサーバーのURLが取得できる
            const endpoint = subscription.endpoint;
            console.log('endpoint: ' + endpoint)
            // pushサーバーの公開鍵が取得できる
            const rawKey = subscription.getKey('p256dh');
            const push_public_key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
            console.log('push_public_key: ' + push_public_key)
            // pushサーバーへリクエストを送る際のtokenが取得できる
            const rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
            const push_auth_token = rawAuthSecret ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';
            console.log('push_auth_token: ' + push_auth_token)
        }).catch(e => console.log('pushサーバーへの登録に失敗しました。'));
    })
})
/** サーバー公開鍵をpushサーバーが求める方式にフォーマットする */
function urlBase64ToUint8Array(server_public_key) {
    const padding = '='.repeat((4 - server_public_key.length % 4) % 4);
    const base64 = (server_public_key + 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;
}

キーペアの作成

2つの方法を紹介します!自身にあったやり方でキーペアを生成してください。
めんどくさいという人は2番目のやり方をおすすめします。

1. Linux環境 でサーバーを管理できる場合

openssl を使ってキーペアを作成します。

  • ecparam(楕円曲線暗号)アルゴリズムで秘密鍵を生成
openssl ecparam -genkey -name prime256v1 -out private_key.pem
  • 生成した秘密鍵をベースに公開鍵を生成して、URLセーフな文字列に変換後、改行文字を除去して public_key.txt に保存
openssl ec -in private_key.pem -pubout -outform DER | tail -c 65 | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n' > public_key.txt
わい
わい

public_key.txt に保存された文字列を index.js の xxxxxxx部分 に記載します。

  • その秘密鍵をベースに秘密鍵を生成して、URLセーフな文字列に変換後、改行文字を除去してprivate_key.txt に保存
openssl ec -in private_key.pem -outform DER | tail -c +8 | head -c 32 | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n' > private_key.txt

呪文のような Linuxコマンド ですが、1つ1つのコマンドの意味は興味があれば、または気持ち悪い方は調べてください。

2. webツールを使って生成する

以下のURLから生成して、Public Key に記載された文字列を index.js のxxxxxxx部分 に記載します。

Push Companion
クロ
クロ

クロはこっちでやるかにゃ~。。。

service-worker.js

index.js にて読み込まれていたファイルですね。
こちらのファイルには Service Worker のライフサイクルに応じたイベント群を記述していきますが、push通知 を実装するにあたっては pushイベント しか必要ではありません。

/** pushサーバーから通知が来たときに発火するevent */
self.addEventListener('push', (event) => {
    const msg = event.data.json();
    const options = {
        icon: '/test.png', // ← ※必要ファイルには含めませんでしたが、push通知 に画像を設定できます。
        body: msg.body,
        data: {
            url: msg.url
        },
    };
    // デスクトップ通知を表示する
    self.registration.showNotification(msg.title, options);
});
クロ
クロ

他のイベント書くと混乱するから最低限にしてるのかな~。

composer.json

composer installコマンド 実行時にインストールされるパッケージ設定ファイル

{
    "require": {
        "minishlink/web-push": "^7.0"
    }
}

composer install を実行する

  • Linux環境で composer をインストールしていない方は以下のコマンドでインストールが可能です。
php -r "copy ( 'https://getcomposer.org/installer', 'composer-setup.php' ) ;"
php composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm -rf $(pwd)/composer-setup.php

↑でインストールできたか確認するために、以下を実行する。

composer -v
   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 2.6.2 2023-09-03 14:09:15
  • composer.jsonファイル があるディレクトリにて、 composer install を実行し、
    vendorディレクトリ が作成されたことを確認してください。

send.php

<?php
// autoload.php を読み込んだら、vendor下のクラスをインスタンス化できるようにする
require_once 'vendor/autoload.php';
// json形式でpostされた値を取得して配列化する
$r_post = json_decode(file_get_contents('php://input'), true);
$webPush = new \Minishlink\WebPush\WebPush([
    'VAPID' => [
        'subject' => 'http://localhost/',
        // ↓生成した公開鍵文字列を入れる
        'publicKey' => 'xxxxxxxxxxxxx',
        // ↓生成した秘密鍵文字列を入れる
        'privateKey' => 'xxxxxxxxxxxxx',
        // ↑2つのようにもちろんこういったハードコーディングはよくないが、そこらへんはよしなに。。。
    ]
]);
// push通知認証用のデータ
$subscription = \Minishlink\WebPush\Subscription::create([
    // ↓検証ツール > console に表示された endpoint URL を入力
    'endpoint' => 'https://〇〇〇〇〇〇〇〇',
    // ↓検証ツール > console に表示された push_public_key を入力
    'publicKey' => '△△△△△△△△△△△△',
    // ↓検証ツール > console に表示された push_auth_token を入力
    'authToken' => 'xxxxxxxxxxxxx',
]);
// pushサーバーに通知リクエストをjson形式で送る
$report = $webPush->sendOneNotification(
    $subscription,
    json_encode([
        'title' => 'title',
        'body' => 'body',
        'url' => 'http://localhost/',
    ])
);
$r_response = [
    'status' => 200,
    'body' => '送信' . (($report->isSuccess()) ? '成功' : '失敗')
];
echo json_encode($r_response);

実際に 通知を送る処理だけど、
ここで勘違いしてはいけないのがここでの処理が直接デスクトップ通知をしているわけではないということ!
あくまで、pushサーバーという自分たちの環境でないサーバーに「通知を送ってよ」というリクエストを送っているだけです。

わい
わい

pushサーバー って急になに? って思った方は 読み飛ばしてください!セクション を参照してください。(結局読んだ方がいいんかいってか!)


じゃあ、その「通知を送ってよ」と言われた pushサーバー はどこに通知を送っているのか?というと、コード中に表示されている endpoint となります!

https://fcm.googleapis.com/fcm/send/xxxxxxxxxxxxxxxxxxxx

↑/send/以下の xxxxxx がブラウザに割り当てられるUUID(ユニークID)で、
pushサーバー はそれをもとに通知をブラウザに対して送っている!
※/send/より前はブラウザによって異なる。

じゃあ、そのブラウザに対して送られてきた通知は、どこで処理するかというと、
それこそが service-worker.js で記述した pushイベント です!
pushイベント で json形式 で送られてきた内容( send.php で json_encode() してるから json形式 ) を紐解いてデスクトップ通知に表示させる内容をあてこみ通知を実行する。

わい
わい

以上で push通知 が送れるようになるかと思います!

最後に

今回は PWA の代表機能である push通知 の実装手順を解説しました!
Service Worker というブラウザのバックグラウンドで動く仕組みは、
普段開発を行っていて意識することがほとんどなく理解するのに時間がかかりましたが、
点在している push通知 の実装手順記事を自分なりに必要箇所だけ抜粋して実装しました。

ケン
ケン

すべてに言えることなんだけど、勘所さえ理解してしまえば今までは難しく感じた記事もすいすい理解できるようになりますね。
最初のアレルギー期間も後のことを考えて、じっくりと理解を確実にできるとどんどん知識も増えて楽しくなりますね!それでは、また!

コメント