|
| 1 | +--- |
| 2 | +title: "Service Worker を使ってアプリケーション データをキャッシュする" |
| 3 | +description: "プログレッシブ ウェブアプリにてアプリケーションデータをキャッシュするためにService Workerを使用する" |
| 4 | +updated_on: 2016-05-04 |
| 5 | +translators: |
| 6 | + - yoichiro |
| 7 | +--- |
| 8 | + |
| 9 | +<p class="intro"> |
| 10 | +データに正しいキャッシュ戦略を選択することは重要であり、これはアプリで提供する |
| 11 | +データの種類によって決まります。たとえば、天気情報や株価など時間の経過とともに |
| 12 | +変動するデータはできるだけ最新のものでなければなりませんが、アバターの画像や記事の |
| 13 | +コンテンツなどは更新の頻度が比較的少なくても問題はないと考えられます。 |
| 14 | +</p> |
| 15 | + |
| 16 | +{% include shared/toc.liquid %} |
| 17 | + |
| 18 | +今回のアプリに適しているのは、**まずキャッシュ、次にネットワークという優先**順でデータを |
| 19 | +取得する戦略です。この戦略では、画面にとにかく早くデータを表示し、その後ネットワーク |
| 20 | +から最新のデータが返された時点でデータの更新を行います。**キャッシュではなく |
| 21 | +ネットワークを優先**した場合、ネットワークからの fetch がタイムアウトになってから |
| 22 | +キャッシュ データが取得されることになり、待ち時間が発生してしまいます。キャッシュ |
| 23 | +優先の場合はこうした待ち時間がなくなります。 |
| 24 | + |
| 25 | +キャッシュ、ネットワークの順でデータを取得するには、キャッシュに 1 回、 |
| 26 | +ネットワークに 1 回、合計 2 回の非同期リクエストを送信する必要があります。 |
| 27 | +アプリのネットワーク リクエストにはそれほど変更を加える必要はありませんが、 |
| 28 | +Service Worker には、応答を返す前にキャッシュを行うよう変更を加える必要が |
| 29 | +あります。 |
| 30 | + |
| 31 | +以上の理由から、非同期リクエストを 2 回(キャッシュに 1 回、ネットワークに 1 回) |
| 32 | +行う必要があります。通常は、キャッシュ データはほぼ瞬時に返され、最近のデータとして |
| 33 | +アプリで利用可能になります。そしてネットワークのリクエストが返されると、ネットワーク |
| 34 | +からの最新データを基にアプリが更新されます。 |
| 35 | + |
| 36 | +## ネットワーク リクエストを傍受して応答をキャッシュする |
| 37 | + |
| 38 | +Service Worker に対し、Weather API へのリクエストを傍受するように、また後の |
| 39 | +アクセスを容易にするためその応答を Cache に格納するように変更を加えます。 |
| 40 | +**キャッシュ、ネットワークの順**にデータを取得する戦略では、ネットワークの応答を |
| 41 | +「確実な情報源」として想定し、常に最新の情報を提供するものとして位置づけます。 |
| 42 | +ネットワークからデータを取得できない場合は、アプリで最新のキャッシュ データを取得 |
| 43 | +しているので、ネットワークで失敗しても問題はないということになります。 |
| 44 | + |
| 45 | +Service Worker に `dataCacheName` を追加し、アプリケーションのデータと |
| 46 | +App Shell を切り離せるように設定しましょう。こうすると、App Shell が更新されて |
| 47 | +古いキャッシュが消去されても、データは変更されず高速な読み込みに対応できます。なお、 |
| 48 | +将来データ形式が変わった場合は、App Shell とコンテンツの同期を確保しつつ新しい |
| 49 | +形式に対応する方法が必要になります。 |
| 50 | + |
| 51 | +`service-worker.js` ファイルの先頭に次の行を追加します。 |
| 52 | + |
| 53 | +{% highlight javascript %} |
| 54 | +var dataCacheName = 'weatherData-v1'; |
| 55 | +{% endhighlight %} |
| 56 | + |
| 57 | +次に、`fetch` イベント ハンドラに変更を加え、データ API へのリクエストを他の |
| 58 | +リクエストと別に処理できるようにする必要があります。 |
| 59 | + |
| 60 | +{% highlight javascript hl_lines="3 4 5 6" %} |
| 61 | +self.addEventListener('fetch', function(e) { |
| 62 | + console.log('[ServiceWorker] Fetch', e.request.url); |
| 63 | + var dataUrl = 'https://publicdata-weather.firebaseio.com/'; |
| 64 | + if (e.request.url.indexOf(dataUrl) === 0) { |
| 65 | + // Put data handler code here |
| 66 | + } else { |
| 67 | + e.respondWith( |
| 68 | + caches.match(e.request).then(function(response) { |
| 69 | + return response || fetch(e.request); |
| 70 | + }) |
| 71 | + ); |
| 72 | + } |
| 73 | +}); |
| 74 | +{% endhighlight %} |
| 75 | + |
| 76 | +このコードでは、リクエストを傍受し、URL の先頭が Weather API のアドレスかどうかを |
| 77 | +確認します。URL の先頭が Weather API のアドレスであれば、`fetch` を使用して |
| 78 | +リクエストを行います。応答が返されたらキャッシュを開き、応答をコピーして格納した後、 |
| 79 | +リクエストの送信元に応答を返します。 |
| 80 | + |
| 81 | +次に、コードの `// Put data handler code here` の部分を以下のコードに |
| 82 | +置き換えます。 |
| 83 | + |
| 84 | +{% highlight javascript %} |
| 85 | +e.respondWith( |
| 86 | + fetch(e.request) |
| 87 | + .then(function(response) { |
| 88 | + return caches.open(dataCacheName).then(function(cache) { |
| 89 | + cache.put(e.request.url, response.clone()); |
| 90 | + console.log('[ServiceWorker] Fetched&Cached Data'); |
| 91 | + return response; |
| 92 | + }); |
| 93 | + }) |
| 94 | +); |
| 95 | +{% endhighlight %} |
| 96 | + |
| 97 | +このアプリはまだオフラインでは動作しません。App Shell のデータのキャッシュと取得を |
| 98 | +実装しましたが、データをキャッシュできてもまだネットワークに依存している状態です。 |
| 99 | + |
| 100 | +## リクエストを行う |
| 101 | + |
| 102 | +前に説明したとおり、アプリではキャッシュに 1 回、ネットワークに 1 回、合計 2 回の |
| 103 | +非同期リクエストを送信する必要があります。アプリでは `window` で利用可能な |
| 104 | +`caches` オブジェクトを使ってキャッシュにアクセスし、最新のデータを取得します。 |
| 105 | +これはプログレッシブ・エンハンスメントを実装する場合の良い例です。すべてのブラウザで |
| 106 | +`caches` オブジェクトが利用可能とは限らず、`caches` オブジェクトが利用できない |
| 107 | +ときはネットワーク リクエストが引き続き動作可能でなければならないからです。 |
| 108 | + |
| 109 | +必要な手順は次のとおりです。 |
| 110 | + |
| 111 | +1. グローバルな `window` オブジェクトにおいて、`caches` オブジェクトが利用可能か |
| 112 | +どうかを確認します。 |
| 113 | +1. キャッシュにデータをリクエストします。 |
| 114 | + 1. サーバーへのリクエストでまだ応答がない場合は、キャッシュ データを使ってアプリを更新します。 |
| 115 | +1. サーバーにデータをリクエストします。 |
| 116 | + 1. データを保存し、後ですばやくアクセスできるようにします。 |
| 117 | + 1. サーバーからの最新データを使ってアプリを更新します。 |
| 118 | + |
| 119 | +まれに、キャッシュよりも先に XHR が応答することがあります。このような場合に |
| 120 | +キャッシュによってアプリが更新されないように、まずフラグを追加しましょう。`app` |
| 121 | +オブジェクトに `hasRequestPending: false` を追加します。 |
| 122 | + |
| 123 | +次に、`caches` オブジェクトが存在するかどうかを確認し、存在する場合はそこから |
| 124 | +最新のデータをリクエストします。方法は、XHRが作られる前に、`app.getForecast` に |
| 125 | +次のコードを追加します。 |
| 126 | + |
| 127 | +{% highlight javascript %} |
| 128 | +if ('caches' in window) { |
| 129 | + caches.match(url).then(function(response) { |
| 130 | + if (response) { |
| 131 | + response.json().then(function(json) { |
| 132 | + // Only update if the XHR is still pending, otherwise the XHR |
| 133 | + // has already returned and provided the latest data. |
| 134 | + if (app.hasRequestPending) { |
| 135 | + console.log('updated from cache'); |
| 136 | + json.key = key; |
| 137 | + json.label = label; |
| 138 | + app.updateForecastCard(json); |
| 139 | + } |
| 140 | + }); |
| 141 | + } |
| 142 | + }); |
| 143 | +} |
| 144 | +{% endhighlight %} |
| 145 | + |
| 146 | +最後に、`app.hasRequestPending` フラグを更新します。それには、XHR の作成の前に |
| 147 | +`app.hasRequestPending = true;` を追加し、XHR の応答ハンドラで |
| 148 | +`app.updateForecastCard(response)` の直前に `app.hasRequestPending = false;` |
| 149 | +と設定します。 |
| 150 | + |
| 151 | +これで、お天気アプリでは、キャッシュから 1 回、XHR を介して 1 回、合計 2 回の |
| 152 | +非同期リクエストが行われるようになりました。キャッシュにデータが存在する場合は |
| 153 | +そのデータが返され、XHR からの応答がなければキャッシュ データが高速(10s/ms) |
| 154 | +に表示されてカードが更新されます。その後、XHR から応答があると、Weather API |
| 155 | +から直接取得した最新のデータを使ってカードが更新されます。 |
| 156 | + |
| 157 | +何らかの理由でキャッシュより早く XHR から応答があった場合は、`hasRequestPending` |
| 158 | +フラグにより、ネットワークの最新データにキャッシュ データが上書きされる事態が回避されます。 |
| 159 | + |
| 160 | +## テスト |
| 161 | + |
| 162 | +* コンソールで、更新のたびに 2 つのイベント(キャッシュからデータが取得されたことを |
| 163 | +示すイベントと、ネットワークからデータが取得されたことを示すイベント)が表示される |
| 164 | +ことを確認します。 |
| 165 | +* この時点で、アプリは完全にオフラインで動作するようになっています。開発用のサーバー |
| 166 | +を停止し、ネットワークの接続を切断して、アプリを実行してみてください。App Shell |
| 167 | +とデータの両方がキャッシュから配信されるようになります。 |
| 168 | + |
| 169 | +<a href="https://weather-pwa-sample.firebaseapp.com/step-07/" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">試す</a> |
0 commit comments