【Next.js】Service Worker を使用してプッシュ通知の実装

Service Worker とは

Webアプリケーションのバックグラウンドで動作するスクリプト

  • ブラウザのメインスレッドとは別に動作
  • オフライン動作が可能
  • プッシュ通知を受信可能
  • バックグラウンドで同期処理が可能

プッシュ通知の流れ

Service Workerのライフサイクル 1. 登録 (Registration) navigator.serviceWorker.register() 2. インストール ‘install’ イベント 3. アクティブ化 ‘activate’ イベント 実行中 通知の受信と表示 登録処理の実行場所: • アプリケーション起動時(layout.tsxやapp.tsx) • ユーザーアクション時(ボタンクリックなど) イベントの実行場所: • service-worker.js 内でイベントをリスン • プッシュ通知やキャッシュの処理を実装

制作手順

Next.jsプロジェクトの作成

PS C:\test> npx create-next-app@latest 1231_push-notification --typescript
Need to install the following packages:
create-next-app@15.1.3
Ok to proceed? (y) y

√ Would you like to use ESLint? ... No / Yes
√ Would you like to use Tailwind CSS? ... No / Yes
√ Would you like your code inside a `src/` directory? ... No / Yes
√ Would you like to use App Router? (recommended) ... No / Yes
√ Would you like to use Turbopack for `next dev`? ... No / Yes
√ Would you like to customize the import alias (`@/*` by default)? ... No / Yes
√ What import alias would you like configured? ... @/*
Creating a new Next.js app in C:\ida_test\1231_push-notification.

web-pushのインストール

# 必要なパッケージのインストール
npm install web-push  # プッシュ通知送信用
npm install @types/web-push --save-dev  # TypeScript型定義

web-pushは、プッシュ通知を送信するためのNode.jsライブラリです。

  1. VAPIDキーの生成
    • プッシュ通知の認証に必要な鍵ペアを生成
    • サーバーとブラウザ間の安全な通信を確保
  2. 通知の送信
    • サーバーからブラウザへプッシュ通知を送信
    • 暗号化された通信を管理

VAPID キーの生成

これはプッシュ通知の認証に必要なキーペアです

# web-pushコマンドラインツールをグローバルにインストール
npm install -g web-push

# VAPIDキーを生成
web-push generate-vapid-keys

.env.local(生成されたキーを記載)

# VAPIDキー(web-push generate-vapid-keysで生成したものを設定)
NEXT_PUBLIC_VAPID_PUBLIC_KEY=あなたのパブリックキー
VAPID_PRIVATE_KEY=あなたのプライベートキー
VAPID_SUBJECT=mailto:your-email@example.com  # 連絡先メールアドレス

構成ファイル群

プロジェクトのフォルダ構造
push-notification-project/
├── public/
│   ├── service-worker.js     # プッシュ通知を受け取るためのService Worker
│   ├── manifest.json         # PWAのマニフェストファイル
│   └── icon.png             # 通知用のアイコン画像(192x192px推奨)
│
├── src/
│   ├── components/
│   │   └── NotificationTest.tsx  # 通知テスト用のコンポーネント
│   │
│   ├── pages/
│   │   ├── index.tsx         # メインページ
│   │   ├── api/             # APIルート
│   │   │   ├── subscribe.ts  # 購読情報を保存するAPI
│   │   │   └── push.ts       # 通知を送信するAPI
│   │   │
│   │   └── _app.tsx         # アプリケーションのルート
│   │
│   └── lib/
│       └── pushUtils.ts      # 通知関連のユーティリティ関数
│
├── .env.local               # 環境変数(VAPIDキーなど)
├── package.json
└── tsconfig.json

package.json

{

...

  "scripts": {
    "dev": "next dev --experimental-https",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "15.1.3",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "web-push": "^3.6.7"
  },
  ...

}

service-worker.js

// public/service-worker.js
self.addEventListener("install", (event) => {
  console.log("Service Worker: インストール完了");
});

self.addEventListener("activate", (event) => {
  console.log("Service Worker: アクティブ化完了");
});

self.addEventListener("push", (event) => {
  const options = {
    body: "プッシュ通知テスト",
    icon: "/icon.png",
    requireInteraction: true,
    renotify: true,
    tag: "push-test",
    data: {
      url: self.location.origin,
    },
  };

  event.waitUntil(self.registration.showNotification("プッシュ通知", options));
});

self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  event.waitUntil(clients.openWindow("/"));
});

manifest.json

{
    /* アプリケーションの基本情報 */
    "name": "プッシュ通知デモ",           // アプリの正式名称
    "short_name": "通知デモ",            // ホーム画面での短い名称
    "description": "プッシュ通知のデモアプリケーション",
    
    /* 表示設定 */
    "start_url": "/",                    // 起動時のURL
    "display": "standalone",             // 表示モード
    "background_color": "#ffffff",       // 背景色
    "theme_color": "#000000",           // テーマ色
    
    /* アイコン設定 */
    "icons": [
        {
            "src": "/icon.png",
            "sizes": "192x192",
            "type": "image/png"
        }
    ],
    
    /* プッシュ通知の許可設定 */
    "permissions": [
        "notifications"
    ]
}

src\app\components\ClientWrapper.tsx

'use client';

import dynamic from 'next/dynamic';

const NotificationTest = dynamic(
  () => import('@/app/components/NotificationTest'),
  { ssr: false }
);

export default function ClientWrapper() {
  return <NotificationTest />;
}

src\components\NotificationTest.tsx

// src/components/NotificationTest.tsx
'use client';

import { useState, useEffect } from 'react';

export default function NotificationTest() {
  const [permission, setPermission] = useState<NotificationPermission>('default');
  const [registration, setRegistration] = useState<ServiceWorkerRegistration | null>(null);

  useEffect(() => {
    setPermission(Notification.permission);
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/service-worker.js')
        .then(reg => {
          console.log('ServiceWorker登録成功:', reg);
          setRegistration(reg);
        })
        .catch(err => console.error('ServiceWorker登録エラー:', err));
    }
  }, []);

  const sendNotification = async () => {
    try {
      // 通知許可確認
      const currentPermission = await Notification.requestPermission();
      setPermission(currentPermission);
      if (currentPermission !== 'granted') {
        alert('通知が許可されていません');
        return;
      }

      console.log('===通知テスト開始===');

      // Service Worker通知
      if ('serviceWorker' in navigator && registration) {
        console.log('SW通知試行');
        await registration.showNotification('SW通知', {
          body: 'SW経由テスト',
          requireInteraction: true,
          renotify: true,
          tag: 'test-sw'
        });
        console.log('SW通知完了');
      }

      // ブラウザ通知
      console.log('ブラウザ通知試行');
      const notification = new Notification('ブラウザ通知', {
        body: 'ブラウザ経由テスト',
        requireInteraction: true,
        renotify: true,
        tag: 'test-browser'
      });
      notification.onclick = () => {
        window.focus();
        notification.close();
      };
      console.log('ブラウザ通知完了');

    } catch (error) {
      console.error('通知エラー:', error);
    }
  };

  return (
    <div className="p-4">
      <button 
        onClick={sendNotification}
        className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"
      >
        テスト通知を送信
      </button>
      <p className="mt-2">通知許可状態: {permission}</p>
    </div>
  );
}

src\components\NotificationTest.tsx

// src/components/NotificationTest.tsx
'use client';

import { useState, useEffect } from 'react';

export default function NotificationTest() {
  const [permission, setPermission] = useState<NotificationPermission>('default');
  const [registration, setRegistration] = useState<ServiceWorkerRegistration | null>(null);

  useEffect(() => {
    setPermission(Notification.permission);
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/service-worker.js')
        .then(reg => {
          console.log('ServiceWorker登録成功:', reg);
          setRegistration(reg);
        })
        .catch(err => console.error('ServiceWorker登録エラー:', err));
    }
  }, []);

  const sendNotification = async () => {
    try {
      // 通知許可確認
      const currentPermission = await Notification.requestPermission();
      setPermission(currentPermission);
      if (currentPermission !== 'granted') {
        alert('通知が許可されていません');
        return;
      }

      console.log('===通知テスト開始===');

      // Service Worker通知
      if ('serviceWorker' in navigator && registration) {
        console.log('SW通知試行');
        await registration.showNotification('SW通知', {
          body: 'SW経由テスト',
          requireInteraction: true,
          renotify: true,
          tag: 'test-sw'
        });
        console.log('SW通知完了');
      }

      // ブラウザ通知
      console.log('ブラウザ通知試行');
      const notification = new Notification('ブラウザ通知', {
        body: 'ブラウザ経由テスト',
        requireInteraction: true,
        renotify: true,
        tag: 'test-browser'
      });
      notification.onclick = () => {
        window.focus();
        notification.close();
      };
      console.log('ブラウザ通知完了');

    } catch (error) {
      console.error('通知エラー:', error);
    }
  };

  return (
    <div className="p-4">
      <button 
        onClick={sendNotification}
        className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"
      >
        テスト通知を送信
      </button>
      <p className="mt-2">通知許可状態: {permission}</p>
    </div>
  );
}

プッシュ通知許可

  1. Windowsシステム通知設定
  • Windowsキー + I(設定を開く)
  • システム > 通知とアクション
  • 「Chrome」を探して許可に設定
  1. Chromeブラウザ通知
  • Chrome右上の⋮ > 設定
  • プライバシーとセキュリティ > サイトの設定 > 通知
  • または chrome://settings/content/notifications を直接入力
  1. localhostサイト許可
  • localhost:3000にアクセス
  • アドレスバー左の🔒をクリック
  • 通知設定を「許可」に変更
  • または上記Chrome通知設定画面で手動で追加
  • 通知送信ボタンを含むコンポーネント
  • Service Workerの初期化コードがある場所
  • pages/api/またはapp/api/内の通知関連のAPIハンドラ