未来の自分へ知識貯金

ゆーきのエンジニアブログ

約6分 Webアプリ開発

仕組みを知らないまま PWA を公開して、後から中身を読み解いた

仕組みを知らないまま PWA を公開して、後から中身を読み解いた

自作の漫画管理アプリ「ほんだな」を、ストアに出さずに Web で配ることにしました。ブラウザで開いて、ホーム画面に追加すると、アプリのように起動する——いわゆる PWA(Progressive Web App) です。

公開してから、おかしなことに気づきました。ホーム画面にアイコンが並び、オフラインでも起動する。そういう「アプリらしい」挙動を、自分でほとんど書いた覚えがないのです。

フレームワークが裏側で勝手に用意してくれていました。動くのはありがたい。でも「何が」「どう」動いているのか分からないまま公開しているのは、正直、少し気持ちが悪い。

そこで後追いで中身を調べました。この記事は、その読み解きの記録です。

なぜストアに出さなかったのか

先に、そもそもの判断から。

このアプリは自分が本屋で使うために作ったものです。棚の前で「これ持ってたっけ?」を解決したい、それだけの道具でした。

その用途に対して、ストア配信は重すぎました。

  • 審査と継続的な維持が要る(規約変更のたびに対応が発生する)
  • 問い合わせ窓口を持つことになる
  • 個人情報の取り扱いが発生する

どれも「動くものを作る」以外の作業です。個人で細く続けるには、ここが軽いほうがいい。URL を開いて、ホーム画面に追加してもらうだけで済むなら、そちらのほうが用途に合っていました。

結果として選んだのが PWA です。技術的なハードルが低いから選んだ、というより、運用を軽くしたかったから選んだというのが実際のところでした。

PWA は「インストールできる Web サイト」

まず大枠から。PWA は、ざっくり言うと インストールできる Web サイトです。

普通の Web サイトとネイティブアプリの、ちょうど中間にいます。

普通のWebサイトPWAネイティブアプリ
配布URLを開くだけURLを開く→ホーム画面に追加ストア審査・配信
ホーム画面アイコンなしありあり
オフライン起動できないできるできる
中身HTML/CSS/JSHTML/CSS/JSOSネイティブ

中身はあくまで Web のままです。それなのに「インストールできて」「オフラインでも起動する」。この2つを支えているのが、manifest.json と Service Worker という2つの部品でした。

デスクトップアプリを配ってきた感覚で言うと、exe を渡す代わりに URL を渡しているのに、相手の端末にはアイコンが残る、という状態です。

manifest.json ―「アプリとしての名乗り」

ホーム画面に追加したとき、アイコンは何か、名前は何か、起動したらどう表示するか。これを宣言しているのが manifest.json です。

{
  "name": "ほんだな",
  "short_name": "ほんだな",
  "start_url": ".",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#0175C2",
  "icons": [
    { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }
  ]
}

効いているのは display です。standalone にすると、アドレスバーやタブが消えて、単体のアプリのようなウィンドウで開きます。ここが「Web サイトを開いている」感じを消している正体でした。

面白いのは、この宣言だけで OS 側の扱いが変わることです。中身は何も変わっていないのに、置き場所と見え方が変わる。

Service Worker ―「通信を横取りする常駐スクリプト」

もう一つが Service Worker です。名前が仰々しいのですが、やっていることは単純でした。

ページとは別に常駐していて、ページからの通信要求を横から捕まえるスクリプトです。

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request);
    })
  );
});

fetch イベントで通信を捕まえて、キャッシュに答えがあればそれを返し、無ければ本物を取りに行く。この一段が挟まっているおかげで、ネットワークが無くてもページが表示できます。

「オフラインで動く」の正体は、魔法でもオフライン用の別実装でもなく、通信の手前に挟まった1枚の層でした。

キャッシュに入っていないものは、オフラインには存在しない

ここまで来ると、当然の帰結が見えてきます。

Service Worker がキャッシュしている範囲=オフラインで動く範囲。

裏を返すと、キャッシュに入っていないものは、オフラインでは存在しないのと同じです。1ファイルでも欠けていて、それが起動に必要なら、アプリは立ち上がりません。

これを頭で理解した直後に、実際に踏みました。

実際につまずいた ― オフラインで起動しない

公開してすぐ、オフラインだとホーム画面から起動できないことが分かりました。真っ白なまま止まってしまう。

原因は、フレームワーク(Flutter Web)が画面描画エンジンを外部の CDN から取ってくる設定になっていたことでした。CDN から取りに行くファイルは、当然ながらローカルのキャッシュ対象に入っていません。ネットが無ければ届かず、起動に必要な部品が欠ける。だから真っ白でした。

対処は、ビルド時に CDN を使わずローカルへ同梱する指定を足すだけです。

flutter build web --no-web-resources-cdn

一行です。ただし、この一行が要ることは、キャッシュの範囲という考え方を知らないと出てきません。「オフラインで起動しない」という症状から「描画エンジンが外部から来ている」に辿り着くには、間に「キャッシュに無いものは存在しない」という理解が要ります。

自動生成に乗っていると、この手の設定が既定のまま通り過ぎるのだと実感しました。動いてしまうので、気づく機会がない。

おわりに

自動生成は速いし、実際ありがたいです。ほんだなも、それに乗ったからこそ短期間で公開できました。

ただ今回のように、中身を知らないまま乗っていると、既定値のまま踏んでいる地雷に気づけません。全部を理解する必要はないと思いますが、少なくとも

  • どこまでがキャッシュされているのか(=オフラインで動く範囲)
  • アプリとして何を名乗っているのか(=manifest.json

この2つの境界だけは、押さえておく価値がありました。

そして今回いちばん腑に落ちたのは、PWA が特別な技術の塊ではなかったことです。宣言ファイルが1枚と、通信の手前に挟まる層が1つ。それだけで「インストールできて、オフラインで動く」が成立していました。

ストア審査を通さずにアプリを配りたい人にとって、この軽さは選択肢になると思います。

そもそもこのアプリを Flutter で作ることにした経緯は、こちらに書いています。

同じ Flutter Web で、今度はタップが効かなくなった話もあります。原因はやはり「知らないと辿り着けない」たぐいのものでした。

この記事をシェアする

ゆーき
ゆーき

プログラマー経験10年以上。好きなプログラム言語はC#。使える言語はC++, Python他。好きな開発環境はVisual Studio。

プロフィールを見る →