Progressive Web App を始めよう

Progressive Web App とは

Progressive Web App (PWA) とは、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 環境です。 現在ではプッシュ通知や、バックグラウンド同期などの機能が提供されており、将来的には定期的な同期や、ジオフェンシング (位置情報を利用したサービス) などが提供される予定です。

Service Worker の対応状況
ブラウザ対応状況説明
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 をサポートするブラウザ

また、Service Worker のどの機能が利用可能であるかはブラウザのバージョンによって異なるため注意が必要です。 ブラウザのバージョン別による機能の実装状況は、以下のページで確認できます。

他にも対象のブラウザで Service Worker が利用できるかは、以下のスクリプトを実行することでも調べることができます。 対応している場合は true、対応していない場合は false を返します。

<html>
  <body>
    <script>
      alert('serviceWorker' in navigator);
    </script>
  </body>
</html>
ブラウザで Service Worker が利用できるか調べる Script

レスポンシブ

PC やモバイルやタブレットは、それぞれ解像度が異なります。 これらのマルチデバイスに個別対応することは非常に難しいため、レスポンシブ Web デザインを用いることで様々なデバイスに適合できます。

Google では、レスポンシブ Web デザインを "ユーザのデバイスに関係なく、画面サイズに応じて表示を変えることができる" ものと定義しています。

レスポンシブウェブデザインの例
レスポンシブウェブデザインの例 (image credits : developers.google.com)

PWA ではレスポンシブな表示を行うことで、どのようなデバイスに対しても同じような操作性、情報のアクセシビリティを可能としています。 モバイルのネイティブアプリでは、解像度が異なるデバイスに最適化させるためには個別の対応が必要となりますが、レスポンシブはそのような開発コストを削減できます。

ネットワークに依存しない

Service Worker の機能により、オフライン、またはネットワークの通信速度が遅い場合でも動作します。 これはデータをキャッシュストレージに格納することで実現しています。

この機能を利用しているサイトの中での代表例としては "Twitter Lite" があります。 Twitter は PWA に対応しており、この機能を利用したモバイルサイトは Twitter Lite と名付けられています。 Twitter Lite はモバイルアプリと同じような使い勝手を提供しており、読み込むデータ量を制限することで軽快に動作します。 例えば、画像付きのツイートはタップするまで画像を読み込まないようにするなど、ネットワークの通信速度が遅い場合でも利用できるように作られています。



アプリケーション感覚

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

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"
}
manifest.json のサンプルコード

Web App Manifest の仕様、および包括的なリファレンスについては W3C や Mozilla Developer Network を参照してください。

manifest.json では、利用可能ないくつかのメンバーがあります。 メンバーとは、CSS のプロパティ (属性) のようなもので、"name""icons" のことを指します。 manifest.json には定義できるメンバーが決められており、各メンバーのプロパティ値 (属性値) も、どのような値が定義できるかが決められています。

Web App Manifest のメンバ一覧
メンバー名規定値
"background_color"任意のカラーコード
"description"任意の文字列
"dir"
  • "ltr"
  • "rtl"
  • "auto"
"auto"
"display"
  • "fullscreen"
  • "standalone"
  • "minimal-ui"
  • "browser"
"browser"
"icons"
[{"src", "sizes", "type"}]
アイコン画像へのファイルパス、大きさ、メディアタイプ
"lang"任意の言語コード
"name"任意の文字列
"orientation"
  • "any"
  • "natural"
  • "landscape"
  • "landscape-primary"
  • "landscape-secondary"
  • "portrait"
  • "portrait-primary"
  • "portrait-secondary"
"prefer_related_applications"
  • true
  • false
false
"related_applications"
[{"platform", "url", "id"}]
ネイティブアプリケーションのプラットフォーム、URL、ID
"scope"任意のパス
"short_name"任意の文字列
"start_url"任意の URL
"theme_color"任意のカラーコード

background_color メンバー

"background_color" メンバーは、アプリケーションの背景色を定義します。 Chrome では、Web アプリを起動した瞬間から初回レンダリングまで指定した色が表示されます。 最初に表示されるページの背景色や、スプラッシュ画面の背景色と同じ色を指定すると、スムーズに遷移しているように見えます。

"background_color": "red"
background_color メンバーのサンプルコード

background_color メンバーは、アプリケーションが読み込まれるまでの背景色であることに注意してください。 アプリケーションの CSS が利用可能になると、CSS で定義された背景色が優先されます。

description メンバー

"description" メンバーは、アプリケーションの説明文を定義します。 最大 132 文字まで設定することが可能で、日本語も使用可能です。

"description": "The app that helps you find the best food in town!"
description メンバーのサンプルコード

dir メンバー

"dir" メンバーは、name メンバー、short_name メンバー、description メンバーのテキスト方向を定義します。 テキストの方向は左から右が一般的ですが、アラビア語などは右から左に読まれるため、lang メンバーと合わせて言語の正しい表示をサポートします。

dir メンバーは、以下の 3 つのいずれかの値を定義します。 省略時は auto が規定値になります。

  • ltr:左から右
  • rtl:右から左
  • auto:自動判定
"dir": "rtl",
"lang": "ar",
"short_name": "أنا من التطبيق!"
dir メンバーのサンプルコード

display メンバー

"display" メンバーは、アプリケーションの表示モードを定義します。 display メンバーは以下の 4 つのいずれかの値を定義します。 省略時は browser が規定値になります。

  • fullscreen
  • standalone
  • minimal-ui
  • browser

"fullscreen"
利用可能な表示エリアがすべて使用されます。

"standalone"
ブラウザの UI が非表示になるためネイティブアプリのように表示できます。

"minimal-ui"
standalone のような外観や操作感になりますが、ナビゲーション制御のために最小限の UI を表示します。 どの UI が表示されるかは、ブラウザによって異なります。

"browser"
ブラウザの UI がそのまま表示されるため、ブラウザで通常のページを開いているような外観や操作感になります。

"display": "standalone"
display メンバーのサンプルコード

もしも、指定された表示モードが何かしらの原因で有効にならなかった場合、下位の表示モードにフォールバックされます。

icons メンバー

"icons" メンバーは、アプリケーションのアイコンを定義します。 アイコンはサイズやタイプによって複数のファイルを指定することが可能で、以下のメンバーを定義することもできます。

  • src:画像ファイルへのパスを指定します。
  • sizes:画像の大きさを指定します。複数のサイズを持つ場合、スペースで区切ります。
  • type:画像のメディアタイプを指定します。
"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"
  }
]
icons メンバーのサンプルコード

lang メンバー

"lang" メンバーは、name メンバーと short_name メンバーで使用する言語を指定します。 指定可能な言語は、IANA で定義された "Subtag" の文字列になります。

"lang": "ja"
lang メンバーのサンプルコード

name メンバー

"name" メンバーは、Web アプリのインストールダイアログ、拡張管理 UI、Chrome ウェブストアに表示される名前を定義します。 最大 45 文字まで定義することが可能で、日本語も使用可能です。

"name": "murashun.jp"
name メンバーのサンプルコード

orientation メンバー

"orientation" メンバーは、画面の縦方向、横方向の向きを定義します。 例えば、横方向のみで使用するゲームのようなアプリで指定すると良いでしょう。 orientation メンバーは、以下の 8 つのいずれかの値を定義します。

  • any:すべての向きに対して回転を許可します。
  • natural:screen.orientation.angle が 0 (正面) になる向きに固定します。
  • landscape:"landscape-primary", "landscape-secondary" の省略形。
  • landscape-primary:縦長が正面の端末で縦方向に固定します。
  • landscape-secondary:横長が正面の端末で縦方向に固定します。
  • portrait:"portrait-primary", "portrait-secondary" の省略形。
  • portrait-primary:横長が正面の端末で横方向に固定します。
  • portrait-secondary:縦長が正面の端末で横方向に固定します。
"orientation": "portrait-primary"
orientation メンバーのサンプルコード

prefer_related_applications メンバー

"prefer_related_applications" メンバーは、Web アプリケーションでは実行できない機能の代わりに related_applications メンバーで定義されているネイティブアプリケーションの機能を提供する場合に指定します。 省略時は false が規定値になります。

"prefer_related_applications": false
prefer_related_applications メンバーのサンプルコード

related_applications メンバー

"related_applications" メンバーは、Web アプリケーションを代替するネイティブアプリケーションの情報を定義します。 related_applications メンバーは、以下のメンバーを定義することもできます。

  • platform:アプリケーションを発見するプラットフォーム。
  • url:アプリケーションの URL。
  • id:アプリケーションの ID。
"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"
  }]
related_applications メンバーのサンプルコード

scope メンバー

"scope" メンバーは、Web アプリケーションコンテキストの範囲を定義します。 ユーザが定義されたスコープ範囲外にアクセスした場合、通常の Web ページに遷移します。

"scope": "/myapp/"
scope メンバーのサンプルコード

short_name メンバー

"short_name" メンバーは、アプリケーション名の短縮した名前を定義します。 12 文字以内で定義することが推奨されており、アプリケーションのフルネームを表示するための十分なスペースがない場所で使用されます。 Google Developers の説明では必須項目となっていますが、省略可能です。 ただし、省略した場合は name メンバーが使用され、文字列が長いと切り捨てられます。

"short_name": "msn"
short_name メンバーのサンプルコード

start_url メンバー

"start_url" メンバーは、アプリケーション起動時に、最初に読み込まれる URL を定義します。 URL には相対パスや GET パラメータを指定することも可能です。

"start_url": "./?pwa=1"
start_url メンバーのサンプルコード

theme_color メンバー

"theme_color" メンバーは、アプリケーションのテーマカラーを指定します。 テーマカラーは、ブラウザのアドレスバーなどの UI 要素の色を指定できます。 これは Android 版の Chrome などにテーマカラーを指定する場合、各ページの meta タグから指定していた定義を manifest.json で一括して定義できます。 その他にも、Android のタスクスイッチャーでは、アプリケーションはテーマカラーで囲まれるなど、OS によって表示方法に影響を与えます。

"theme_color": "aliceblue"
theme_color メンバーのサンプルコード

manifest.json の存在をブラウザに伝える

manifest.json を作成してサイトに配置したら、Web アプリを含むすべてのページに以下のような link タグを追加します。

<link rel="manifest" href="/manifest.json">
manifest.json の存在をブラウザに伝える

Web App Manifest をテストする

Web App Manifest が正しく設定されていることを確認するには、Chrome DevTools で簡単に確認できます。 Chrome DevTools から "Application" パネルにある "Manifest" タブを開き、以下のように表示されれば、正しく設定されています。

Web App Manifest の確認画面
Web App Manifest の確認画面

Service Worker

Service Worker とは、PWA の中枢的な機能であり、実態は Web ページの裏側で処理を行うイベント駆動の JavaScript 環境です。 Service Worker を利用すると、オフラインサポートをはじめ、これまでの Web では実現できなかった機能を提供できます。

オフラインをサポートする技術は Service Worker より前にも AppCache という技術がありました。 しかし、挙動が不安定で、SPA (Single Page Application) では機能するものの、ページ遷移を伴うサイトではうまく動かない問題点などから普及しませんでした。 Service Worker は、それらの問題点を回避するように設計されています。

Service Worker の特徴をまとめると以下のようなものがあります。

  • イベント駆動の JavaScript Worker のひとつ。
  • Web サイトとは独立したバックグラウンド処理を行う。
  • Web サイトとは独立したライフサイクルを持つ。
  • ネットワークリクエストをコントロール可能。
  • 必要に応じて起動、終了するため、ライフサイクル間でステータスを共有できない。ただし、IndexedDB API を利用すればライフサイクル間で情報を共有可能。
  • DOM に直接アクセスができない。ただし、コントールするページ経由であれば可能。
  • JavaScript の Promises を多用する。

ただし、上記の機能は 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 をインポートする例

古いバージョンのブラウザも動作保証対象とする場合、polyfill.js をインポートすることを検討してください。

Service Worker のライフサイクル

Service Worker は Web ページとは独立したバックグラウンド環境で動作するため、ライフサイクルも異なります。 Web ページのライフサイクルは、ページを開いてから他のページに移動、またはページを閉じるまでになります。 一方、Service Worker のライフサイクルは、インスタンスが初期化され、それが破棄されるまでの期間になります。 Service Worker のライフサイクルにおけるステータスは、以下の 6 つの状態があります。

  • parsed
  • installing
  • installed
  • activating
  • activated
  • redundant

"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 が無効となった状態を表します。

  • installing 中にエラーが発生した
  • activating 中にエラーが発生した
  • 新しい Service Worker と置き換えられた

Service Worker のライフサイクルを図示すると以下のようになります。

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 は安全に利用できます。 Service Worker の重要な 3 つのセキュリティルールを見ていきましょう。

Service Worker は "HTTPS" (localhost も含む) で提供されている Web ページでしか登録できません。 このルールによって、通信の盗聴や改ざんを受けていないことを保証しています。

Service Worker は "同一のオリジンポリシー" から提供されたリソースにのみアクセスできます。 同一のオリジンポリシーとは、スキーム、ポート、ホストが同じであることを指します。 これは Web セキュリティにおける重要な仕組みであり、悪意の含まれているリソースの分離を目的としています。 以下の例では、同一のオリジンとの比較で不一致になったページには、比較元の URL から登録された Service Worker を制御下に置くことはできません。

http://app.example.com/dir/page.html と同一のオリジンであるかの比較
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 の制御下に置ける
Service Worker の scope 範囲

ただし、HTTP ヘッダの "Service-Worker-Allowed" フィールドに任意のパスを指定した場合、Service Worker ファイルよりも上位のパスにあるページを制御できるようになります。

Service-Worker-Allowed: /www
Service Worker の scope 範囲を変更する例

Service Worker のリクエストでは HTTP ヘッダに "Service-Worker: script" が含まれます。 そのため、サイト側の設定で除外することや、ホワイトリストで許可されたリクエストのみ通信することも可能です。

Service Worker の登録

Service Worker を登録するには register() 関数を使います。

navigator.serviceWorker.register('/sw.js');
Service Worker の登録

Service Worker を登録する場合、パラメータを省略すると Service Worker のファイルが存在する階層が規定値として設定されます。 スコープを明示的に設定する場合、以下のように設定します。

navigator.serviceWorker.register('/sw.js', {scope: '/example'});
Service Worker の登録 (スコープ指定あり)

register() での初回登録時、または Service Worker ファイルが更新されている場合、onupdatefound イベントが発火します。 すでに Service Worker が登録済みであるかどうかは、ブラウザが自動的にチェックしてくれます。

Service Worker のインストール

Service Worker の新規インストール、または更新インストールの場合、installing 状態になり oninstall イベントが発火します。 インストール時に何かの処理を行う場合、Service Worker ファイルの中で oninstall イベントを監視します。

self.addEventListener('install', function(event) {
  // 任意の処理
});
oninstall イベントを監視する

Service Worker の更新

ブラウザは保持している 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/Fetch API

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);
    })
  );
});
install イベントハンドラ

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);
        }
      }));
    })
  );
});
activate イベントハンドラ

キャッシュをコントロールするために、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;
      });
    })
  );
});
fetch イベントハンドラ

Service Worker がブラウザをコントロールしている時にリソースのリクエストが発生すると fetch イベントが発火します。 fetch イベントをハンドルしなければ、通常通りネットワーク経由でリクエストが処理されます。 上記のサンプルコードでは、fetch イベントをハンドルして処理を行っています。

最初の caches.match() 関数では、リソースがキャッシュに格納済みであるかを確認し、キャッシュに格納済みであればそのリソースをレスポンスとして返しています。 キャッシュになければサーバにリクエストを送り、レスポンスはキャッシュに格納してからクライアントに返しています。

ここで重要な点は、リクエストとレスポンスは Stream 形式であるためデータを使いまわすことができない点です。 そのため、フェッチやキャッシュを行う場合は、クローンを作成する必要があります。

Service Worker からリソース取得が取得できているか確認するには Chrome の DevTools の Network から確認できます。 Size の列が "(from ServiceWorker)" となっていれば成功です。

Service Worker からのリソース取得
Service Worker からのリソース取得

ここで紹介したサンプルコードは、Service Worker の Cache API と Fetch API を利用した一般的なものですが、この他にも様々なことができます。 Service Worker の様々なレシピについては The offline cookbook に詳しくまとめられています。

まとめ

PWA は既存のネイティブなモバイルアプリケーションと比べて多くのメリットがあり、大きな可能性を秘めています。 しかし、PWA はまだ新しいために、受け入れられるには時間がかかるかもしれません。

特に Service Worker は既存サイトにオプトインで対応可能で、Service Worker をサポートしているブラウザは UX が向上する一方で、Service Worker をサポートしていないブラウザに一切の悪影響を与えません。 PWA による Web サイトのアプリケーション化は行わなくても、キャッシュストレージを利用したパフォーマンスチューニングだけでも導入を検討する余地があります。 少し難しい点もありますが、本記事が理解の助けになれば幸いです。