PWA： ServiceWorkerを使って、キャッシュをコントロールする(オフラインハンドリング)

## はじめに
前回の記事
(https://qiita.com/OMOIKANESAN/items/5b23fa8ea9ea0d181df5)
で、最小構成でPWAのWebサイトを作成する方法についてご紹介しました。
今回は、別のページを作成して、「オフラインのときの挙動を制御する」に取り組みました。
前回も、オフライン状態で動作するページを作成しましたが、複数のページにまたがるような、つまりは通常のWebサイトでの使い方には触れなかったので、今回はページを跨ったときのオフライン表示をコントロールしようというのが主題です。

### コードの概要

今回も前回同様に、なるべく最小構成でのオフラインハンドリングを目指しました。
すべてこの記事内に書くと冗長になってしまうため、GitHubへアップロードしましたので、適宜参照してください。

https://github.com/ktsh2017/pwa_offline_sample

トップページが目次になっていて、それぞれの花の名前を選択すると、選択した花の詳細画面に遷移し、タイトルとその花の画像が一枚表示されるサイトです。
ファイル数は増えていますが、キャッシュ取得済みでオフライン表示可能なものと、未キャッシュで表示できないものを確認するためにページを多めに作ったためです。
基本的な構成部分は、前回からほとんど増えていません。
見た目を整えるためにBootstrapは追加しましたが特別なことはしていません。

動作例はこちら

https://qiita-pwa.s3.ap-northeast-1.amazonaws.com/flowerbook/index.html

### コード解説

前回解説したものと、各花のページといったいくつかのファイルは省略します。

#### index.html

前回は、index.htmlにServiceWorkerを呼び出すためのJSを直接書いていましたが、main.jsへ括りだしました。
main.jsはそれだけなので、気にしなくて問題ないです。
main.cssもほぼ何もしていないので無視していただいて問題ないです。

``` HTML
<meta name="viewport" content="width=device-width,initial-scale=1">
```

これはスマホサイズでも見やすいように調整しただけです。
あとは各ページへのリンクです。

#### templete.html

下記のページはすべてこちらのテンプレートからコピーして作成しています。

* ajisai.html
* aster.html
* fujinohana.html
* himawari.html
* suzuran.html
* tulip.html
* whiterose.html
* yaezakura.html
* offline.html

#### offline.html

オフラインのときに、キャッシュからページデータを取得できなかったとき、このファイルが表示されます。
他の花のページと同じくtemplete.htmlから作ってあります。特別な変更はありません。

#### serviceWorker.js

やはり、このサービスワーカーが肝ですね。全行解説します。

ここは前回と同じでキャッシュのバージョン管理のための文字列です。
自分がわかれば任意でなんでも大丈夫です。

``` JavaScript
var CACHE_NAME  = "fb-cache-v8-10";
```

下記は少し新しい要素が増えています。
Bootstrapはスタイル用で、特に重要なのがoffline.htmlとfireflower.jpgです。
この2つがオフライン時に使用するリソースにあたります。

``` JavaScript
var urlsToCache = [
    "index.html",
    "favicon.ico",
    "https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js",
    "css/bootstrap.css",
    "js/bootstrap.js",
    "logo/logo192.png",
    "offline.html",
    "img/fireflower.jpg"
];
```

これは、本質には関係ないですが、開発時は頻繁にキャッシュを消すので、勝手に消えるように設定を追加しました。

``` JavaScript
const CACHE_KEYS = [
  CACHE_NAME
];
```

前記事と同じく、ServiceWorker読み込み時にインストールイベントで実行されます。

``` JavaScript
self.addEventListener('install', function(event) {
    event.waitUntil(
        caches.open(CACHE_NAME) // 上記で指定しているキャッシュ名
          .then(
          function(cache){
              return cache.addAll(urlsToCache); // 指定したリソースをキャッシュへ追加
              // 1つでも失敗したらService Workerのインストールはスキップされる
          })
    );
});
```

新しいバージョンがインストールされた際に、過去のバージョンを削除しています。
本質とは関係ないですが、これがあると手作業で過去のバージョンのキャッシュを消す手間が省けます。

``` JavaScript
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys => {
      return Promise.all(
        keys.filter(key => {
          return !CACHE_KEYS.includes(key);
        }).map(key => {
          // 不要なキャッシュを削除
          return caches.delete(key);
        })
      );
    })
  );
});
```

最後のFetchイベントの内容です。
これは長いので、コード内に書きます。
※不要な部分を少し削っています。

``` JavaScript
self.addEventListener('fetch', function(event) {
  //ブラウザが回線に接続しているかをboolで返してくれる
  var online = navigator.onLine;

  //回線が使えるときの処理
  if(online){
    event.respondWith(
      caches.match(event.request)
        .then(
        function (response) {
          if (response) {
            return response;
          }
          //ローカルにキャッシュがあればすぐ返して終わりですが、
          //無かった場合はここで新しく取得します
          return fetch(event.request)
            .then(function(response){
              // 取得できたリソースは表示にも使うが、キャッシュにも追加しておきます
              // ただし、Responseはストリームなのでキャッシュのために使用してしまうと、ブラウザの表示で不具合が起こる(っぽい)ので、複製しましょう
              cloneResponse = response.clone();
              if(response){
                //ここ&&に修正するかもです
                if(response || response.status == 200){
                  //現行のキャッシュに追加
                  caches.open(CACHE_NAME)
                    .then(function(cache)
                    {
                      cache.put(event.request, cloneResponse)
                      .then(function(){
                        //正常にキャッシュ追加できたときの処理(必要であれば)
                      });
                    });
                }else{
                  //正常に取得できなかったときにハンドリングしてもよい
                  return response;
                }
                return response;
              }
            }).catch(function(error) {
              //デバッグ用
              return console.log(error);
            });
        })
    );
  }else{
    //オフラインのときの制御
    event.respondWith(
      caches.match(event.request)
        .then(function(response) {
          // キャッシュがあったのでそのレスポンスを返す
          if (response) {
            return response;
          }
          //オフラインでキャッシュもなかったパターン
          return caches.match("offline.html")
              .then(function(responseNodata)
              {
                //適当な変数にオフラインのときに渡すリソースを入れて返却
                //今回はoffline.htmlを返しています
                return responseNodata;
              });
        }
      )
    );
  }
});
```

### 動作説明

トップページ(ServiceWorkerインストール) → fujinohanaページへ遷移(ふじのはなをキャッシュ) → トップページ(オフライン化) → 再びfujinohanaページへ遷移(キャッシュから表示) → トップページ → himawariページへ遷移(ひまわりのリソースが無いためオフライン表示)
※オンラインにしてひまわりのリソースを取得してから再度オフラインにして遷移すれば、キャッシュされたことがわかります。

トップページの表示
<img width="555" alt="初回起動.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/153290/e10a565c-8c1a-0cb7-05f7-a23d316b714c.jpeg">

ふじのはなを表示
<img width="523" alt="ふじのはなページへ遷移.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/153290/be0b23c9-39bf-4899-f5e6-9375c0af3952.jpeg">

一度トップへ戻って、オフラインにする
<img width="264" alt="オフラインモード.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/153290/e8570dcb-6331-0a71-c0aa-008874e5b743.jpeg">

オフライン状態でもふじのはなへの遷移は正常にできる
<img width="450" alt="オフラインふじのはな.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/153290/99e3d2d8-cdcf-50f6-da41-22cd45a73727.jpeg">


ただし、トップページから、まだ未到達でキャッシュされていない、ひまわりのページへ遷移すると、オフライン用の表示へ切り替わる
<img width="443" alt="ひまわりページへ遷移オフラインページ.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/153290/4f244704-5184-f968-1343-f644b3a4b758.jpeg">

オフライン時にめちゃめちゃにエラー出る・・・
(2回目のインデックス遷移時にも詳細把握出来ていないエラーが出ていますが、とりあえず動いてはいます)


## 考察

本稿で実装したコードで、オンラインのときに必要なリソースをローカルに貯めて、オンライン時にも、オフラインのときでもコンテンツを提供できるようになりました。
しかし、高速化の観点から言うとディスクキャッシュとほぼ同じじゃないかと思う部分もあります。
このくらいのシンプルなページ構成の場合は特にそれが顕著に感じますが、もう少し多くのリソースが存在するサービスで、それぞれの種類に合わせて必ずサーバーからリソースを取得するもの、なるべくオフラインからリソースを取得するものという形で、エンジニアがキャッシュを細かくコントロールしてあげられれば、現状よりも効率よく表示できるページが作れるのではないかと思います。
不必要にキャッシュを多用してもそれほど効果がないパターンも考えられますので、そのPWAに適したコントロールが必要になりそうです。

## 開発時の便利知識

シェルフに追加でPWA化したアプリをPCへインストールした後、インストールしたものの実態がどこにあるかわからず、アンインストール出来ずに困った場面がありました。
そんなときは、
chrome://apps/
にアクセスすれば、Chromeでインストールしたアプリの一覧が見れるので、右クリックしてChromeから削除してしまいましょう。
これで、何度でもインストール可能です。
(前回の記事に書いたほうがよかったですね)

# おわりに

エンジニアが適切にキャッシュをコントロールすることができれば、クライアントの端末だけでなく、サーバー側の負荷も軽減され、トータルでパフォーマンスが上がることも視野に入ります。
完全にPWAにせずとも、ServiceWorkerによって、キャッシュをコード上で扱えることを覚えておくと、役に立つ時が来るかもしれません。
次回以降は、ローカルストレージや端末側のDB機能の使用、プッシュ通知等のPWAで使える機能に触れてみようと思います。

以上