超シンプルなPWAを自分で作る

作るもの: 「Hello PWA」アプリ

目次

シンプルなWebページ

シンプルなWebページ:

フォルダ構成:


ステップ1: 以下のフォルダを作成

ステップ1: index.html
新しいファイル index.html を作成して、以下をコピペ:

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="theme-color" content="#2196F3">
    <title>My First PWA</title>
    <link rel="stylesheet" href="style.css">
    <link rel="manifest" href="manifest.json">
</head>
<body>
    <div class="container">
        <h1>🚀 Hello PWA!</h1>
        <p class="subtitle">Progressive Web App を作ってみた</p>
        
        <div class="card">
            <p>現在時刻:</p>
            <p class="time" id="time">--:--:--</p>
        </div>
        
        <div class="card">
            <p>クリックカウント:</p>
            <p class="count" id="count">0</p>
            <button id="countBtn">カウント +1</button>
        </div>
        
        <div class="status">
            <p id="status">オンライン 🟢</p>
        </div>
    </div>
    
    <script src="app.js"></script>
</body>
</html>

ステップ2: style.css
新しいファイル style.css を作成:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 20px;
}

.container {
    background: white;
    border-radius: 20px;
    padding: 40px;
    max-width: 400px;
    width: 100%;
    box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}

h1 {
    color: #333;
    text-align: center;
    margin-bottom: 10px;
    font-size: 2em;
}

.subtitle {
    text-align: center;
    color: #666;
    margin-bottom: 30px;
    font-size: 0.9em;
}

.card {
    background: #f5f5f5;
    border-radius: 10px;
    padding: 20px;
    margin-bottom: 20px;
    text-align: center;
}

.time, .count {
    font-size: 2em;
    font-weight: bold;
    color: #2196F3;
    margin: 10px 0;
}

button {
    background: #2196F3;
    color: white;
    border: none;
    padding: 12px 24px;
    border-radius: 25px;
    font-size: 1em;
    cursor: pointer;
    transition: all 0.3s;
}

button:hover {
    background: #1976D2;
    transform: scale(1.05);
}

button:active {
    transform: scale(0.95);
}

.status {
    text-align: center;
    margin-top: 20px;
    padding: 10px;
    background: #e8f5e9;
    border-radius: 10px;
}

.status.offline {
    background: #ffebee;
}

#status {
    color: #333;
    font-weight: bold;
}

ステップ3: app.js
新しいファイル app.js を作成:

// Service Worker の登録
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/service-worker.js')
            .then(registration => {
                console.log('✅ Service Worker 登録成功:', registration);
            })
            .catch(error => {
                console.log('❌ Service Worker 登録失敗:', error);
            });
    });
}

// 時計の表示
function updateTime() {
    const now = new Date();
    const timeString = now.toLocaleTimeString('ja-JP');
    document.getElementById('time').textContent = timeString;
}

// 1秒ごとに更新
setInterval(updateTime, 1000);
updateTime(); // 初回表示

// カウンター
let count = localStorage.getItem('count') || 0;
document.getElementById('count').textContent = count;

document.getElementById('countBtn').addEventListener('click', () => {
    count++;
    document.getElementById('count').textContent = count;
    localStorage.setItem('count', count);
});

// オンライン/オフライン状態の監視
function updateOnlineStatus() {
    const statusElement = document.getElementById('status');
    const statusContainer = statusElement.parentElement;
    
    if (navigator.onLine) {
        statusElement.textContent = 'オンライン 🟢';
        statusContainer.classList.remove('offline');
    } else {
        statusElement.textContent = 'オフライン 🔴';
        statusContainer.classList.add('offline');
    }
}

window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
updateOnlineStatus(); // 初回チェック

ステップ4: service-worker.js 重要!
新しいファイル service-worker.js を作成:

// キャッシュ名(バージョン管理用)
const CACHE_NAME = 'my-first-pwa-v1';

// キャッシュするファイル一覧
const urlsToCache = [
    '/',
    '/index.html',
    '/style.css',
    '/app.js'
];

// インストール時: キャッシュにファイルを保存
self.addEventListener('install', (event) => {
    console.log('🔧 Service Worker: インストール中...');
    
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then((cache) => {
                console.log('📦 キャッシュを開きました');
                return cache.addAll(urlsToCache);
            })
            .then(() => {
                console.log('✅ すべてのファイルをキャッシュしました');
            })
    );
});

// アクティベート時: 古いキャッシュを削除
self.addEventListener('activate', (event) => {
    console.log('🚀 Service Worker: アクティベート中...');
    
    event.waitUntil(
        caches.keys().then((cacheNames) => {
            return Promise.all(
                cacheNames.map((cacheName) => {
                    if (cacheName !== CACHE_NAME) {
                        console.log('🗑️ 古いキャッシュを削除:', cacheName);
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
});

// フェッチ時: キャッシュから返す(オフライン対応)
self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request)
            .then((response) => {
                // キャッシュにあればそれを返す
                if (response) {
                    console.log('📦 キャッシュから返します:', event.request.url);
                    return response;
                }
                
                // なければネットワークから取得
                console.log('🌐 ネットワークから取得:', event.request.url);
                return fetch(event.request);
            })
    );
});

ステップ5: manifest.json
新しいファイル manifest.json を作成:

{
    "name": "My First PWA",
    "short_name": "First PWA",
    "description": "私が作った最初のPWA",
    "start_url": "/",
    "display": "standalone",
    "background_color": "#ffffff",
    "theme_color": "#2196F3",
    "icons": [
        {
            "src": "/icons/icon-192.png",
            "sizes": "192x192",
            "type": "image/png",
            "purpose": "any maskable"
        },
        {
            "src": "/icons/icon-512.png",
            "sizes": "512x512",
            "type": "image/png",
            "purpose": "any maskable"
        }
    ]
}

ステップ6: アイコン画像


`icons` フォルダに2つの画像を用意:

icon-192.png` と `icon-512.png` として保存

🌐 **ローカルサーバーで実行**



PWAは **HTTPS または localhost** でしか動かない!

方法1: VS Code の Live Server(簡単)

  1. Live Server 拡張機能をインストール
  2. index.html を右クリック
  3. 「Open with Live Server」

→ ブラウザで自動的に開く!

🧪 オフライン動作の確認

  1. ページを開く
  2. リロード(Service Worker がインストールされる)
  3. DevTools > Application > Service Workers
    → “Offline” にチェック
  4. ページをリロード

結果:
✅ オフラインでも表示される!
✅ 時計も動く!
✅ カウンターも動く!
✅ 「オフライン 🔴」と表示される

→ PWA 成功!

📱 PWAとしてインストール

Chrome のアドレスバー:
→ インストールアイコン(+マーク)が表示される
→ クリックしてインストール

結果:
✅ デスクトップにアイコン
✅ 独立したウィンドウで開く
✅ アプリっぽい!

目次