WorkboxでPWAを導入する方法～オフライン対応編～


## 目標：ServiceWorkerにキャッシュを保持して、とりあえずオフラインでも表示できるようにする。
- PWAやってみたい
- ページスピードあげたい

### はじめに
- そもそもサービスワーカーって何!?という方はこちらの記事が分かりやすかったです。

> 参考：[Service Workerってなんなのよ (Service Workerのえほん)](https://qiita.com/riocampos/items/43b8c47e14d63dc6f6a4)

実装するにあたり[Workbox](https://developers.google.com/web/tools/workbox/)というGoogleのライブラリを使います。PWAのライブラリというよりはサービスワーカーの設定が簡単にできるライブラリという認識です。（※そのためホーム画面追加やプッシュ通知はこのライブラリには含まれません。多分。）
今回は最も簡単な方法をご説明したいので、GulpやWorkboxCLI等のビルトツールは使いません。キャッシュしたい（オフライン対応させたい）リソースを地道に書いていきます。（別の記事で書きます）

### Serviceworkerのキャッシュには二種類ある
1. プリキャッシュ
2. ランタイムキャッシュ

実際にやってみた感想（自分の言葉で言うと）

1. プリキャッシュ・・・ヘッダー・フッター等どのページでも変わらないもの。アップシェル
2. ランタイムキャッシュ・・・ネットワークを介してページを表示した時に、併せてキャッシュもしてくよ、というもの。キャッシュの有効期間、ファイル数、戦略（キャッシュファースト、ネットワークファースト等）を指定したいもの。

> 参考：[PWA: Workbox Precaching 和訳 (その１)](https://qiita.com/stoneshower/items/0c1ea518197b2b700383)

#### プリキャッシュの書き方
```javascript:sw.js
workbox.precaching.precacheAndRoute([
  './plugin/jquery/jquery-3.3.1.min.js',
  '/js/main.js',
  './cmn/header.css',
  './cmn/footer.css',
  './images/mv.jpg',
  'manifest.json',
]);
```

#### ランタイムキャッシュの書き方（シンプル版）
```javascript:sw.js
workbox.routing.registerRoute(
  new RegExp('index.html'),
  new workbox.strategies.NetworkFirst()
);

workbox.routing.registerRoute(
  new RegExp('js/test.js'),
  new workbox.strategies.StaleWhileRevalidate()
);
```
#### 絶対パスも書けばキャッシュできる（スコープのドメインと異なっていてもOK）
```javascript:sw.js
workbox.routing.registerRoute(
  new RegExp('https://e.his-j.com/js/test.js'),
  new workbox.strategies.StaleWhileRevalidate()
);
```

#### ランタイムキャッシュの書き方（色々設定版）
```javascript:sw.js
workbox.routing.registerRoute(
  new RegExp('./cmn/icon/.*'),
  new workbox.strategies.CacheFirst({
    cacheName: 'top-icon',
    maxEntries: 50,
    plugins: [
      new workbox.expiration.Plugin({
        maxAgeSeconds: 60 * 60 * 24, // 1day
        purgeOnQuotaError: true
      }),
    ],
  })
);
```
| オプション | 内容 | 
|:------------:|:------------:|
| cacheName | キャッシュストレージにキャッシュされるときの名前。想定したリソースがキャッシュされているか、名前をつけて分類できる |
| maxEntries | キャッシュエントリ数（キャッシュする数） |
| maxAgeSeconds | キャッシュされたエントリの最大経過時間 |
| purgeOnQuotaError | 「Webアプリが使用可能なストレージを超えた場合に、指定されたキャッシュを自動的に削除しても安全であるとマークできます。」（[引用](https://developers.google.com/web/tools/workbox/guides/storage-quota)） |

cacheName（イメージ）
![cache_name.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271161/0130c4fe-f8c1-55a5-d4c3-8e35409f0801.png)


他にも色々あるようですが、
とりあえず、これでキャッシュの設定は終わりです。



#### 一番上にworkboxをインストール
```javascript:sw.js
importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');
```


#### オフライン化したいhtmlの下部あたりにサービスワーカーを起動させる記述を追加します。
```html:index.html
<script>
  window.addEventListener('load', function () {
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register("sw.js")
        .then(function (registration) {
          console.log("sw registed.");
        }).catch(function (error) {
          console.warn("sw error.", error);
        });
    }
  });
</script>
```

#### オフライン化したいスコープのルートディレクトリにmanifest.jsonを置きます。
```json:manifest.json
{
  "name": "H.I.S.",
  "short_name": "H.I.S.",
  "theme_color": "#004098",
  "background_color": "#004098",
  "display": "minimal-ui",
  "scope": "/",
  "start_url": "kanto_sp.html?debug=pwa&cid=top_homescreen&utm_source=top_homescreen&utm_medium=homescreen&utm_campaign=top_homescreen",
  "icons": [
    {
      "src": "/cmn/icon/pwa/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/cmn/icon/pwa/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "/cmn/icon/pwa/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "/cmn/icon/pwa/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "/cmn/icon/pwa/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "/cmn/icon/pwa/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/cmn/icon/pwa/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/cmn/icon/pwa/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "splash_pages": null
}
```



### サービスワーカーが動いているかの確認方法
デベロッパーツールのNetworkタブのsizeを確認。
ページを開くと、一回目はサービスワーカーからリソースを返していない。
更新すると、通常○○KBと書いてあるところが「ServiceWorker」と書かれている。
![キャプチャ01.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271161/678b2621-a144-5b54-2132-1ae5957b96a6.png)



上部のOfflineにチェックして、更新すると一部リソースは取得できてませんが、
オフラインでも表示されました！！
jQueryもインストールしているので、jsで動かしている部分も動きます。
![キャプチャ02.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271161/274d20c3-f37b-7bb8-c0c9-7dd1a14c07d8.png)



### ソース全容例
```javascript:sw.js
importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');

workbox.core.skipWaiting();

workbox.core.clientsClaim();

workbox.navigationPreload.enable();

// ------------------  runtime caching starts ---------------------
// frequently updated resources
workbox.routing.registerRoute(
  /kanto\/json/,
  workbox.strategies.networkFirst({
    cacheName: 'fetch-objects-cache',
  }),
  'GET'
);

// html
workbox.routing.registerRoute(
  new RegExp('kanto_sp.html'),
  new workbox.strategies.NetworkFirst()
);

// banner imgages
workbox.routing.registerRoute(
  new RegExp('./images/.*'),
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'images-',
    maxEntries: 50,
  })
);
workbox.routing.registerRoute(
  new RegExp('./kokunai/images/.*'),
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'kokunai-images',
    maxEntries: 50,
  })
);

// product
workbox.routing.registerRoute(
  new RegExp('https://e.his-j.com/images/'),
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'e.-images',
    maxEntries: 50,
  })
);
workbox.routing.registerRoute(
  new RegExp('https://e.his-j.com/js/ciao/common/minmax/.*'),
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'e.-minmax',
    maxEntries: 2,
  })
);
workbox.routing.registerRoute(
  new RegExp('https://www.his-j.com/kokunai/kanto/photo/item/.*'),
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'kokunai-product-images',
    maxEntries: 20,
  })
);

// other images
workbox.routing.registerRoute(
  new RegExp('/cmn/icon/.*'),
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'top-icon',
    maxEntries: 50,
     plugins: [
       new workbox.expiration.Plugin({
         maxAgeSeconds: 60 * 60 * 24, // 1day
         purgeOnQuotaError: true
       }),
     ],
  })
);
workbox.routing.registerRoute(
  new RegExp('/cmn/content/images/.*'),
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'top-content-images',
    maxEntries: 40,
  })
);

// index page content
workbox.routing.registerRoute(
  new RegExp('/cmn/content/js/.*'),
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'top-content-js',
    maxEntries: 5,
  })
);

// manifest
workbox.routing.registerRoute(
  new RegExp('manifest.json'),
  new workbox.strategies.StaleWhileRevalidate()
);



// ------------------  precaching the assets ---------------------
const OFFLINE_PAGE = '/tyo/pwa/offline.html';
workbox.precaching.precacheAndRoute([
  // js
  '/cmn/plugin/jquery/jquery-3.3.1.min.js',
  '/cmn/plugin/vue/vue.js',
  '/cmn/plugin/cookie_js/jquery.cookie.js',
  '/cmn/plugin/slick/slick.min.js',
  '/cmn/parts/js/parts.js',
  '/cmn/content/js/content.js',
  '/cmn/content/js/lazyload.js',
  '/cmn/headfoot/incl/kanto_header_sp.js',
  '/cmn/headfoot/incl/kanto_footer_sp.js',
  '/cmn/headfoot/js/headfoot_sp.js',
  '/cmn/headfoot/js/headfoot_login.js',
  '/cmn/kanto/js/kanto_cookie.js',
  '/headfoot/js/number_stores.js',
  '/cmn/plugin/polyfill/intersection-observer.js',
  '/cmn/plugin/polyfill/fetch.umd.js',
  '/cmn/plugin/polyfill/es6-promise.min.js',
  '/cmn/js/gt_info.js',
  '/cmn/js/recently_check.js',
  '/js/ppz_draw_pc_34006.js',
  '/js/ppz_34006.js',
  '/kanto/json/template_sp.js',
  '/kanto/incl/incl_index_sp.js',
  '/cmn/content/js/resource_hints.js',
  'call_sw.js',
  // css
  '/cmn/headfoot/css/headfoot_sp.css',
  '/cmn/css/common.css',
  '/cmn/parts/css/parts.css',
  '/cmn/content/css/content.css',
  '/cmn/plugin/slick/slick.css',
  './css/page_index.css',
  //img
  './images/kanto/mv_sss_sp.jpg',
  '/cmn/content/images/product_panel_icon_panel_arrow_kaigai.png',
  '/cmn/content/images/product_panel_icon_panel_arrow_kokunai.png',
  '/cmn/content/images/feature_message_sp.png',
  '/cmn/content/images/business_list_thumb_01.png',
  '/cmn/content/images/feature_item_01.jpg',
  '/cmn/content/images/feature_item_02.jpg',
  '/cmn/content/images/feature_item_03.jpg',
  '/cmn/content/images/feature_message_sp.png',
  '/cmn/parts/images/go_to_button_sp.png',
  '/cmn/headfoot/images/icon-phone-kaigai.png',
  '/cmn/headfoot/images/icon-phone-kokunai.png',
  '/cmn/headfoot/images/sprite_common_sp.png',
  // other
  'manifest.json',
  OFFLINE_PAGE,
]);

```



### ページが更新されない!？とならないように注意すべき点
一番注意すべきことは、**一回でもネットワークを介すかどうか。** 一回もネットワークを介さない可能性があると何回ページを更新しても修正したものが反映されないという事態になります。 
CacheFirstは注意が必要です！CacheFirstは発音的に、キャッシュに最初にアクセスして、なければネットワークから取得という感じですが、注意すべき点はキャッシュがあった場合はキャッシュから取得してネットワークにからとってくることはありません。つまり情報が古くてもキャッシュがあればキャッシュしか表示されません。

#### おすすめ：
**StaleWhileRevalidate**（キャッシュを表示させつつ、裏でネットワークから最新のものを取得。差分があればキャッシュし、次回から最新のものを表示する。）

怖ければ迷わず**NetworkFirst**（ネットワークが繋がらなければキャッシュから取ってくる。繋がればネットワーク優先）を設定しましょう。

キャッシュ戦略（Cache Strategies）は以下のページから確認しておきましょう。
> 参考：[Workbox Strategies](https://developers.google.com/web/tools/workbox/modules/workbox-strategies)

---
## 参考
- [Service Workerってなんなのよ (Service Workerのえほん)](https://qiita.com/riocampos/items/43b8c47e14d63dc6f6a4)
- [PWA: Workbox Precaching 和訳 (その１)](https://qiita.com/stoneshower/items/0c1ea518197b2b700383)
- [Workbox Cache Expiration](https://developers.google.com/web/tools/workbox/modules/workbox-cache-expiration)
- [Understanding Storage Quota](https://developers.google.com/web/tools/workbox/guides/storage-quota)
- [Workbox Strategies](https://developers.google.com/web/tools/workbox/modules/workbox-strategies)