2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsで高速eコマースアプリを構築する | エピソード9: Progressive Web App(PWA)への変換

Posted at

こんにちは!前回のエピソードでは、next-intlを使って多言語対応を実装し、グローバル市場向けにアプリを拡張しました。今回は、Progressive Web App(PWA)機能を追加して、モバイルでのネイティブアプリのような体験を提供します。next-pwaを使ってオフラインサポートやプッシュ通知を導入し、ユーザーエンゲージメントを向上させます!

このエピソードのゴール

  • next-pwaを使ってPWA機能を追加。
  • マニフェストファイルとサービスワーカーを設定してオフライン対応を実現。
  • プッシュ通知をFirebase Cloud Messaging(FCM)で実装。
  • モバイルデバイスでのPWA動作を確認。

必要なもの

  • 前回のプロジェクト(next-ecommerce)がセットアップ済み。
  • next-pwaパッケージ。
  • Firebaseアカウント(プッシュ通知用)。
  • 基本的なTypeScript、React、Next.jsの知識。

ステップ1: next-pwaのセットアップ

next-pwaは、Next.jsプロジェクトにPWA機能を簡単に追加できるライブラリです。まず、必要なパッケージをインストールします:

npm install next-pwa

next.config.jsを更新してnext-pwaを統合します:

/** @type {import('next').NextConfig} */
const withNextIntl = require('next-intl/plugin')();
const withPWA = require('next-pwa')({
  dest: 'public',
  disable: process.env.NODE_ENV === 'development', // 開発環境ではPWAを無効化
});

const nextConfig = {
  images: {
    domains: ['cdn.shopify.com'],
  },
};

module.exports = withNextIntl(withPWA(nextConfig));

この設定は:

  • PWAのキャッシュファイルを出力先にpublicフォルダを指定。
  • 開発環境でのPWA無効化を有効化。
  • next-intlと共存するように構成。

ステップ2: Webマニフェストの設定

PWAには、アプリのメタデータを定義するmanifest.jsonが必要です。public/manifest.jsonファイルを作成:

{
  "name": "Next.js eCommerce",
  "short_name": "eCommerce",
  "description": "A high-performance eCommerce app built with Next.js",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#1d4ed8",
  "icons": [
    {
      "src": "/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
  • start_url: アプリ起動時のURL。
  • display: standalone: ネイティブアプリのようなフルスクリーン表示。
  • icons: ホーム画面に追加されるアイコン。

publicフォルダにicon-192x192.pngicon-512x512.pngを用意してください(例: ロゴ画像)。デザインツールやオンラインツールで生成可能です。

次に、src/app/[locale]/layout.tsxにマニフェストをリンク:

import { NextIntlClientProvider } from 'next-intl';
import { notFound } from 'next/navigation';
import Navbar from '@/components/Navbar';
import Head from 'next/head';
import '../styles/globals.css';

export function generateStaticParams() {
  return [{ locale: 'en' }, { locale: 'ja' }];
}

export default async function LocaleLayout({
  children,
  params: { locale },
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  let messages;
  try {
    messages = (await import(`../../messages/${locale}.json`)).default;
  } catch (error) {
    notFound();
  }

  const alternateLocales = ['en', 'ja'].filter((l) => l !== locale);

  return (
    <html lang={locale}>
      <Head>
        <link rel="manifest" href="/manifest.json" />
        <meta name="theme-color" content="#1d4ed8" />
        <link rel="alternate" hrefLang={locale} href={`https://yourdomain.com/${locale}`} />
        {alternateLocales.map((altLocale) => (
          <link
            key={altLocale}
            rel="alternate"
            hrefLang={altLocale}
            href={`https://yourdomain.com/${altLocale}`}
          />
        ))}
        <link rel="alternate" hrefLang="x-default" href="https://yourdomain.com/en" />
      </Head>
      <body>
        <NextIntlClientProvider locale={locale} messages={messages}>
          <Navbar />
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

ステップ3: サービスワーカーの確認

next-pwaは、ビルド時にサービスワーカーを自動生成します。public/sw.jspublic/workbox-*.jsが生成されることを確認してください。これにより、ページやアセットがキャッシュされ、オフラインでもアプリが動作します。

デフォルトのキャッシュ戦略をカスタマイズしたい場合、next.config.jsに以下を追加:

const withPWA = require('next-pwa')({
  dest: 'public',
  disable: process.env.NODE_ENV === 'development',
  runtimeCaching: [
    {
      urlPattern: /^https?.*/,
      handler: 'NetworkFirst',
      options: {
        cacheName: 'offlineCache',
        expiration: {
          maxEntries: 200,
        },
      },
    },
  ],
});

この設定は、ネットワーク優先のキャッシュ戦略を採用し、最大200エントリをキャッシュします。


ステップ4: プッシュ通知の実装

プッシュ通知を追加するために、Firebase Cloud Messaging(FCM)を統合します。

  1. Firebaseのセットアップ
    • Firebaseコンソールでプロジェクトを作成。
    • Webアプリを追加し、Firebase SDKの設定情報を取得。
    • public/firebase-messaging-sw.jsファイルを作成し、サービスワーカーでFCMを初期化:
importScripts('https://www.gstatic.com/firebasejs/9.0.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/9.0.0/firebase-messaging-compat.js');

firebase.initializeApp({
  apiKey: 'あなたのAPIキー',
  authDomain: 'あなたのドメイン',
  projectId: 'あなたのプロジェクトID',
  storageBucket: 'あなたのストレージバケット',
  messagingSenderId: 'あなたの送信者ID',
  appId: 'あなたのアプリID',
  measurementId: 'あなたの測定ID',
});

const messaging = firebase.messaging();
  1. クライアントサイドの設定
    src/lib/firebase.tsファイルを作成:
import { initializeApp } from 'firebase/app';
import { getMessaging, getToken, onMessage } from 'firebase/messaging';

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
  measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
};

const app = initializeApp(firebaseConfig);
const messaging = getMessaging(app);

export async function requestNotificationPermission() {
  try {
    const permission = await Notification.requestPermission();
    if (permission === 'granted') {
      const token = await getToken(messaging, {
        vapidKey: process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY,
      });
      console.log('FCMトークン:', token);
      return token;
    }
  } catch (error) {
    console.error('通知許可エラー:', error);
  }
}

export function onMessageListener(callback: (payload: any) => void) {
  onMessage(messaging, callback);
}

.env.localにFirebase設定を追加:

NEXT_PUBLIC_FIREBASE_API_KEY=あなたのAPIキー
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=あなたのドメイン
NEXT_PUBLIC_FIREBASE_PROJECT_ID=あなたのプロジェクトID
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=あなたのストレージバケット
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=あなたの送信者ID
NEXT_PUBLIC_FIREBASE_APP_ID=あなたのアプリID
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=あなたの測定ID
NEXT_PUBLIC_FIREBASE_VAPID_KEY=あなたのVAPIDキー
  1. 通知のトリガー
    src/components/NotificationButton.tsxを作成して通知許可をリクエスト:
import { useState } from 'react';
import { requestNotificationPermission, onMessageListener } from '@/lib/firebase';

export default function NotificationButton() {
  const [subscribed, setSubscribed] = useState(false);

  const handleSubscribe = async () => {
    const token = await requestNotificationPermission();
    if (token) {
      setSubscribed(true);
      onMessageListener((payload) => {
        alert(`通知: ${payload.notification.title}`);
      });
    }
  };

  return (
    <button
      onClick={handleSubscribe}
      disabled={subscribed}
      className="bg-primary text-white px-4 py-2 rounded hover:bg-opacity-90 disabled:opacity-50"
    >
      {subscribed ? '通知を許可済み' : '通知を有効にする'}
    </button>
  );
}

src/components/Navbar.tsxにボタンを追加:

import Link from 'next/link';
import { useCartStore } from '@/lib/cartStore';
import NotificationButton from './NotificationButton';

export default function Navbar() {
  const items = useCartStore((state) => state.items);
  const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);

  return (
    <nav className="bg-primary text-white p-4">
      <div className="container mx-auto flex justify-between items-center">
        <Link href="/" className="text-2xl font-bold">
          Next.js eCommerce
        </Link>
        <div className="flex items-center gap-4">
          <Link href="/cart" className="relative">
            カート
            {itemCount > 0 && (
              <span className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full px-2 text-sm">
                {itemCount}
              </span>
            )}
          </Link>
          <NotificationButton />
        </div>
      </div>
    </nav>
  );
}

ステップ5: 動作確認

  1. Firebaseコンソールでプッシュ通知をテスト送信。
  2. プロジェクトをビルド(npm run build)してローカルで実行(npm run start)。
  3. http://localhost:3000にアクセスし、以下の点を確認:
    • 「通知を有効にする」ボタンをクリックすると、通知許可ダイアログが表示される。
    • アプリをホーム画面に追加できる(Chromeの「インストール」アイコン)。
    • オフラインでページにアクセスし、キャッシュされたコンテンツが表示される。
    • モバイルデバイスでPWAをインストールし、ネイティブアプリのような動作を確認。
  4. Firebaseからテスト通知を送信し、受信されることを確認。

エラーがあれば、Firebase設定やサービスワーカーのログを確認してください。


まとめと次のステップ

このエピソードでは、next-pwaを使ってPWA機能を追加し、オフラインサポートとプッシュ通知を実装しました。これにより、モバイルユーザーにネイティブアプリのような体験を提供できました。

次回のエピソードでは、アプリケーションをVercelにデプロイし、セキュリティ強化と拡張性を確保します。CI/CDの設定やAIを活用した商品レコメンドのアイデアも紹介しますので、引き続きお楽しみに!


この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです!次回のエピソードもお楽しみに!

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?