自作の漫画管理アプリ「ほんだな」を、ストアに出さずに Web で配ることにしました。ブラウザで開いて、ホーム画面に追加すると、アプリのように起動する——いわゆる PWA(Progressive Web App) です。
公開してから、おかしなことに気づきました。ホーム画面にアイコンが並び、オフラインでも起動する。そういう「アプリらしい」挙動を、自分でほとんど書いた覚えがないのです。
フレームワークが裏側で勝手に用意してくれていました。動くのはありがたい。でも「何が」「どう」動いているのか分からないまま公開しているのは、正直、少し気持ちが悪い。
そこで後追いで中身を調べました。この記事は、その読み解きの記録です。
なぜストアに出さなかったのか
先に、そもそもの判断から。
このアプリは自分が本屋で使うために作ったものです。棚の前で「これ持ってたっけ?」を解決したい、それだけの道具でした。
その用途に対して、ストア配信は重すぎました。
- 審査と継続的な維持が要る(規約変更のたびに対応が発生する)
- 問い合わせ窓口を持つことになる
- 個人情報の取り扱いが発生する
どれも「動くものを作る」以外の作業です。個人で細く続けるには、ここが軽いほうがいい。URL を開いて、ホーム画面に追加してもらうだけで済むなら、そちらのほうが用途に合っていました。
結果として選んだのが PWA です。技術的なハードルが低いから選んだ、というより、運用を軽くしたかったから選んだというのが実際のところでした。
PWA は「インストールできる Web サイト」
まず大枠から。PWA は、ざっくり言うと インストールできる Web サイトです。
普通の Web サイトとネイティブアプリの、ちょうど中間にいます。
| 普通のWebサイト | PWA | ネイティブアプリ | |
|---|---|---|---|
| 配布 | URLを開くだけ | URLを開く→ホーム画面に追加 | ストア審査・配信 |
| ホーム画面アイコン | なし | あり | あり |
| オフライン起動 | できない | できる | できる |
| 中身 | HTML/CSS/JS | HTML/CSS/JS | OSネイティブ |
中身はあくまで 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 で、今度はタップが効かなくなった話もあります。原因はやはり「知らないと辿り着けない」たぐいのものでした。