mcpField
P
progressive-web-app

progressive-web-app

github.com/sickn33開発49.2k72 回インストール1 日前に更新
リポジトリ

progressive-web-app について

プログレッシブウェブアプリの構築を扱うスキルで、マニフェスト、サービスワーカー、キャッシュ戦略、オフライン対応を説明します。インストール可能なWebアプリや不安定なネットワークへの対応を目指す開発者向けです。

主な機能

  1. 01Web App Manifestの設定
  2. 02サービスワーカーの登録と管理
  3. 03アプリシェルの事前キャッシュ
  4. 04リソース別キャッシュ戦略の選択
  5. 05オフラインページとキャッシュ更新の処理
  6. 06ブラウザーごとのインストール差異の説明

活用シーン

  1. 01Webアプリへのオフラインアクセス追加
  2. 02モバイルサイトのインストール対応
  3. 03静的・動的リソースのキャッシュ設計
  4. 04サービスワーカーのデプロイ問題の調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/sickn33/agentic-awesome-skills --skill progressive-web-app

インストール後、Claude Code で/progressive-web-appを入力して読み込まれたか確認します。

詳細

プログレッシブウェブアプリ(PWA)

概要

プログレッシブウェブアプリは、最新のブラウザー機能を活用して、信頼性の低いネットワークでも高速で信頼性が高く、インストール可能なエクスペリエンスを提供するウェブアプリケーションです。一般的な構成要素は次のとおりです(インストール要件はブラウザーによって異なります)。

  1. HTTPS — 本番環境でサービスワーカーを登録するには必須です(開発時は localhost の場合を除きます)。
  2. ウェブアプリマニフェスト(manifest.json)— アプリをインストール可能にし、デバイスのホーム画面での外観を定義します。
  3. サービスワーカー(sw.js)— ネットワークリクエストを傍受し、キャッシュを管理して、オフライン機能を実現するバックグラウンドスクリプトです。

このスキルを使用する場面

  • ユーザーがウェブアプリをオフラインや信頼性の低いネットワークでも動作させたい場合に使用します。
  • ユーザーがアプリをホーム画面にインストールできるようにする、モバイルファーストのウェブプロジェクトを構築する場合に使用します。
  • ユーザーがキャッシュ戦略、サービスワーカー、ウェブアプリのパフォーマンスや回復性の向上について尋ねた場合に使用します。
  • ユーザーが Workbox、ウェブアプリマニフェスト、バックグラウンド同期、ウェブのプッシュ通知について言及した場合に使用します。
  • ユーザーが「自分のウェブサイトをアプリのようにインストールできますか?」や「サイトをオフラインで動作させるにはどうすればよいですか?」と尋ねた場合は、PWA という言葉を使っていなくても使用します。

成果物チェックリスト

この例の構成では以下のファイルを使用します。これらのファイル名は、すべての PWA に共通する要件ではありません。

  • index.html — マニフェストをリンクし、サービスワーカーを登録します
  • manifest.json — アプリのメタデータとアイコン一式を定義します
  • sw.js — インストール、アクティベート、フェッチの各ハンドラーを備えたサービスワーカーです
  • app.js — サービスワーカーの登録とインストールプロンプトの処理を含む、アプリのメインロジックです
  • offline.html — オフライン時にナビゲーションが失敗した場合に表示されるフォールバックページです(この例の cache.addAll リストでは必要ですが、PWA 標準の要件ではありません)

ステップ 1:ウェブアプリマニフェスト(manifest.json)

インストール時のアプリの外観を定義します。<link rel="manifest"> を使って <head> からリンクする必要があります。

{
  "name": "My Awesome PWA",
  "short_name": "MyPWA",
  "description": "A fast, offline-capable Progressive Web App.",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "orientation": "portrait-primary",
  "background_color": "#ffffff",
  "theme_color": "#0055ff",
  "icons": [
    {
      "src": "/assets/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/assets/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ],
  "screenshots": [
    {
      "src": "/assets/screenshots/desktop.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide"
    }
  ]
}

主なフィールド:

  • display:standalone はブラウザー UI を非表示にします。minimal-ui は最小限のコントロールを表示します。browser は標準のタブ表示です。
  • アイコンに purpose: "maskable" を指定すると、Android でアダプティブアイコンを使えます(セーフゾーンが重要です。コンテンツは中央の 80% に収めてください)。
  • screenshots は任意ですが、デスクトップ版 Chrome の強化されたインストールダイアログには必須です。

ステップ 2:HTML シェル(index.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Awesome PWA</title>

  
  <link rel="manifest" href="/manifest.json">

  
  <meta name="theme-color" content="#0055ff">

  
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="default">
  <meta name="apple-mobile-web-app-title" content="MyPWA">
  <link rel="apple-touch-icon" href="/assets/icons/icon-192x192.png">

  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <div id="app">
    <header><h1>My PWA</h1></header>
    <main id="content">Loading...</main>
    
    <button id="install-btn" hidden>Install App</button>
  </div>
  <script src="/app.js"></script>
</body>
</html>

ステップ 3:サービスワーカーの登録とインストールプロンプト(app.js)

// ─── Service Worker Registration ───────────────────────────────────────────
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js');
      console.log('[App] SW registered, scope:', registration.scope);
    } catch (err) {
      console.error('[App] SW registration failed:', err);
    }
  });
}

// ─── Install Prompt (Add to Home Screen) ───────────────────────────────────
let deferredPrompt;
const installBtn = document.getElementById('install-btn'); // may be null if omitted

// Capture the browser's install prompt — it fires before the browser's own UI
window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault(); // Stop automatic mini-infobar on mobile
  deferredPrompt = e;
  if (installBtn) installBtn.hidden = false; // Show your custom install button
});

if (installBtn) {
  installBtn.addEventListener('click', async () => {
    if (!deferredPrompt) return;
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    console.log('[App] Install outcome:', outcome);
    deferredPrompt = null;
    installBtn.hidden = true;
  });
}
// Fires when the app is installed (via browser or your button)
window.addEventListener('appinstalled', () => {
  console.log('[App] PWA installed successfully');
  if (installBtn) installBtn.hidden = true;
});

ステップ 4:サービスワーカー(sw.js)

キャッシュのバージョン管理(重要 — デプロイ時には必ずインクリメント)
const CACHE_VERSION = 'v1';
const CACHE_PREFIX = 'my-pwa-'; // Unique to this app and deployment scope.
const STATIC_CACHE = `${CACHE_PREFIX}static-${CACHE_VERSION}`;
const DYNAMIC_CACHE = `${CACHE_PREFIX}dynamic-${CACHE_VERSION}`;

// Files to pre-cache during install (the "App Shell")
const APP_SHELL = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
  '/assets/icons/icon-192x192.png',
  '/offline.html', // Fallback page shown when network is unavailable
];

手書きの戦略は、説明のための概略です。古いページと新しいワーカーの互換性問題を避けるため、skipWaiting/clients.claim の前に更新を調整してください。キャッシュのサイズと保持期間に上限を設け、ほかのアプリのキャッシュを保持し、バックグラウンドでの再検証を拒否時の処理とともに event.waitUntil に関連付けてください。

インストール — アプリシェルを事前キャッシュする
self.addEventListener('install', (event) => {
  console.log('[SW] Installing...');
  event.waitUntil(
    caches.open(STATIC_CACHE).then((cache) => {
      console.log('[SW] Pre-caching app shell');
      return cache.addAll(APP_SHELL);
    })
  );
  // Activate immediately without waiting for old SW to die
  self.skipWaiting();
});
アクティベート — 古いキャッシュをクリーンアップする
self.addEventListener('activate', (event) => {
  console.log('[SW] Activating...');
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter((name) => name.startsWith(CACHE_PREFIX) && name !== STATIC_CACHE && name !== DYNAMIC_CACHE)
          .map((name) => {
            console.log('[SW] Deleting old cache:', name);
            return caches.delete(name);
          })
      );
    })
  );
  // Take control of all pages immediately
  self.clients.claim();
});
フェッチ — キャッシュ戦略

リソースの種類ごとに適切な戦略を選択してください。

self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);

  // Only handle GET requests from our own origin
  if (request.method !== 'GET' || url.origin !== location.origin) return;

  // Explicit public resources only. Never cache authenticated/user-specific
  // HTML or API responses by URL; cache contents can survive logout/account changes.
  if (!APP_SHELL.includes(url.pathname)) return;

  // Strategy A: Cache-First (for static assets — fast, tolerates stale)
  if (url.pathname.match(/\.(css|js|png|jpg|svg|woff2)$/)) {
    event.respondWith(cacheFirst(request));
    return;
  }

  // Strategy B: Network-First (for HTML pages — fresh, falls back to cache)
  if (request.headers.get('Accept')?.includes('text/html')) {
    event.respondWith(networkFirst(request));
    return;
  }

  // API caching is intentionally omitted; default to network-only for private data.
});

// ─── Strategy Implementations ──────────────────────────────────────────────

async function cacheFirst(request) {
  const cached = await caches.match(request);
  if (cached) return cached;
  try {
    const response = await fetch(request);
    const cache = await caches.open(STATIC_CACHE);
    if (response.ok && response.type !== "opaque") await cache.put(request, response.clone());
    return response;
  } catch {
    // Nothing useful to fall back to for assets
    return new Response('Asset unavailable offline', { status: 503 });
  }
}

async function networkFirst(request) {
  try {
    const response = await fetch(request);
    const cache = await caches.open(DYNAMIC_CACHE);
    if (response.ok && response.type !== "opaque") await cache.put(request, response.clone());
    return response;
  } catch {
    const cached = await caches.match(request);
    return cached || await caches.match('/offline.html') || new Response("Offline", { status: 503 });
  }
}

async function staleWhileRevalidate(request) {
  const cache = await caches.open(DYNAMIC_CACHE);
  const cached = await cache.match(request);
  const fetchPromise = fetch(request).then(async (response) => {
    if (response.ok && response.type !== "opaque") await cache.put(request, response.clone());
    return response;
  });
  return cached || fetchPromise;
}

エッジケースとプラットフォームに関する注意点

iOS / Safari の注意点
  • Safari はマニフェストとサービスワーカーをサポートしていますが、beforeinstallprompt はサポートしていません。ユーザーは共有 →「ホーム画面に追加」メニューから手動でインストールする必要があります。
  • iOS と適切に連携するには、index.html(上記参照)に示されている apple-mobile-web-app-* メタタグを使用してください。
  • Safari は、非アクティブな状態が約 7 日続くと、サービスワーカーのキャッシュを消去することがあります(インテリジェント・トラッキング防止機能)。
HTTPS の要件
  • サービスワーカーを登録できるのは https:// オリジンのみです。開発時は http://localhost だけが例外です。
  • カスタムホスト名を使ってローカルで HTTPS が必要な場合は、mkcert や ngrok などのツールを使用してください。
デプロイ時のキャッシュ無効化
  • 新しいアセットをデプロイする際は、必ず sw.js の CACHE_VERSION をインクリメントしてください。これにより、アクティベート時に古いキャッシュが消去され、ユーザーは新しいファイルを取得できます。
  • よくある方法は、ビルドツール(例:Vite、Webpack)を使ってバージョンを自動的に挿入することです。
不透明なレスポンス(クロスオリジンリクエスト)
  • クロスオリジンの no-cors レスポンスは不透明になることがあり、その場合、ステータスは 0 で、本文を読み取れません。CORS が有効なレスポンスは、不透明である必要はありません。ステータス 0 を成功と解釈しないでください。
  • クロスオリジンリソースには staleWhileRevalidate を優先するか、この処理を安全に行う Workbox のようなライブラリを使用してください。

Workbox(オプション:本番環境向けの近道)

本番環境のアプリでは、戦略を独自に実装する代わりに、Workbox(Google の PWA ライブラリ)の使用を検討してください。エッジケース、キャッシュの有効期限、バージョン管理を自動的に処理します。

// With Workbox (via CDN for simplicity — use npm + bundler in production)
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js');

const { registerRoute } = workbox.routing;
const { CacheFirst, NetworkFirst, StaleWhileRevalidate } = workbox.strategies;
const { precacheAndRoute } = workbox.precaching;

precacheAndRoute(self.__WB_MANIFEST || []); // Injected by build plugin

registerRoute(({ request }) => request.destination === 'image', new CacheFirst());
registerRoute(({ request }) => request.mode === 'navigate', new NetworkFirst());
registerRoute(({ request }) => request.destination === 'script', new StaleWhileRevalidate());

リリース前のチェックリスト

  • サイトが HTTPS 経由で配信されている
  • manifest.json に name、short_name、start_url、display、icons(192 + 512)が設定されている
  • アイコンに purpose: "any maskable" が設定されている
  • sw.js が DevTools → Application → Service Workers でエラーなく登録される
  • DevTools でネットワークを「Offline」に制限したときに、アプリシェルがキャッシュから読み込まれる
  • オフライン時にナビゲーションが失敗すると、offline.html のフォールバックがキャッシュから配信される
  • 現在のブラウザーにおけるインストール可能性、オフライン対応、アクセシビリティ、パフォーマンスのチェックに合格している。廃止された Lighthouse の PWA カテゴリには依存しないでください
  • iOS Safari(手動インストール手順)と Android Chrome(インストールプロンプト)でテスト済みである

制限事項

  • タスクが上記の説明範囲に明確に合致する場合にのみ、このスキルを使用してください。
  • 出力を、環境固有の検証、テスト、または専門家によるレビューの代わりとして扱わないでください。
  • 必要な入力、権限、安全上の境界、または成功基準が不足している場合は、作業を中断して確認を求めてください。

コメント