Vite (React, TypeScript)プロジェクトをPWA化

yarn create vite my-app --template react-ts で作ったプロジェクトをPWA化するための簡単なメモです。ここでのPWA化とはインストール可能とすることを目的としています。

PWA

方法

Vite Plugin PWAを使いました。

iOSSafari限定で「ホーム画面に追加」からおこないます。

詳細

パッケージのインストール

yarn add vite-plugin-pwa -D

public/robots.txtの追加

robots.txt は、検索エンジンのクロール(サイトを巡回し、ページの内容を収集・保存する処理)を制御するためのファイルだそうです。例えば私のGatsbyのブログのそれは https://blog.pitang1965.com/robots.txt のようにサイト名に/robots.txtを追加したURLで確認できます。

今回は次としました。

User-agent: *
Allow: /

アイコンの作成

以下のサイズの画像を用意して、public フォルダに配置します。

  • 180×180 // ファイル名の例:apple-touch-icon.png
  • 192×192 // ファイル名の例:icon-192×192.png
  • 256×256 // ファイル名の例:icon-256×256.png
  • 384×384 // ファイル名の例:icon-384×384.png
  • 512×512 // ファイル名の例:icon-512×512.png
  • favicon.ico

これをおこなうのに PWA Manifest Generator が便利でした。

具体的には、

  • 既に作成済のロゴのsvgファイルからInkscapeにて512×512サイズのlogo.pngを作り、それをPWA Manifest Generatorに次のように指定しました。manifest.zipが生成されるのでそれを伸張して使います。
PWA Manifest Generator
  • apple-touch-icon.png は作成済のロゴのsvgファイルからInkscapeにて180×180サイズで作ります。
  • 作成済のロゴのsvgファイルをfabicon.svgとします。
  • pngから Convertio にて favicon.icoに変換。

vite.config.tsの編集

以下のmanifestの部分は、PWA Manifest Generatorが生成した manifiest.webmanifest にある内容を元にしていますが、purpose: 'any maskable' のところを追加していることに注意します。

import { VitePWA } from 'vite-plugin-pwa';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      includeAssets: [
        'offline.html',
        'favicon.svg',
        'favicon.ico',
        'robots.txt',
        'apple-touch-icon.png',
      ],
      manifest: {
        theme_color: '#ffffff',
        background_color: '#4a90e2',
        display: 'standalone',
        scope: '/',
        start_url: '/',
        short_name: '\u305f\u3066\u30c4\u30a4\u30fc\u30c8',
        description:
          '\u7e26\u66f8\u304d\u30c4\u30a4\u30fc\u30c8\u3092\u3059\u308b\u305f\u3081\u306e\u30c4\u30fc\u30eb\u3002',
        name: '\u305f\u3066\u30c4\u30a4\u30fc\u30c8',
        icons: [
          {
            src: '/icon-192x192.png',
            sizes: '192x192',
            type: 'image/png',
          },
          {
            src: '/icon-256x256.png',
            sizes: '256x256',
            type: 'image/png',
          },
          {
            src: '/icon-384x384.png',
            sizes: '384x384',
            type: 'image/png',
          },
          {
            src: '/icon-512x512.png',
            sizes: '512x512',
            type: 'image/png',
          },
          {
            src: '/icon-512x512.png',
            sizes: '512x512',
            type: 'image/png',
            purpose: 'any maskable',
          },
        ],
      },
    }),
  ],
});

index.htmlの編集

OGPの設定は省略しますが、次のようにしてみました。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>たてツイート</title>
    <meta
      name="description"
      content="縦書きでツイートするためにテキストを変換します。"
    />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <link
      rel="alternate icon"
      href="/favicon.ico"
      type="image/png"
      sizes="16x16"
    />
    <link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180" />
    <link rel="mask-icon" href="/favicon.svg" color="#FFFFFF" />
    <meta name="theme-color" content="#ffffff" />


:


  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
    <!-- PWA -->
    <script>
      if ('serviceWorker' in navigator) {
        window.addEventListener('load', () => {
          navigator.serviceWorker
            .register('./serviceWorker.js')
            .then((reg) => console.log('サービスワーカーの登録成功', reg.scope))
            .catch((err) => console.log('サービスワーカーの登録失敗', err));
        });
      }
    </script>
  </body>
</html>

public/serviceWorke.js の追加

index.htmlscript タグで参照している serviceWorker.js を次の内容で public フォルダに配置しました。オフラインのときに表示する offline.html を指定しています。また、このファイルは vite.config.ts でも指定しています。

const CACHE_NAME = 'version-1';
const urlsToCache = ['index.html', 'offline.html'];

// Install SW
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log('Opened cache');
      return cache.addAll(urlsToCache);
    })
  );
});

// Listen for requests
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then(() => {
      return fetch(event.request).catch(() => caches.match('offline.html'));
    })
  );
});

// Activate the SW
self.addEventListener('activate', (event) => {
  const casheWhitelist = [];
  casheWhitelist.push(CACHE_NAME);

  event.waitUntil(
    caches.keys().then((casheNames) =>
      Promise.all(
        casheNames.map((casheName) => {
          if (!casheWhitelist.includes(casheName)) {
            return cashes.delete(casheName);
          }
        })
      )
    )
  );
});

public/offline.htmlの追加

オフラインでキャッシュにない場合は次のような画面を表示するためのHTMLを追加してみました。

オフライン時のフォールバック
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>たてツイート</title>
    <style type="text/css">
      body {
        display: flex;
        flex-direction: column;
        background: #00aff9;
        height: 100vh;
        margin: 0;
        color: white;
      }
      h1 {
        margin: 0.8em 3rem;
        font: 4em Roboto;
      }
      p {
        margin: 0.2em 3rem;
        font: 2em Roboto;
      }
    </style>
  </head>
  <body>
    <div>
      <h1>おっと!!!</h1>
      <p>このページはオフラインでは表示できません。</p>
      <p><a href="../index.html">ホームページに戻る </a></p>
    </div>
  </body>
</html>