Skip to content

PWA (Progressive Web App)

PWA(Progressive Web App)は、Webアプリケーションにネイティブアプリのような体験を提供する技術です。Next.jsでは、next-pwaを使用してPWAを実装できます。

従来のWebアプリケーションの課題

Section titled “従来のWebアプリケーションの課題”

問題のあるWebアプリケーション:

// オフライン時に動作しない
export default function App() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(response => response.json())
.then(data => setData(data));
}, []);
// 問題点:
// - オフライン時に動作しない
// - インストールできない
// - プッシュ通知ができない
// - 起動が遅い
}

PWAの解決:

// Service Workerによるオフライン対応
// インストール可能
// プッシュ通知対応
// 高速な起動
// メリット:
// - オフラインでも動作
// - ホーム画面に追加可能
// - プッシュ通知が可能
// - ネイティブアプリのような体験

メリット:

  1. オフライン対応: インターネット接続がなくても動作
  2. インストール可能: ホーム画面に追加可能
  3. プッシュ通知: ユーザーに通知を送信
  4. 高速な起動: キャッシュによる高速な読み込み
Terminal window
npm install next-pwa
next.config.ts
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development',
});
module.exports = withPWA({
// 他の設定
});

public/manifest.json:

{
"name": "My PWA App",
"short_name": "MyApp",
"description": "My Progressive Web App",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#0070f3",
"icons": [
{
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<head>
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#0070f3" />
</head>
<body>{children}</body>
</html>
);
}

public/sw.js:

// Service Workerのカスタムロジック
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('my-cache').then((cache) => {
return cache.addAll([
'/',
'/styles.css',
'/script.js',
]);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
hooks/useOnlineStatus.ts
'use client';
import { useEffect, useState } from 'react';
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
components/OfflineIndicator.tsx
'use client';
import { useOnlineStatus } from '@/hooks/useOnlineStatus';
export default function OfflineIndicator() {
const isOnline = useOnlineStatus();
if (isOnline) return null;
return (
<div role="alert" aria-live="polite">
<p>オフラインです。一部の機能が制限される場合があります。</p>
</div>
);
}
components/InstallPrompt.tsx
'use client';
import { useEffect, useState } from 'react';
export default function InstallPrompt() {
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
const [showPrompt, setShowPrompt] = useState(false);
useEffect(() => {
const handler = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e);
setShowPrompt(true);
};
window.addEventListener('beforeinstallprompt', handler);
return () => {
window.removeEventListener('beforeinstallprompt', handler);
};
}, []);
const handleInstall = async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
console.log('User accepted the install prompt');
}
setDeferredPrompt(null);
setShowPrompt(false);
};
if (!showPrompt) return null;
return (
<div>
<p>このアプリをインストールしますか?</p>
<button onClick={handleInstall}>インストール</button>
<button onClick={() => setShowPrompt(false)}>キャンセル</button>
</div>
);
}
hooks/usePushNotification.ts
'use client';
import { useEffect, useState } from 'react';
export function usePushNotification() {
const [permission, setPermission] = useState<NotificationPermission>('default');
useEffect(() => {
if ('Notification' in window) {
setPermission(Notification.permission);
}
}, []);
const requestPermission = async () => {
if ('Notification' in window) {
const permission = await Notification.requestPermission();
setPermission(permission);
return permission;
}
return 'denied';
};
const showNotification = (title: string, options?: NotificationOptions) => {
if (permission === 'granted') {
new Notification(title, options);
}
};
return {
permission,
requestPermission,
showNotification,
};
}
app/layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
manifest: '/manifest.json',
themeColor: '#0070f3',
appleWebApp: {
capable: true,
statusBarStyle: 'default',
title: 'My PWA App',
},
viewport: {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<head>
<link rel="apple-touch-icon" href="/icon-192x192.png" />
</head>
<body>{children}</body>
</html>
);
}

Next.jsでPWAを実装するポイント:

  • next-pwa: PWA実装のためのライブラリ
  • マニフェスト: アプリのメタデータとアイコン
  • Service Worker: オフライン対応とキャッシング
  • インストールプロンプト: ユーザーにインストールを促す
  • プッシュ通知: ユーザーに通知を送信

PWAは、Webアプリケーションにネイティブアプリのような体験を提供する強力な技術です。適切に実装することで、ユーザー体験を大幅に向上させることができます。