PWA入門 - ManifestとService Workerでオフライン画面を表示する

中級 | 20分 で読める | 2025.12.02

公式ドキュメント

今回やること

PWA(Progressive Web App)は、Webサイトへインストールやオフライン対応などを段階的に加える考え方です。

この記事ではViteの小さなページに次の2つを追加します。

  • アプリ名やアイコンをブラウザへ伝えるWeb App Manifest
  • オフライン時に固定のoffline.htmlを返すService Worker

プッシュ通知やバックグラウンド同期は扱いません。まず、オンラインで一度開いた後、通信を切っても説明画面が出る状態を作ります。

ブラウザのページ移動をService Workerがまずネットワークへ送り、通信失敗時だけキャッシュのoffline.htmlを返す流れの図

前提条件

  • Node.js 24 LTSとnpmが使える(推奨)
  • HTMLとJavaScriptの基本を知っている
  • 192×192pxと512×512pxの正方形PNG画像を用意できる

この記事で使うVite 8の最低要件は、Node.js 20.19以上または22.12以上です。初学者にはNode.js 24 LTSを推奨します。

Service Workerは安全な接続でのみ利用できます。ただし、開発用のlocalhostは例外として利用できます。

1. プロジェクトを作る

npm create vite@latest pwa-offline -- --template vanilla
cd pwa-offline
npm install
mkdir -p public/icons

Windows PowerShellでは、最後の行の代わりに次を実行します。

New-Item -ItemType Directory -Force public/icons

用意した正方形PNGを、次の名前で保存してください。

public/icons/icon-192.png
public/icons/icon-512.png

アイコンが存在しないと、Manifestを正しく読み込めてもインストール要件を満たせません。

2. Web App Manifestを作る

public/manifest.webmanifestを作ります。JSONにはコメントを書けないため、次の内容をそのまま保存してください。

{
  "id": "/",
  "name": "Offline Practice",
  "short_name": "Offline",
  "description": "Service Workerのオフライン表示を試す学習用PWA",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "background_color": "#f8fafc",
  "theme_color": "#4338ca",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

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="#4338ca" />
    <link rel="manifest" href="/manifest.webmanifest" />
    <link rel="stylesheet" href="/src/style.css" />
    <title>Offline Practice</title>
  </head>
  <body>
    <main>
      <h1>Offline Practice</h1>
      <p>オンラインで表示しています。</p>
    </main>

    <script type="module">
      if ("serviceWorker" in navigator) {
        navigator.serviceWorker
          .register("/sw.js")
          .catch((error) => console.error("Service Worker registration failed", error));
      }
    </script>
  </body>
</html>

src/style.cssは次の内容にします。

:root {
  font-family: system-ui, sans-serif;
  color: #1e293b;
  background: #f8fafc;
}

body {
  margin: 0;
  padding: 2rem;
}

main {
  max-width: 40rem;
  margin: 4rem auto;
  padding: 2rem;
  border-radius: 1rem;
  background: white;
  box-shadow: 0 1rem 3rem rgb(15 23 42 / 0.12);
}

3. 固定のオフライン画面を作る

public/offline.htmlを作ります。外部のCSSや画像へ依存させず、単体で表示できる内容にします。

<!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="#4338ca" />
    <title>オフラインです</title>
    <style>
      body {
        margin: 0;
        padding: 2rem;
        font-family: system-ui, sans-serif;
        color: #1e293b;
        background: #f8fafc;
      }
      main {
        max-width: 40rem;
        margin: 4rem auto;
        padding: 2rem;
        border-radius: 1rem;
        background: white;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>オフラインです</h1>
      <p>接続を確認してから、もう一度読み込んでください。</p>
    </main>
  </body>
</html>

4. Service Workerを作る

public/sw.jsを作ります。

const CACHE_PREFIX = "offline-practice-";
const CACHE_NAME = `${CACHE_PREFIX}v1`;
const STATIC_PATHS = [
  "/offline.html",
  "/src/style.css",
  "/icons/icon-192.png",
  "/icons/icon-512.png",
];
const STATIC_URLS = new Set(
  STATIC_PATHS.map((path) => new URL(path, self.location.origin).href),
);

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_PATHS)),
  );
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches
      .keys()
      .then((names) =>
        Promise.all(
          names
            .filter(
              (name) => name.startsWith(CACHE_PREFIX) && name !== CACHE_NAME,
            )
            .map((name) => caches.delete(name)),
        ),
      )
      .then(() => self.clients.claim()),
  );
});

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

  if (request.method !== "GET" || url.origin !== self.location.origin) {
    return;
  }

  if (request.mode === "navigate") {
    event.respondWith(
      fetch(request).catch(() => caches.match("/offline.html")),
    );
    return;
  }

  if (STATIC_URLS.has(request.url)) {
    event.respondWith(
      caches.match(request).then((cached) => cached ?? fetch(request)),
    );
  }
});

この例では、ページ移動のレスポンスをキャッシュへ保存しません。ログイン後の画面や利用者ごとに異なるHTMLを誤って他人へ見せる危険を避け、通信失敗時だけ固定のoffline.htmlを返します。

静的キャッシュも、STATIC_PATHSへ明示した同一オリジンのファイルだけです。API、POSTリクエスト、外部サイトのファイルには介入しません。

成功確認

npm run dev
  1. 表示されたlocalhostのURLをChromeまたはEdgeで開く
  2. DevToolsの「Application」→「Manifest」で名前とアイコンを確認する
  3. 「Application」→「Service Workers」で登録を確認する
  4. Service Workerがページを制御するよう、一度ページを再読み込みする
  5. DevToolsの「Network」で「Offline」を選ぶ
  6. ページを再読み込みし、「オフラインです」と表示されることを確認する
  7. 「Online」へ戻し、再読み込みする

これで、Manifestの読込と固定オフライン画面の表示を確認できました。

よくあるつまずき

プロジェクト作成直後にNode.jsのバージョンエラーが出る

Vite 8はNode.js 20.19以上または22.12以上を必要とします。Node.js 18や20.18以前では、npm create vite@latestまたはnpm run devが失敗します。

node --version

古い場合はNode.js 24 LTSへ更新し、ターミナルを開き直してからプロジェクトを作り直してください。

Service Workerが登録されない

  • file:///で直接HTMLを開かず、npm run devのURLを使う
  • sw.jspublic直下へ保存したか確認する
  • DevToolsのConsoleに404や構文エラーがないか確認する

オフライン画面が表示されない

最初の表示時点では、Service Workerがまだページを制御していない場合があります。オンラインのまま一度再読み込みしてから、Offlineへ切り替えてください。

cache.addAllでインストールに失敗する

STATIC_PATHSのうち1つでも404になると、事前キャッシュ全体が失敗します。特に2つのPNGアイコンのファイル名と配置を確認してください。

修正内容が反映されない

sw.jsを変更したらCACHE_NAMEv1v2へ変更します。確認中の古いService Workerは、DevToolsの「Application」から登録解除できます。

練習

  1. offline.htmlの文章と色を変更する
  2. public/help.htmlを作ってSTATIC_PATHSへ追加する
  3. CACHE_NAMEv2へ変更し、古いキャッシュが削除されることを確認する

キャッシュ対象を追加した時は、存在しないパスや個人情報を含むページを入れていないか確認してください。

次のステップ

実サービスでは、ページごとの公開範囲、更新方法、保存容量を決めてからキャッシュ戦略を設計します。認証済みページやAPIレスポンスを無条件にキャッシュしないことが重要です。

次は、ブラウザのインストール条件と、更新を利用者へ知らせる方法を公式資料で確認しましょう。

参考リソース

← 一覧に戻る
PR
PR
PR
PR