Progressive Web Apps (PWA) への対応まとめ
このブログサイトで対応した作業をベースに、PWA の基本的な内容やその周辺状況について書きました。
どうも ogaclejapan です。
気づけば、最後のブログ投稿から丸5年以上経ってしまいましたが、、、 今年からブログを少しずつ再開していきたいと思っています。
さて、 Youtube が先日 Progressive Web Apps (以下、PWA) に対応したようですね。
YouTube available to install as Progressive Web App (PWA) - 9to5Google
このブログサイトも無駄に PWA に対応しています。
実際に対応した作業をベースに、基本的な導入までの流れや関連する技術について調べたことを記事にまとめました。
PWA とは
PWA は、ネイティブアプリのような体験をウェブブラウザ上で提供するウェブアプリケーションを指します。
間違いを恐れずに意味を解釈すると、「Progressive Web Apps=段階的に進化するブラウザの機能拡張(プログレッシブ・エンハンスメント)により、 最新機能が使える実行環境では、よりリッチな体験を提供するウェブアプリケーション」となります。
PWA 導入事例
実際、どのような体験の向上につながり、サイトへどのような変化をもたらすのか?気になりますよね。 PWA Stats には、 PWA に対応したサイトの指標がどのように変化したのか、コミュニティドリブンで報告が寄せられています。
いくつか挙げてみると、
- コンバージョンが増加した ≒ 収益増
- ページ読み込み時間の短縮 ≒ 訪問者増加
- 直帰率が減少 ≒ 滞在時間の増加
結果として、ページ表示速度の改善が大きな役割を果たし、他の指標に好影響をもたらしているようです。 やはり、スピードは正義なのか。
Why does speed matter? - web.dev
基本的にポジティブな報告しか書かれていませんが、これは PWA の特長だと考えます。 PWA に対応したサイトは、最新機能が使える環境でのみ、サイトの体験向上につながる付加価値を提供します。 そのため、良くなることしかないのも納得がいきます。
他にも、日本経済新聞社さんの事例紹介が参考になります。
PWA で実現できること
ブラウザごとに対応状況は異なりますが、次のことが可能になります。
- デバイスへのインストール
- 高速化とオフライン起動
- デバイスのプラットフォーム固有機能
- アプリストア配布
デバイスへのインストール
PWA の動作要件を満たし、かつインストールをサポートするデバイス上のブラウザでサイトへアクセスすると、 ブラウザが次のようなインストール導線を自動的に表示してくれます。
どのような条件でインストール導線が出るかはブラウザの実装に依存します。 例えば、Chrome ブラウザだと内部のエンゲージメントスコアが閾値を超えないとページ内に表示されないようです。
What does it take to be installable? #Install criteria - web.dev
ちなみにページ内のインストール導線は、デフォルト挙動を抑止することで差し替えることが可能です。 このブログでは、デフォルトのインストール導線だと訴求が強すぎるので、独自の位置に導線を置いています。
高速化とオフライン起動
PWA の動作要件に含まれる Service Worker (のちほど説明します)に関わってくる部分ですが、 ページのリソースに対して適切なキャッシュ戦略をとることで、表示の高速化やオフライン環境で閲覧できます。
デバイスのプラットフォーム固有機能
こちらも同様に、PWA の動作要件に含まれる Service Worker に関わってくる部分ですが、 ネイティブアプリと同様にバックグラウンドの状態でもプッシュ通知を受け取ることができます。
他にも、PWA に関連した「Project Fugu 🐡」と称するプロジェクトでは、 Chrome 開発チームを中心にウェブとネイティブアプリのギャップを埋めるためのAPIを実験的に提供しています。 開発進行中の API は、 Fugu API Tracker で公開状況を確認できます。
ちなみに、このブログだと「Web Share API」をコンテンツのシェア機能ボタンで活用しています。 プラットフォーム固有の共有機能が使えるのはとても助かります。
アプリストア配布
PWA の直接的な文脈から若干外れますが、アプリストアからネイティブアプリのように配布することが可能になってきています。 この辺は、まだ試せてないので、別の記事として詳細に掘り下げていければと思います。
さらに、昨年末の Chrome Dev Summit 2020 では、 Google Play Payments に対応すると発表がありました。 どこまでネイティブアプリと同様のストア課金形態が使えるのか気になります。
What’s new for web apps in Play - YouTube
PWA で実現できないこと
実現できることの冒頭で、「ブラウザごとに対応状況は異なり〜」と一文添えましたが、 PWA に対応しても提供できる機能は、ブラウザの実行環境に依存します。
最新 Chrome ブラウザを OS 別の対応状況で表にしてみます。
PWA | インストール | オフライン起動 | プッシュ通知 |
---|---|---|---|
Android | ○ | ○ | ○ |
iOS | × | × *1 | × |
macOS | ○ | ○ | ○ |
Windows | ○ | ○ | ○ |
*1: ブラウザキャッシュは効くが、現実的な利用は厳しいためバツとします。
iOS とそれ以外のプラットフォームで対応状況が分かれています。 調べたところ、iOS 版の Chrome アプリは WKWebView を内部のレンダリングエンジンとして利用しているようです。 つまり、 WKWebView が PWA の要件を満たす API を提供してくれない限り、この状況が改善する見込みはありません。
Do all browsers on iOS use WKWebview or UIWebVIew? - Stack Overflow
このように、同一ブラウザであっても、実現できないことを理解しておきましょう。
PWA の動作要件
サイトを PWA に対応させるには、次の3つの項目を満たす必要があります。
- Web App Manifest
- Service Worker
- HTTPS
Web App Manifest
Web App Manifest は、アプリケーションに関する情報を提供します。 アプリの名前やデバイスに合わせた高解像度アイコン、起動時の URL などを JSON 形式のファイルに記述します。
{
"name": "Blueslash.",
"short_name": "Blueslash",
"description": "A personal blog for software developers.",
"lang": "ja-JP",
"start_url": "/",
..
"icons":[
{
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/icon-maskable-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/icon.svg",
"sizes": "512x512",
"type": "image/svg+xml"
}
]
}
アイコンは、定義したサイズや形式の組み合わせからデバイスの解像度に応じて自動的に選択されます。 このブログでは、次の4つのパターンのアイコンを用意しておきました。
- PNG: 192px x 192px
- PNG: 512px x 512px
- PNG: 512px x 512px (Maskable icons)
- SVG: - (ベクター形式のため実質サイズ制限なし)
まず、1と2のパターンは、 Chrome ブラウザが必要としています。
Add a web app manifest #Key manifest properties - web.dev
For Chrome, you must provide at least a 192x192 pixel icon, and a 512x512 pixel icon. If only those two icon sizes are provided, Chrome will automatically scale the icons to fit the device. If you’d prefer to scale your own icons, and adjust them for pixel-perfection, provide icons in increments of 48dp.
3のパターンは、 Android Oreo から導入された Adaptive Icons に対応します。 Firefox と Chrome がこの形式をサポートしていて、Maskable icons と呼びます。
4のパターンは、 W3C の仕様上だと SVG 形式の指定が可能そうなので、おもに超高解像度向けの選択肢として念のため用意しています。
Service Worker
Service Worker は、ネットワークリクエストの制御やバックグラウンド処理などウェブサイトに強力な機能を提供します。 詳細な内容は、次の記事が参考になります。
Service Worker の紹介 | Web Fundamentals | Google Developers
Service Worker は、ブラウザ内のページプロセスから独立していて、 Worker コンテキスト上で動作する Web Worker の一種になります。 特長としては、ページのライフサイクルに依存せず、バックグラウンドで処理を継続することが可能です。
この図は、ページと Worker の関係を示す画像として、 Chrome Dev Summit 2020 で公開された動画「 PWA patterns for window and service worker communication - Youtube 」から引用しています。
PWA では、次の3つの用途で Service Worker が提供する機能を利用します。
- 高速化(キャッシング)
- オフライン体験(キャッシング、ルーティング)
- バックグラウンド処理(プッシュ通知、データ同期、etc)
HTTPS
Service Worker は、 HTTPS を介して提供されるページでのみ動作するように制限されています。 そのため、少なくとも TLS 証明書が用意されたサーバーや HTTPS に対応したホスティングサービスからコンテンツを配信する必要があります。
PWA の導入作業
前項の PWA の動作要件で挙げた3つ項目について、実際にこのブログで実施した作業をベースに導入までの流れを説明していきます。 うまくいかないときは、 Chrome DevTools の Application タブを開くと、 PWA の機能に関する確認に役立ちます。
Debug Progressive Web Apps | Chrome DevTools | Google Developers
Web App Manifest の設置
Web App Manifest ファイルの準備ができたら、サイトの公開ページと同様に外部からアクセス可能な場所へ設置します。
そして、ブラウザへアプリケーションの情報を伝えるために、各ページの head
要素にファイルへの URL を link
要素で定義します。
<html>
<head>
..
<link rel="manifest" href="/manifest.webmanifest">
..
</head>
なお、 公式のファイル拡張子は .webmanifest
になります。
Web App Manifest の仕様に .json
の拡張子を使う場合の注意点が書かれています。
NOTE: manifest.webmanifest or manifest.json? The official file extension for the manifest is .webmanifest. Some web servers recognize this extension and transfer the file using the standardized application manifest MIME type (application/manifest+json). Developers can also choose a different extension (e.g. .json) or none at all (e.g. /api/GetManifest), but are encouraged to transfer the manifest using the application/manifest+json MIME type, although any JSON MIME type is ok.
Service Worker の実装
Service Worker は、ページ内の JavaScript から最初に登録することで有効になります。 つまり、 Service Worker の実装には、次の2つの JavaScript が必要になります。
- Serivce Worker として動かすコード
- ページ内から Service Worker を登録するコード
このブログでは、 Google製の Workbox ライブラリを Service Worker の基本的な実装に活用しました。 Workbox ライブラリには、キャッシュ戦略やルーティングの簡素化など Service Worker の実装に役立つモジュールが多数含まれています。
Welcome! Workbox is a set of libraries that can power a production-ready service worker for your Progressive Web App.
ちなみに、ライブラリに頼らず自前で実装したい方は、「 Using Service Workers - Web APIs | MDN 」のデモコードが参考になります。 キャッシュ優先のみの実装ですが、プリキャッシュからルーティングまで一通りの流れを分かりやすく解説されています。
Serivce Worker として動かすコード
次の3つの処理に分けて Workbox ライブラリで実装します。 この3つで使わないモジュールは、何か特定の機能向けと思っておけば、導入へのハードルが下がります。
- プリキャッシュ
- ルーティング
- フォールバック
まず、プリキャッシュのサンプルコードです。
import { precacheAndRoute } from 'workbox-precaching';
precacheAndRoute(self.__WB_MANIFEST);
self.__WB_MANIFEST
の部分は、
Workbox が提供する CLI や Webpack プラグインなどを活用すると自動的に書き換えてくれます。
つまり、プリキャッシュするリソースをビルド時に外部から動的に解決することができます。
Using bundlers with Workbox | Google Developers
どのようなリソースをプリキャッシュしておくべきか? 一般的には、キャッシュ優先で即応答したい、不変なコンテンツやハッシュ付きのリソースが該当します。 さらに、オフラインで動作させるなら、フォールバック用途のページや画像なども役立ちます。
次に、ルーティングのサンプルコードです。
import { registerRoute } from 'workbox-routing';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import { NetworkFirst } from 'workbox-strategies';
registerRoute(
({ request }) => request.mode === 'navigate',
new NetworkFirst({
cacheName: 'pages',
plugins: [
new CacheableResponsePlugin({
statuses: [200],
})
]
})
);
ルーティングでは、対象のリクエストをどのようなキャッシュ戦略で制御するかを指定できます。 Workbox には、キャッシュ戦略の専用モジュールが用意されており、コンテンツに特性に合わせて使い分けることができます。
Workbox Strategies | Google Developers
最後に、フォールバックのサンプルコードです。
import { matchPrecache } from 'workbox-precaching';
import { setCatchHandler } from 'workbox-routing';
setCatchHandler(({event}) => {
switch (event.request.destination) {
case 'document':
return matchPrecache('/offline.html');
case 'image':
return matchPrecache('/no-image.png');
default:
return Response.error();
}
});
Workbox は、 workbox-routing
モジュール内部にデフォルトハンドラをもっています。
このデフォルトハンドラには、ルーティングでエラーが発生したときにフォールバックするデフォルトエラーハンドラを設定できます。
setCatchHandler
で登録したハンドラは、内部のデフォルトエラーハンドラに設定されます。
フォールバックは、オフライン体験で特に重要になります。 ブラウザは、オフライン時にキャッシュが存在せず、ページを表示できないとき、何もできません。 そのため、適切なフォールバックが、優れた体験を提供するうえで推奨されています。
Create an offline fallback page - web.dev
ページ内から Service Worker を登録するコード
Service Worker API をサポートしているブラウザでは、 navigator
オブジェクトに serviceWorker
プロパティが 定義されています。
このプロパティが存在する場合のみ、register メソッドを呼び出して Service Worker を登録します。
// e.g. 最小限の Service Worker 登録処理
window.addEventListener('load', function(event) {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
});
Service Worker の配置場所は、有効となるスコープ範囲に関わってきます。 例えば、ルート直下ならルート配下全体に対して Service Worker が有効になります。
登録時のオプションパラメーターでスコープの変更はできますが、スコープの昇格にはセキュリティ上の理由から別途 Service-Worker-Allowed レスポンスヘッダーが Service Worker のファイルに対して必要になります。 特別な理由がなければ、原則ルート直下に配置しましょう。
なお、 こちらも Workbox ライブラリを活用すると、次のように書き直せます。
import { Workbox } from 'workbox-window';
if ('serviceWorker' in navigator) {
const wb = new Workbox('/sw.js');
wb.register();
}
workbox-window
モジュールには、
Window - Service Worker 間のメッセージ通信やライフサイクルの制御に役立つ機能が含まれています。
しかし、単純な登録のみの利用だと、オーバースペックな実装になりますので、用途を決めて使うべきか判断しましょう。
HTTPSへの対応
このブログは、 Firebase Hosting を利用しました。 カスタムドメインから配信する際、HTTP→HTTPSへの転送やHSTSレスポンスヘッダーを自動で設定してくれます。
$ curl -D - -s -o /dev/null http://blog.ogaclejapan.dev
HTTP/1.1 301 Moved Permanently
..
Location: https://blog.ogaclejapan.dev/
$ curl -D - -s -o /dev/null https://blog.ogaclejapan.dev
HTTP/2 200
..
strict-transport-security: max-age=31556926
利用するホスティングサービスによって設定方法は異なりますが、 サイトが PWA として利用されるように一度確認しておくとよさそうです。
余談ですが、他に個人サイトなどでたまに見かけるのは、wwwサブドメインとネイキッドドメインの制御漏れです。 必ずどちらか一方へ転送しておきましょう。
$ curl -D - -s -o /dev/null https://ogaclejapan.dev
HTTP/2 301
..
location: https://www.ogaclejapan.dev/
これらは、同一オリジンではありません。
Same-origin policy - Wikipedia
まとめ
より優れた PWA の体験を提供するためのチェックリストが web.dev で公開されています。 PWA をフル活用していくなら、他に何を対応すべきなのか確認しておくとよさそうです。
また、Push 通知の実装には触れませんでしたが、 Web Push の仕組みや実装は、Google Developers の記事「How Push Works」を参考にしてみてください。 配信と購読の管理面が大変なので、最終的に Firebase Cloud Messaging へ辿り着きます。
PWA の所感としては、キャッシュ戦略でページをより柔軟に制御できることと、 ストアの障壁なく、使ってみてよかったら入れてくれ!というシームレスな体験をウェブ上で完結できるのはとても魅力的でした。
…あれ、Androidにも Instant Apps あったな? 🤔
現場からは以上です。