PWA対応

pluginPwa()はSSGで生成したサイトにWeb App ManifestとService Workerを追加します。HTMLやアセットをブラウザに保存し、オフラインでもページを開けるようにします。ストアへの公開は不要です。

Table of Contents

プラグインを追加する

PWAを使うプロジェクトにvite-plugin-pwaを追加します。ministaではoptional peer dependencyとして扱うため、PWAを使わないプロジェクトではインストール不要です。

npm install -D vite-plugin-pwa

VitePWA()を直接追加する代わりに、ministaのpluginPwa()を使います。SSGで生成した各HTMLへのタグ挿入と、最終成果物のキャッシュ対象への追加を連携します。

vite.config.ts
import { defineConfig, pluginSsg, pluginPwa } from "minista"

export default defineConfig({
  plugins: [
    pluginSsg(),
    pluginPwa({
      registerType: "autoUpdate",
      manifest: {
        name: "My Site",
        short_name: "My Site",
        theme_color: "#182433",
        icons: [
          { src: "icon-192.png", sizes: "192x192", type: "image/png" },
          { src: "icon-512.png", sizes: "512x512", type: "image/png" },
        ],
      },
    }),
  ],
})

public/icon-192.pngとpublic/icon-512.pngに、指定したサイズのアイコンを用意してください。Manifestにサイト名やアイコンを設定すると、対応ブラウザからホーム画面への追加やインストールを行えます。

自動再読み込みを設定する

registerType: "autoUpdate"とクライアント側のvirtual:pwa-registerを組み合わせます。新しいService Workerの検出・インストール後に、開いているページを自動で再読み込みします。

src/assets/pwa.ts
/// <reference types="vite/client" />
/// <reference types="vite-plugin-pwa/vanillajs" />

import { registerSW } from "virtual:pwa-register"

if (import.meta.env.PROD && "serviceWorker" in navigator) {
  registerSW({ immediate: true })
}

このモジュールを共通レイアウトのscriptから読み込みます。ページやレイアウトのサーバー側コードから直接importせず、ブラウザで実行するエントリーとして配置します。

src/layouts/index.tsx
import type { LayoutProps } from "minista/types"

export default function Layout({ children }: LayoutProps) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>My Site</title>
        <script type="module" src="/src/assets/pwa.ts" />
      </head>
      <body>{children}</body>
    </html>
  )
}

更新時には再読み込みが発生するため、フォーム入力などの状態が失われる可能性があります。利用者に更新タイミングを選んでもらう場合は、registerType: "prompt"とonNeedRefreshを使う更新UIを用意してください。詳細は自動再読み込みと更新通知を参照してください。

保存されるファイルと更新

既定では、ビルドしたHTML・JavaScript・CSS・JSON・画像・フォントをService Workerのprecacheに含めます。保存が完了すれば、まだ訪れていないページも対象に含まれている限りオフラインで開けます。保存先はブラウザのキャッシュであり、ZIPを端末へダウンロードする機能ではありません。

キャッシュする拡張子を変更する場合はworkbox.globPatternsを指定します。指定した配列は既定値を置き換えるため、HTMLと必要なアセットを含めてください。外部APIや外部ドメインのファイルは、このglobだけでは保存されません。

pluginPwa({
  workbox: {
    globPatterns: ["**/*.{html,js,css,json,svg,png}"],
  },
})

オンラインで新版が検出されると、precache対象全体について変更されたファイルを取得します。ページ遷移のたびにすべてのファイルを取得するわけではありません。開いたページの再読み込みは、前述のクライアント設定が担当します。

更新の検出時期はブラウザや実行状態に依存します。アプリの再起動1回や特定のページ遷移だけで、必ず最新版になるとは保証できません。この例では定期的な更新チェックを追加していません。保存容量やブラウザによるキャッシュ削除にも影響されるため、オフラインでの永続保存は保証されません。

ビルドと端末での確認

通常の開発サーバーではService Workerを有効にしません。npm run buildで生成した成果物を、npm run previewまたはHTTPSの公開サイトで確認してください。localhostではHTTPSなしでも動作確認できます。

  1. オンラインでサイトを開き、キャッシュの保存完了を待ちます。registerSWのonOfflineReadyで完了を表示できます。
  2. オフラインにして、別ページへの遷移と再読み込みを確認します。
  3. オンラインに戻して内容を変更・再デプロイし、新版の検出後に表示が更新されることを確認します。

iOSではSafariの共有メニューから「ホーム画面に追加」し、追加したアプリでも確認します。再起動だけを更新完了の判定にせず、表示内容とオフライン時のページを確認してください。

リポジトリのplayground/pwaには、HomeとAboutを使った自動再読み込みの例があります。オプションと制約はpluginPwaを参照してください。

次は納品コードの整形で成果物を読みやすくします。