HOME
PWA (Progressive Web Apps) とは、Google や Mozilla が推進するプロジェクトで、Web ページでネイティブなモバイルアプリのような UX を提供する仕組みです。PWA に必要な Web App Manifest や Service Worker について解説します。
PWA (Progressive Web Apps) とは、Google や Mozilla が推進するプロジェクトで、Web ページでネイティブなモバイルアプリのような UX を提供する仕組みです。
今までのモバイルアプリは、App Store や Google Play からダウンロードしてインストールを行う方法が一般的でした。しかし、モバイルアプリの開発にはコストがかかり、ダウンロードするデータは大きく、アップデートを行う必要があります。一方、PWA はモバイルアプリと比較して以下の特徴を持ちます。
これらの特徴について詳しく見ていきましょう。
PWA は、正しい HTML / XHTML で作成し、CSS3 をサポートしているブラウザに対して、リッチなデザインや機能を提供する "プログレッシブエンハンスメント (Progressive Enhancement)" を基本理念としています。そのため、モダンブラウザであれば環境に依存することなく、コンテンツを利用してもらえます。
モバイルアプリは iOS / Android のいずれかの環境に依存する問題点を抱えていますが、PWA ではブラウザを利用することで環境依存の問題点を回避しています。ただし、基本的には HTML5 / CSS3 をサポートしているモダンブラウザをターゲットにしており、レガシーブラウザは PWA の対象ではありません。
PWA が利用可能なブラウザかどうかは "Service Worker" という機能をブラウザがサポートしているかで決まります。Service Worker とは、PWA の中枢的な機能であり、実態は Web ページの裏側で処理を行うイベント駆動の JavaScript 環境です。現在ではプッシュ通知や、バックグラウンド同期などの機能が提供されており、将来的には定期的な同期や、ジオフェンシング (位置情報を利用したサービス) などが提供される予定です。
ブラウザ | 対応状況 | 説明 |
---|---|---|
Chrome (デスクトップ版) | ◯ | 対応可能。 |
Chrome (Android 版) | ◯ | 対応可能。 |
Chrome (iOS版) | ◯ | 対応可能。 |
Firefox (デスクトップ版) | ◯ | 対応可能。 |
Firefox (Android 版) | ◯ | 対応可能。 |
Firefox (iOS版) | ◯ | 対応可能。 |
Safari (デスクトップ版) | ◯ | 対応可能。 |
Safari (iOS版) | ◯ | 対応可能。 |
Internet Explorer | ✕ | Internet Explorer では対応不可。 |
Edge | ◯ | 対応可能。 |
Opera | ◯ | 対応可能。 |
各ブラウザにおける Service Worker の最新状況は、以下のサイトで確認できます。
また、Service Worker のどの機能が利用可能であるかはブラウザのバージョンによって異なるため注意が必要です。ブラウザのバージョン別による機能の実装状況は、以下のページで確認できます。
他にも対象のブラウザで Service Worker が利用できるかは、以下のスクリプトを実行することでも調べることができます。対応している場合は true
、対応していない場合は false
を返します。
<html>
<body>
<script>
alert('serviceWorker' in navigator);
</script>
</body>
</html>
PC やモバイルやタブレットは、それぞれ解像度が異なります。これらのマルチデバイスに個別対応することは非常に難しいため、レスポンシブ Web デザインを用いることで様々なデバイスに適合できます。
Google では、レスポンシブ Web デザインを "ユーザのデバイスに関係なく、画面サイズに応じて表示を変えることができる" ものと定義しています。
PWA ではレスポンシブな表示を行うことで、どのようなデバイスに対しても同じような操作性、情報のアクセシビリティを可能としています。モバイルのネイティブアプリでは、解像度が異なるデバイスに最適化させるためには個別の対応が必要となりますが、レスポンシブはそのような開発コストを削減できます。
Service Worker の機能により、オフライン、またはネットワークの通信速度が遅い場合でも動作します。これはデータをキャッシュストレージに格納することで実現しています。
この機能を利用しているサイトの中での代表例としては "Twitter Lite" があります。Twitter は PWA に対応しており、この機能を利用したモバイルサイトは Twitter Lite と名付けられています。Twitter Lite はモバイルアプリと同じような使い勝手を提供しており、読み込むデータ量を制限することで軽快に動作します。例えば、画像付きのツイートはタップするまで画像を読み込まないようにするなど、ネットワークの通信速度が遅い場合でも利用できるように作られています。
Introducing Twitter Lite on mobile web! 📱
— Twitter (@Twitter) 2017年4月6日
Loads quickly, takes up less space, and is data-friendly. Learn more: https://t.co/Zd825WOdQz pic.twitter.com/l1n0cYJuPc
PWA は "アプリケーションシェル (App Shell)" モデルによって作られています。アプリケーションシェルのアーキテクチャは、ネイティブアプリ感覚のような操作性を提供する PWA を構築するための重要な要素です。
アプリケーションの "Shell" とは、ユーザインタフェースが機能する必要最低限の HTML、CSS、JavaScript です。これらのファイルをキャッシュストレージに格納することで、再訪問時に高いパフォーマンスが発揮されます。
アプリケーションシェルが有効なページの例としては、シングルページアプリ (Single-Page Application:SPA) が挙げられます。シングルページアプリは、JavaScript を多用したアーキテクチャであるため、Service Worker にキャッシュされた Shell によってコンテンツを高速に表示できます。アプリケーションシェルのアーキテクチャで Service Worker を使用すると以下のメリットがあります。
"常に高速で安定したパフォーマンス"
再訪問時にページが高速で表示されるレスポンスを提供します。これは、最初の訪問時に静的なファイル (HTML、CSS、JavaScript、画像など) がキャッシュされるため、再訪問時に瞬時に読み込むことができます。
"ネイティブアプリのような操作性"
オフライン時においても、ネイティブアプリのようなナビゲーションと操作性をユーザに提供します。
"データの効率的な使用"
データ使用量を最小限に抑えて、キャッシュするファイルを適切に選別するように設計されています。例えば、すべてのページで使用されていない大きな画像ファイルをキャッシュすることは、通信料が高コストである発展途上国や新興市場を考慮すると行うべきではありません。
PWA は Service Worker の更新プロセスにより、常に最新の状態に保たれます。ユーザに最新のデータを提供することと、キャッシュデータを提供することはトレードオフの関係にありますが、正しいキャッシュ戦略はアプリが提供するデータの種類によって決まります。例えば、天気情報や株価など時間経過とともに変化するデータは最新でなければなりませんが、画像やテキスト記事はキャッシュから提供しても問題はありません。
データを提供する場合、キャッシュとネットワークの優先順位がキャッシュ戦略の重要な要素になります。例えば、画面に素早くデータを表示したい場合はキャッシュからのデータを返し、その後に非同期でネットワークから取得した最新のデータのみを更新する方法があります。キャッシュ優先の戦略では、ユーザの待ち時間がなくなりますが、キャッシュアクセスと、ネットワークアクセスの合計 2 回の非同期リクエストを送信する必要があります。
Service Worker では fetch
イベントハンドラを使用することでリクエストを傍受し、キャッシュから送信元にレスポンスを返すことができます。
self.addEventListener('fetch', function(e) {
e.respondWith(
fetch(e.request).catch(function() {
return caches.match(e.request);
})
);
});
fetch
イベントハンドラを使用してリクエストを傍受するサンプルコード ただし、caches
オブジェクトはすべてのブラウザで利用可能ではないため、もしも利用できない場合は、ネットワークでのリクエストに処理が移るように実装を行うべきです。caches
オブジェクトが利用可能であるか確認するためには、グローバルの window
オブジェクトを使用することで確認できます。
if ('caches' in window) {
// caches オブジェクトが利用可能
} else {
// caches オブジェクトが利用不可
}
caches
オブジェクトが利用可能か確認するためのサンプルコードService Worker は HTTPS (localhost を含む) でのみ動作します。そのため、PWA は通信の盗聴やコンテンツの改ざんを防ぐことができます。
HTTP で通信を行った場合、HTTP と HTTPS の 混在コンテンツ (mixed content) となり、セキュリティ的に HTTP で配信されるコンテンツが安全ではないため、ブラウザ側でブロックされます。混在コンテンツのブロックは解除できますが、セキュリティ上の理由から行うべきではありません。もしも混在コンテンツのブロックを解除した場合、ページ全体のセキュリティが低下し、リクエストが中間者攻撃に対して脆弱になります。
PWA は Web App Manifest と Service Worker によりアプリケーションとして認識されます。しかし、実態は Web サイトであるため、アプリケーションストアだけではなく検索エンジンからも発見できます。このようなファインダビリティに有利な側面は、ユーザにリーチしやすく、エンゲージメントを高めることができます。
発見しやすい特徴は、Google Play と App Store でアプリケーションを探す場所が異なる問題点も解決しています。Android と iOS のどちらの OS であったとしても、検索エンジンから Web サイトを検索できますし、PWA に対応していればアプリケーションとしても登録できます。
PWA は、使い続けてユーザとの関係性が構築されていくにつれて、より強力なアプリケーションとなります。その代表例として "プッシュ通知の送信" があります。ユーザが好みそうな関連情報をプッシュ通知として送信することで、再エンゲージメント (ユーザの再訪問) を促すことができます。
PWA に対応している Web サイトであれば、リンクをホーム画面に残しておくことができます。一般的な Web サイトでも同じようにお気に入りのサイトをホーム画面に残すことは可能ですが、PWA に対応している場合、モバイルアプリをインストールするようにリンクをホーム画面に残すことができます。ホーム画面に残すアイコンは、多くの場合 favicon となりますが、PWA ではオリジナルのアイコンを設定することも可能です。また、モバイルアプリをインストールする場合と異なり、ダウンロードする情報は小さいというメリットもあります。
モバイルアプリを開発する場合、複雑なインストール処理を作り込まなければなりませんが、PWA では URL のリンク情報だけで済みます。また、PWA の実態はアプリケーションではなく Web サイトであるため、簡単にリンクの共有を行うことができます。
Web App Manifest は、シンプルな JSON 形式のファイルです。このファイルの目的は、モバイルのホーム画面などに表示するアイコンや、名前などを指定するために作られます。ファイル名は任意の名前を付けることができますが、一般的には "manifest.json" という名前が使用されます。ファイルの中身は以下のような構造になっています。
{
"name": "murashun.jp",
"short_name": "murashun",
"icons": [{
"src": "img/favicon96.png",
"type": "image/png",
"sizes": "96x96"
}, {
"src": "img/favicon144.png",
"type": "image/png",
"sizes": "144x144"
}, {
"src": "img/favicon192.png",
"type": "image/png",
"sizes": "192x192"
}],
"start_url": "https://murashun.jp",
"display": "standalone",
"background_color": "#f4f4f4",
"theme_color": "#f4f4f4",
"orientation": "portrait"
}
Web App Manifest の仕様、および包括的なリファレンスについては W3C や Mozilla Developer Network を参照してください。
manifest.json では、利用可能ないくつかのメンバーがあります。メンバーとは、CSS のプロパティ (属性) のようなもので、"name"
や "icons"
のことを指します。manifest.json には定義できるメンバーが決められており、各メンバーのプロパティ値 (属性値) も、どのような値が定義できるかが決められています。
メンバー名 | 値 | 規定値 |
---|---|---|
"background_color" | 任意のカラーコード | - |
"description" | 任意の文字列 | - |
"dir" |
| "auto" |
"display" |
| "browser" |
"icons" [{"src", "sizes", "type"}] | アイコン画像へのファイルパス、大きさ、メディアタイプ | - |
"lang" | 任意の言語コード | - |
"name" | 任意の文字列 | - |
"orientation" |
| - |
"prefer_related_applications" |
| false |
"related_applications" [{"platform", "url", "id"}] | ネイティブアプリケーションのプラットフォーム、URL、ID | - |
"scope" | 任意のパス | - |
"short_name" | 任意の文字列 | - |
"start_url" | 任意の URL | - |
"theme_color" | 任意のカラーコード | - |
"background_color" メンバーは、アプリケーションの背景色を定義します。Chrome では、Web アプリを起動した瞬間から初回レンダリングまで指定した色が表示されます。最初に表示されるページの背景色や、スプラッシュ画面の背景色と同じ色を指定すると、スムーズに遷移しているように見えます。
"background_color": "red"
background_color メンバーは、アプリケーションが読み込まれるまでの背景色であることに注意してください。アプリケーションの CSS が利用可能になると、CSS で定義された背景色が優先されます。
"description" メンバーは、アプリケーションの説明文を定義します。最大 132 文字まで設定することが可能で、日本語も使用可能です。
"description": "The app that helps you find the best food in town!"
"dir" メンバーは、name メンバー、short_name メンバー、description メンバーのテキスト方向を定義します。テキストの方向は左から右が一般的ですが、アラビア語などは右から左に読まれるため、lang メンバーと合わせて言語の正しい表示をサポートします。
dir メンバーは、以下の 3 つのいずれかの値を定義します。省略時は auto が規定値になります。
"dir": "rtl",
"lang": "ar",
"short_name": "أنا من التطبيق!"
"display" メンバーは、アプリケーションの表示モードを定義します。display メンバーは以下の 4 つのいずれかの値を定義します。省略時は browser が規定値になります。
"fullscreen"
利用可能な表示エリアがすべて使用されます。
"standalone"
ブラウザの UI が非表示になるためネイティブアプリのように表示できます。
"minimal-ui"
standalone のような外観や操作感になりますが、ナビゲーション制御のために最小限の UI を表示します。どの UI が表示されるかは、ブラウザによって異なります。
"browser"
ブラウザの UI がそのまま表示されるため、ブラウザで通常のページを開いているような外観や操作感になります。
"display": "standalone"
もしも、指定された表示モードが何かしらの原因で有効にならなかった場合、下位の表示モードにフォールバックされます。
"icons" メンバーは、アプリケーションのアイコンを定義します。アイコンはサイズやタイプによって複数のファイルを指定することが可能で、以下のメンバーを定義することもできます。
"icons": [
{
"src": "img/icon.webp",
"sizes": "48x48",
"type": "image/webp"
},
{
"src": "img/icon.ico",
"sizes": "48x48 96x96 128x128 256x256"
},
{
"src": "img/icon.svg",
"sizes": "48x48"
}
]
"lang" メンバーは、name メンバーと short_name メンバーで使用する言語を指定します。指定可能な言語は、IANA で定義された "Subtag" の文字列になります。
"lang": "ja"
"name" メンバーは、Web アプリのインストールダイアログ、拡張管理 UI、Chrome ウェブストアに表示される名前を定義します。最大 45 文字まで定義することが可能で、日本語も使用可能です。
"name": "murashun.jp"
"orientation" メンバーは、画面の縦方向、横方向の向きを定義します。例えば、横方向のみで使用するゲームのようなアプリで指定すると良いでしょう。orientation メンバーは、以下の 8 つのいずれかの値を定義します。
screen.orientation.angle
が 0 (正面) になる向きに固定します。"landscape-primary", "landscape-secondary"
の省略形。"portrait-primary", "portrait-secondary"
の省略形。"orientation": "portrait-primary"
"prefer_related_applications" メンバーは、Web アプリケーションでは実行できない機能の代わりに related_applications メンバーで定義されているネイティブアプリケーションの機能を提供する場合に指定します。省略時は false が規定値になります。
"prefer_related_applications": false
"related_applications" メンバーは、Web アプリケーションを代替するネイティブアプリケーションの情報を定義します。related_applications メンバーは、以下のメンバーを定義することもできます。
"related_applications": [
{
"platform": "play",
"url": "https://play.google.com/store/apps/details?id=example.app1",
"id": "example.app1"
}, {
"platform": "itunes",
"url": "https://itunes.apple.com/app/example-app1/id123456789"
}]
"scope" メンバーは、Web アプリケーションコンテキストの範囲を定義します。ユーザが定義されたスコープ範囲外にアクセスした場合、通常の Web ページに遷移します。
"scope": "/myapp/"
"short_name" メンバーは、アプリケーション名の短縮した名前を定義します。12 文字以内で定義することが推奨されており、アプリケーションのフルネームを表示するための十分なスペースがない場所で使用されます。Google Developers の説明では必須項目となっていますが、省略可能です。ただし、省略した場合は name メンバーが使用され、文字列が長いと切り捨てられます。
"short_name": "msn"
"start_url" メンバーは、アプリケーション起動時に、最初に読み込まれる URL を定義します。URL には相対パスや GET パラメータを指定することも可能です。
"start_url": "./?pwa=1"
"theme_color" メンバーは、アプリケーションのテーマカラーを指定します。テーマカラーは、ブラウザのアドレスバーなどの UI 要素の色を指定できます。これは Android 版の Chrome などにテーマカラーを指定する場合、各ページの meta タグから指定していた定義を manifest.json で一括して定義できます。その他にも、Android のタスクスイッチャーでは、アプリケーションはテーマカラーで囲まれるなど、OS によって表示方法に影響を与えます。
"theme_color": "aliceblue"
manifest.json を作成してサイトに配置したら、Web アプリを含むすべてのページに以下のような link
タグを追加します。
<link rel="manifest" href="/manifest.json">
Web App Manifest が正しく設定されていることを確認するには、Chrome DevTools で簡単に確認できます。Chrome DevTools から "Application" パネルにある "Manifest" タブを開き、以下のように表示されれば、正しく設定されています。
Service Worker とは、PWA の中枢的な機能であり、実態は Web ページの裏側で処理を行うイベント駆動の JavaScript 環境です。Service Worker を利用すると、オフラインサポートをはじめ、これまでの Web では実現できなかった機能を提供できます。
オフラインをサポートする技術は Service Worker より前にも AppCache という技術がありました。しかし、挙動が不安定で、SPA (Single Page Application) では機能するものの、ページ遷移を伴うサイトではうまく動かない問題点などから普及しませんでした。Service Worker は、それらの問題点を回避するように設計されています。
Service Worker の特徴をまとめると以下のようなものがあります。
ただし、上記の機能は Service Worker だけで実現する機能ではありません。Service Worker をベースに "Fetch API"、"Cache API"、"Push API" といった別の機能を組み合わせて実現しています。PWA 自体も新しい機能ではなく、このような既存の技術を組み合わせて名前を付けたものになります。
ただし、Service Worker をサポートしているブラウザでも、バージョンに注意する必要があります。例えば、Chrome 46 より前のバージョンでは Cache.addAll()
など一部の重要な機能が利用できません。古いバージョンの Chrome もサポートしたい場合は、"polyfill" を利用して足りない機能を補う必要があります。以下のようにインポートされたスクリプトは、Service Worker によってキャッシュされます。
// sw.js から polyfill.js をインポートする
importScripts('serviceworker-cache-polyfill.js');
古いバージョンのブラウザも動作保証対象とする場合、polyfill.js をインポートすることを検討してください。
Service Worker は Web ページとは独立したバックグラウンド環境で動作するため、ライフサイクルも異なります。Web ページのライフサイクルは、ページを開いてから他のページに移動、またはページを閉じるまでになります。一方、Service Worker のライフサイクルは、インスタンスが初期化され、それが破棄されるまでの期間になります。Service Worker のライフサイクルにおけるステータスは、以下の 6 つの状態があります。
"parsed" は、Service Worker がまだインストールされていない初期状態を表します。parsed 状態では、すぐに次の状態に遷移するため、始点の意味で使われます。
"installing"は、Service Worker をインストールしている状態を表します。Service Worker が未登録状態での新規インストール (register) または、登録済みでの更新インストール (update) の状態となります。この際、Service Worker のライフサイクルイベントである "install" イベントが発生します。ただし、更新インストールにおいて何も更新する必要がない場合は、activated までスキップされます。
"installed" は、installing を正常に終了した状態を表します。新規インストールの場合は、次の activating に遷移します。更新インストールの場合は、古い Service Worker がまだ Web ページを制御しているため、更新によるデータ不整合を防ぐために新しい Service Worker はこの状態で待機 (waiting) します。ユーザがページから離脱するなどして、古い Service Worker が安全に破棄された後にユーザが再訪問すると、次の activating 状態に遷移します。
"activating" は、新しい Service Worker を有効にしている状態を表します。この際、Service Worker のライフサイクルイベントである "activate" イベントが発生します。
"activated" は、新しい Service Worker が正常に有効になった状態を表します。この状態になると Service Worker は "fetch" イベントや "message" イベントを待つアイドル状態になります。
"redundant" は、以下の理由で Service Worker が無効となった状態を表します。
Service Worker のライフサイクルを図示すると以下のようになります。
Service Worker の登録やインストールは、以下のようなコードで実装できます。
// sw.js の登録処理を記述する
navigator.serviceWorker.register('/sw.js', {scope: './'});
// sw.js にインストール処理を記述する
self.addEventListener('install', function(e) {
e.waitUntil(
caches.open('static-v1').then(function(cache) {
return cache.addAll([
'/',
'/index.html',
'/css/style.css',
'/img/logo.png',
'/js/script.js'
]);
})
);
});
一般的にインストール処理では、静的なアセットをキャッシュするために使われます。静的なアセットとは、変更される機会が少ないファイルを指します。
Service Worker におけるインストール処理の成功・失敗は、データベースで言う原子性 (Atomicity) が保証されています。つまり、結果は "すべて成功" か "すべて失敗" のいずれかしかありません。例えば、複数のアセットをインストールする場合、ひとつでもダウンロードに失敗すると、すべてのアセットがインストールされません。
Service Worker はユーザのリクエストに対して強制的に割り込めるため、どこまでページを制御できるかが定められています。これらの定義により、Service Worker は安全に利用できます。Service Worker の重要な 3 つのセキュリティルールを見ていきましょう。
Service Worker は "HTTPS" (localhost も含む) で提供されている Web ページでしか登録できません。このルールによって、通信の盗聴や改ざんを受けていないことを保証しています。
Service Worker は "同一のオリジンポリシー" から提供されたリソースにのみアクセスできます。同一のオリジンポリシーとは、スキーム、ポート、ホストが同じであることを指します。これは Web セキュリティにおける重要な仕組みであり、悪意の含まれているリソースの分離を目的としています。以下の例では、同一のオリジンとの比較で不一致になったページには、比較元の URL から登録された Service Worker を制御下に置くことはできません。
URL | 結果 | 理由 |
---|---|---|
http://app.example.com/dir1/main.html | 同一 | - |
http://app.example.com/dir2/sub.html | 同一 | - |
https://app.example.com/secure.html | 不一致 | 異なるスキーム |
http://app.example.com:81/port.html | 不一致 | 異なるポート |
http://blog.example.com/host.html | 不一致 | 異なるホスト |
Service Worker は "スコープ" よりも上位のパスにあるページを制御下に置くことはできません。そのため、Service Worker のファイルを最上位のパスに置いて、サイト全体を Service Worker の制御下にしているサイトが多くあります。以下の例では、scope 配下のみ Service Worker の制御下に置くことが可能で、www 配下の index.html は制御下に置くことはできません。
/www
|-- index.html # Service Worker の制御下に置けない
|
|-- /scope
|-- sw.js # Service Worker のファイル
|-- page1.html # Service Worker の制御下に置ける
|
|-- /sub
|-- page2.html # Service Worker の制御下に置ける
ただし、HTTP ヘッダの "Service-Worker-Allowed" フィールドに任意のパスを指定した場合、Service Worker ファイルよりも上位のパスにあるページを制御できるようになります。
Service-Worker-Allowed: /www
Service Worker のリクエストでは HTTP ヘッダに "Service-Worker: script" が含まれます。そのため、サイト側の設定で除外することや、ホワイトリストで許可されたリクエストのみ通信することも可能です。
Service Worker を登録するには register()
関数を使います。
navigator.serviceWorker.register('/sw.js');
Service Worker を登録する場合、パラメータを省略すると Service Worker のファイルが存在する階層が規定値として設定されます。スコープを明示的に設定する場合、以下のように設定します。
navigator.serviceWorker.register('/sw.js', {scope: '/example'});
register()
での初回登録時、または Service Worker ファイルが更新されている場合、onupdatefound
イベントが発火します。すでに Service Worker が登録済みであるかどうかは、ブラウザが自動的にチェックしてくれます。
Service Worker の新規インストール、または更新インストールの場合、installing 状態になり oninstall
イベントが発火します。インストール時に何かの処理を行う場合、Service Worker ファイルの中で oninstall
イベントを監視します。
self.addEventListener('install', function(event) {
// 任意の処理
});
oninstall
イベントを監視するブラウザは保持している Service Worker と、ダウンロードする Service Worker に 1 byte でも違う場合、更新されたと判断します。Service Worker が更新される場合、現在保持している Service Worker がデータ不整合を起こさず安全に破棄されるまで waiting 状態で待機します。ユーザがページから離脱すると Service Worker は破棄され、再訪問時に waiting 状態から active 状態に移行します。
ただし、データ不整合などが発生しないことが明らかで、すぐに active 状態にしても問題がない場合は skipWaiting()
関数を呼ぶことで waiting 状態をスキップできます。
self.addEventListener('install', function(event) {
event.waitUntil(self.skipWaiting());
});
skipWaiting()
関数で waiting 状態をスキップする また、Service Worker は active 状態になってもすぐにブラウザをコントロールしません。コントロールするタイミングは、次回ページが表示された時です。ただし、claim()
を呼ぶことですぐにコントロールできます。
self.addEventListener('install', function(event) {
event.waitUntil(self.claim());
});
claim()
関数で即時コントロールを得るCache API とは、リソースをキャッシュストレージに格納する API で、キャッシュされたリソースは Fetch API を利用することでオフライン環境でも取得できるようになります。キャッシュする対象のリソースは、画像、CSS、スクリプトなどの静的ファイルです。実際に Service Worker に書かれる install イベント、activate イベント、fetch イベントをハンドルするサンプルコードを順番に紹介します。
const CACHE_NAME = '20180101-01';
const CACHE_FILE = [
'./',
'./css/styles.css',
'./img/image.png',
'./js/script.js'
];
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME).then(function(cache) {
return cache.addAll(CACHE_FILE);
})
);
});
CACHE_NAME はキャッシュストレージに格納される時の名前です。Chrome の DevTools の Application から正しく格納されているかが確認できます。
self.addEventListener('activate', function(event) {
var cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(cacheNames.map(function(cacheName) {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
}));
})
);
});
キャッシュをコントロールするために、CACHE_NAME を利用してバージョン管理を行います。CACHE_NAME を変更すると、Service Worker は新しいキャッシュ名がキャッシュストレージに存在しないため、キャッシュが更新されたと判断します。
このタイミングで install イベントが発火し、キャッシュストレージには古いキャッシュと、新しいキャッシュの 2 つのバージョンが混在する状態になります。ユーザがページから離脱し、再訪問すると activate イベントが発火し、古いキャッシュは削除され、新しいキャッシュが有効になります。
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
if (response) {
return response;
}
// リクエストのクローンを作成する
let ReqClone = event.request.clone();
return fetch(ReqClone).then(function(response) {
if (!response ||
response.status !== 200 ||
response.type !== 'basic') {
return response;
}
// レスポンスのクローンを作成する
let ResClone = response.clone();
caches.open(CACHE_NAME).then(function(cache) {
cache.put(event.request, ResClone);
});
return response;
});
})
);
});
Service Worker がブラウザをコントロールしている時にリソースのリクエストが発生すると fetch イベントが発火します。fetch イベントをハンドルしなければ、通常通りネットワーク経由でリクエストが処理されます。上記のサンプルコードでは、fetch イベントをハンドルして処理を行っています。
最初の caches.match()
関数では、リソースがキャッシュに格納済みであるかを確認し、キャッシュに格納済みであればそのリソースをレスポンスとして返しています。キャッシュになければサーバにリクエストを送り、レスポンスはキャッシュに格納してからクライアントに返しています。
ここで重要な点は、リクエストとレスポンスは Stream 形式であるためデータを使いまわすことができない点です。そのため、フェッチやキャッシュを行う場合は、クローンを作成する必要があります。
Service Worker からリソース取得が取得できているか確認するには Chrome の DevTools の Network から確認できます。Size の列が "(from ServiceWorker)" となっていれば成功です。
ここで紹介したサンプルコードは、Service Worker の Cache API と Fetch API を利用した一般的なものですが、この他にも様々なことができます。Service Worker の様々なレシピについては The offline cookbook に詳しくまとめられています。
PWA は、既存のネイティブなモバイルアプリケーションと比べて多くのメリットがあり、大きな可能性を秘めています。しかし、PWA はまだ新しいために受け入れられるには時間がかかるかもしれません。
特に Service Worker は既存サイトにオプトインで対応可能で、Service Worker をサポートしているブラウザは UX が向上する一方で、Service Worker をサポートしていないブラウザに一切の悪影響を与えません。PWA による Web サイトのアプリケーション化は行わなくても、キャッシュストレージを利用したパフォーマンスチューニングだけでも導入を検討する余地があります。少し難しい点もありますが、本記事が理解の助けになれば幸いです。