最小PWAのサンプル

## TL;DR

`https://`から始まるURLにて`html`を描画できるURLの`head`タグ内で次のタグを記述し、`manifest.json`ファイルへのパスを指定すればいけます。  
以下は`DocumentRoot`直下に`manifest.json`が置いてある場合の例です。
ただし、オフライン時も動作するようにするには別途JavaScriptでサービスワーカーを実装し、静的ファイルのキャッシュ機能をもたせる必要があります。

```html
<link rel="manifest" href="manifest.json">
```

GitHub でサンプルになるソースコードを公開しています。

https://github.com/hidao80/mini-pwa

上記ソースが実際動作しているのはGitHub Pagesで公開しているこちらをどうぞ。

https://hidao80.github.io/mini-pwa/

:::note warn
デスクトップ版FirefoxにPWAをインストールするにはアドオンが必要です。[Firefox 86で非対応になった模様](https://bugzilla.mozilla.org/show_bug.cgi?id=1682593)です。Android版Firefoxではアドオンなしでもインストールできました。
ちなみにFirefoxはPWAの事を**SSB**（Site-specific browser、サイト固有のブラウザ）と呼んでいるようです。
:::

## 雑な説明

**PWA**（Progressive web apps, プログレッシブウェブアプリ）とは、ざっくり言うとAndroidやiOS、iPadOSやWindows、macOS、Linuxデスクトップなどにアプリのようにインストール・アンインストールでき、独立したウインドウとして利用できるブラウザベースのWebアプリのことです。

PWA側の[サービスワーカー](https://developer.mozilla.org/ja/docs/Web/API/ServiceWorker)によって通信がなくてもキャッシュされたhtmlやJavaScriptだけでもユーザ端末内で起動できます。
また、`fetch` APIなどで通信することによりクライアントはユーザの端末側、データなどはサーバ側に分割できたり、アップデートの自動配信などにも対応できます。

おまけとしてPWAのインストール元ブラウザにインストールされている拡張機能も有効になるので、[Stylus](https://chromewebstore.google.com/detail/stylus/clngdbkpkpeebahjckkjfobafhncgmne?hl=ja)や[tampermonkey](https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=ja)で悪さがし放題です。😎

正確な説明は毎度おなじみMDNを参照ください。

[プログレッシブウェブアプリ (PWA) | MDN](https://developer.mozilla.org/ja/docs/Web/Progressive_web_apps)

## 実装手順

ここからは実装方法を紹介します。

1. `manifest.json`の作成
1. `index.html`から`link`タグで`manifest.json`ファイルをマニフェストファイルとして読み込む
1. 必要に応じてオフライン動作できるようにするためのサービスワーカー用JavaScriptファイルを作る
1. インストール機能を実装する。ブラウザの機能でインストールしてもらうなら不要

一般的には`node.js`のライブラリを使うようですが、私は[`VanillaJS`](http://vanilla-js.com/)使いなのでここでは割愛します。

## manifest.jsonの概要

jsonにはコメントを入れることができませんので、実際に利用するときはコメント部分をすべて削除してください。

```jsonc:manifest.json
{
    // 正式名。必須。インストール済みアプリリストやタイトルバーに表示される
    "name": "Minimum PWA",

    // OSの一般的なアプリとして振る舞う`fullscreen`や
    // ブラウザのタブとして振る舞う`browser`などがある
    "display": "standalone",

    // WebアプリのID。配布元のドメインを変更したりして
    // URLが変わっても、同一アプリであることを示すことができる
    // アプリ名をそのまま使っても構わない
    "id": "03a25b2d-b372-4e5e-8daa-eb5012b66ac5", 

    // このアプリがアクセスできる範囲の一番外側。
    // これより上のパスにアクセスするときはアプリを離れブラウザで開く
    "scope": "/",

    // アプリ起動時のURL。ブラウザの実装によって無視されることがある
    "start_url": "/index.html",
    
    // アプリアイコン。必須。ホーム画面にあるときなど、表示サイズに応じて変更できる
    // ここでは一つだけ指定しているが、サイズやファイルタイプによって複数指定できる
    "icons": [
        {
            // 画像ファイルパス
            "src": "icon256x256.png",

            // ファイルタイプのヒント
            "type": "image/png",

            // 画像サイズ
            "sizes": "256x256"
        }
    ]
}
```

## PWAの実装例

かなりアレな実装ですが、最小のサンプルということで勘弁してください。

ここではPWAのインストールにJavaScriptを使わずブラウザの機能でインストールしてもらう前提にしています。

![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76242/6180b075-4988-c758-a93d-b48a74ceb85d.png)&emsp;⇐アドレスバーのPWAインストールボタン（Edgeの場合）

また、PWAをインストールさせるページを作るには`https://`で始まるセキュアなURLが必要です。

### トップページ（HTML）

PWAを開いたときの最初のページになるHTMLファイルです。サービスワーカーにキャッシュさせることにより、オフライン起動が可能になります。

```html:index.html
<!DOCTYPE html>
<head>
    <title>Minimum PWA</title>
    <link rel="shortcut icon" type="image/png" href="icon256x256.png"><!-- favicon -->
    <link rel="manifest" href="manifest.json">
</head>
<body>
    <!-- アプリとしての機能 -->
    <button type="button" onclick="alert('UUID: ' + crypto.randomUUID())">Generate</button>

    <script>
        // サービスワーカーの登録。黙って登録しているのでお行儀が悪い
        window.addEventListener("load", (e) => {
            if ("serviceWorker" in navigator) {
                navigator.serviceWorker.register("./sw.js");
            }
        });
    </script>
</body>
</html>
```

### サービスワーカー（JavaScript）

PWAの動的に動作する部分です。
バージョン番号はちょっとがんはれば`fetch` APIで`manifest.json`から取ってくることも可能ですが、今回は省略してシンプルに徹しています。

```js:sw.js
const APP_NAME = "mini-pwa";
const VERSION = "202404281828JST"; // 普通に"v1"とかでもよい

// このサービスワーカーのキャッシュデータのキーとなる文字列
// バージョン番号を入れることで、ソースがアップデートされたときに
// キャッシュファイルの更新をする狙い
const CACHE_NAME = APP_NAME + "_" + VERSION;

// 静的ファイルの相対パスを指定し、インストール時にダウンロードしてキャッシュしておくファイル群
// ローカルではルートにアクセスしたときのためにパス"/"を解決してくれないらしいので登録しておく
// jsファイルやcssファイル、画像ファイルなどがあればそれもリストアップしておくと良い
const assets = [
  "/",
  "/index.html",
  "/icon256x256.png",
];

/**
 * サービスワーカーのインストールイベント発生時の処理
 */
self.addEventListener("install", (e) => {
  e.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      // 上記配列で指定したファイル群をダウンロードしてローカルにキャッシュしておく
      // これによりオフラインでも起動できるようになる
      return cache.addAll(assets);
    })
  );
});

/** 
 * サービスワーカーからサーバにアクセスするときの処理
 */
self.addEventListener("fetch", (e) => {
  e.respondWith(
    caches.match(e.request).then((response) => {
      // キャッシュされているときは通信せずにローカルのファイルを参照する
      return response ? response : fetch(e.request);
    })
  );
});

/**
 * サービスワーカーが起動するときの処理
 */
self.addEventListener("activate", (e) => {
  e.waitUntil(
    caches.keys().then((keyList) => {
      return Promise.all(
        keyList.map((key) => {
          // このファイル先頭で定義されているファイル名の配列になく、
          // キャッシュされていたらキャッシュ（＝ローカル）から削除しておく
          // バージョンアップ時にゴミが残らないようにする工夫
          if (key !== CACHE_NAME) {
            return caches.delete(key);
          }
        })
      );
    })
  );
});
```

### アプリアイコン

![icon256x256.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76242/1c8343da-4772-5473-1d81-8ccfe63d72c9.png)
ダウンロードしたらファイル名を「`icon256x256.png`」に変更してご利用ください。

**動作画面**  
ZorinOS（Linux）+ Edgeでインストールして実行するとこんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76242/28e6410b-2c3b-d9f5-19d6-eef9bcd2b154.png)

## 最後に

Microsoft StoreのアプリはPWAが登録でき、Windowsアプリのように振る舞いつつストアの機能で課金の収納代行もしてくれるようです。PWAを公開しているURLが必要なので、公開されているPWAをどう収益化するかは悩みどころですが。ログイン機能でもつけますかね。
これで`Electron`や`Stripe`ともおサラバですね！（せやろか