JSとPHPでWebPushを送信するWebアプリケーションを作ってみる

公開:2021/01/26
更新:2021/01/28
8 min読了の目安(約7600字TECH技術記事

はじめましての方ははじめまして。
またお前かな方はこんにちは。

Nな人(えぬなひと)と申します。

普段はQiitaや自前ブログ「Nな人のWeb示録」で記事を投稿しておりますが、
今回はノリでZennに記事を書いていこうと思います。

では、やっていきましょうか。

目的

service workerなるものを使えば、ブラウザでPush通知が使える!(iPhone以外)
アプリケーションではなく、Webの技術で作れるってすごい!先進的!(iPhone以外)

ということで、ServiceWorkerで個人的に一番やってみたかった、WebPushを実装していこうと思います。
これでアプリを作らなくてもPush通知が送れるようになりますね!(iPhone以外)
とても便利な世界になってきました。(iPhone以外)

お断り

ServiceWorkerには
「manifest.json」や「service-worker.js」
を用意する必要があります。

ですがこやつらはググれば一発で出てくるので、詳しくは書きません。
コードは載せますが、言及しません。
あしからず。

WebPushの流れ

  1. ブラウザでWebPushを利用していいかユーザに尋ねる
  2. 公開鍵と秘密鍵を使ってサーバーキーを作成
  3. push managerという仕組みにサーバーキーを渡してトークンを取得
  4. 取得されたトークンを変換(ようわからん)する
  5. 変換したトークンを使ってWebサーバからPush通知を送信!

以上

WebPushの仕組みを作っていく

事前準備

今回するコードです。

ファイルリストはこんな感じ。

ひとまず、以下のファイルをコピペで準備してください。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
	<title>WebPushテスト</title>

	<meta charset='utf-8'>
	<meta name='viewport' content='width=device-width,initial-scale=1'>

	<!-- アドレスバー等のブラウザのUIを非表示 -->
	<meta name="apple-mobile-web-app-capable" content="yes">
	<!-- default(Safariと同じ) / black(黒) / black-translucent(ステータスバーをコンテンツに含める) -->
	<meta name="apple-mobile-web-app-status-bar-style" content="black">
	<!-- ホーム画面に表示されるアプリ名 -->
	<meta name="apple-mobile-web-app-title" content="WebPusher">
	<!-- ホーム画面に表示されるアプリアイコン -->
	<link rel="apple-touch-icon" href="icon-152x152.png">

	<!-- ウェブアプリマニフェストの読み込み -->
	<link rel="manifest" href="manifest.json">

	<link rel='icon' type='image/png' href='favicon.png'>

    <script defer src='service-worker.js'></script>
    <script src='webpush.js'></script>
</head>

<body>

    <a href="javascript:allowWebPush()">WebPushを許可する</a>

</body>
</html>

manifest.json

{
    "name": "WebPusher",
    "short_name": "WebPusher",
    "theme_color": "#44518d",
    "background_color": "#2e3659",
    "display": "standalone",
    "scope": "/",
    "start_url": "/"
  }

service-worker.js

// プッシュ通知を受け取ったときのイベント
self.addEventListener('push', function (event) {
    const title = 'Push通知テスト';
    const options = {
        body: event.data.text(), // サーバーからのメッセージ
        tag: title, // タイトル
        icon: 'icon-512x512.png', // アイコン
        badge: 'icon-512x512.png' // アイコン
    };

    event.waitUntil(self.registration.showNotification(title, options));
});

// プッシュ通知をクリックしたときのイベント
self.addEventListener('notificationclick', function (event) {
    event.notification.close();

    event.waitUntil(
        // プッシュ通知をクリックしたときにブラウザを起動して表示するURL
        clients.openWindow('https://nnahito.com/')
    );
});


// Service Worker インストール時に実行される
// キャッシュするファイルとかをここで登録する
self.addEventListener('install', (event) => {
    console.log('service worker install ...');
});

公開鍵、秘密鍵の用意

鍵の文字数に制限があるようです。
以下のサイト様で発行してくれます。

https://web-push-codelab.glitch.me

Public KeyPrivate Keyの値をコピーしましょう。

WebPushを許可してもらうコードの作成

以下を丸コピで作成してください。
ただし、27行目のconst appServerKey = 'ここに取得したPublicKeyを書く';の部分は、
先ほど用意した公開鍵(Public Key)を当てはめてください。

また、43行目付近の「// 必要なトークンを変換して取得(これが重要!!!)」は後で使います。
そのことだけ念頭に置いておいてください。

webpush.jsの作成

/**
 * サービスワーカーの登録
 */
self.addEventListener('load', async () => {
    if ('serviceWorker' in navigator) {
        window.sw = await navigator.serviceWorker.register('/service-worker.js', {scope: '/'});
    }
});


/**
 * WebPushを許可する仕組み
 */
async function allowWebPush() {
    if ('Notification' in window) {
        let permission = Notification.permission;

        if (permission === 'denied') {
            alert('Push通知が拒否されているようです。ブラウザの設定からPush通知を有効化してください');
            return false;
        } else if (permission === 'granted') {
            alert('すでにWebPushを許可済みです');
            return false;
        }
    }
    // 取得したPublicKey
    const appServerKey = 'ここに取得したPublicKeyを書く';
    const applicationServerKey = urlB64ToUint8Array(appServerKey);

    // push managerにサーバーキーを渡し、トークンを取得
    let subscription = undefined;
    try {
        subscription = await window.sw.pushManager.subscribe({
            userVisibleOnly: true,
            applicationServerKey
        });
    } catch (e) {
        alert('Push通知機能が拒否されたか、エラーが発生しましたので、Push通知は送信されません。');
        return false;
    }


    // 必要なトークンを変換して取得(これが重要!!!)
    const key = subscription.getKey('p256dh');
    const token = subscription.getKey('auth');
    const request = {
        endpoint: subscription.endpoint,
        userPublicKey: btoa(String.fromCharCode.apply(null, new Uint8Array(key))),
        userAuthToken: btoa(String.fromCharCode.apply(null, new Uint8Array(token)))
    };

    console.log(request);
}



/**
 * トークンを変換するときに使うロジック
 * @param {*} base64String 
 */
function urlB64ToUint8Array (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;
}

これでフロント側の準備は整いました。

ここでPHPのサーバを立てて、一度ブラウザでJSを実行してみます。
今まで作成したコードのファイルが有るディレクトリまでcdし、
以下のコマンドを叩いてください。

php -S localhost:8080

ブラウザで localhost:8080 にアクセスし、「WebPushを許可する」リンクをクリックしてください。
Push通知許可の後、ブラウザコンソールに情報が表示されると思います。

次のステップではこの情報を利用していきます。

Push通知を送信する(PHP)

以下のライブラリを使うと簡単に実装できます。

https://github.com/web-push-libs/web-push-php
composer require minishlink/web-push

SendPush.phpの作成

以下の項目を埋めて、コードを完成させてください。

  • VAPID_SUBJECT
  • PUBLIC_KEY
  • PRIVATE_KEY
  • endpoint
    • webpush.jsの「必要なトークンを変換して取得(これが重要!!!)」で取得されたendpoint
  • publicKey
    • webpush.jsの「必要なトークンを変換して取得(これが重要!!!)」で取得されたuserPublicKey
  • authToken
    • webpush.jsの「必要なトークンを変換して取得(これが重要!!!)」で取得されたuserAuthToken
<?php
require_once 'vendor/autoload.php';

use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;

const VAPID_SUBJECT = 'ここにあなたのWebサイトのURL(http://localhost:5000/ とか https://nnahito.com/ とか)';
const PUBLIC_KEY = '公開鍵( https://web-push-codelab.glitch.me/ で取得したやつ )';
const PRIVATE_KEY = '秘密鍵( https://web-push-codelab.glitch.me/ で取得したやつ )';

// push通知認証用のデータ
$subscription = Subscription::create([
    'endpoint' => '「必要なトークンを変換して取得(これが重要!!!)」で取得されたendpoint',
    'publicKey' => '「必要なトークンを変換して取得(これが重要!!!)」で取得されたuserPublicKey',
    'authToken' => '「必要なトークンを変換して取得(これが重要!!!)」で取得されたuserAuthToken',
]);

// ブラウザに認証させる
$auth = [
    'VAPID' => [
        'subject' => VAPID_SUBJECT,
        'publicKey' => PUBLIC_KEY,
        'privateKey' => PRIVATE_KEY,
    ]
];

$webPush = new WebPush($auth);

$report = $webPush->sendOneNotification(
    $subscription,
    'push通知の本文だよ!'
);

$endpoint = $report->getRequest()->getUri()->__toString();

if ($report->isSuccess()) {
    echo '送信成功!';
} else {
    echo '送信失敗やで';
}

ここまで用意できたらPHPを実行するだけです。

php SendPush.php

これでブラウザに通知が行くと思います。

以上です。

おわりに

いかがでしたでしょうか?
まーコピペでなんとなく作れるかなーくらいの粒度の記事ですね。

実用的にするのであれば、
「必要なトークンを変換して取得(これが重要!!!)」で取得されたデータは、
DBに保存しておき、SendPush.phpをcronで叩く感じにすればいい感じになるかと。

はい、今回は以上になります。
何かあればコメントいただけると嬉しいです。

それでは、また次回。

参考