Service Worker触ってみた

この記事は[Gunosy Advent Calendar 2017](https://qiita.com/advent-calendar/2017/gunosy)の11日目の記事です。
昨日は@okataiさんの[DDLを使って0クリックから始めるパーソナライズ](https://qiita.com/okatai/items/2f0e691f84c86ec1b6ee)でした。

どーも広告技術部の@k_7016です。
普段はRails書いたりしてます。

皆さん、かなりちゃんと記事書いてて焦るところですが、空気読まずゆるふわやってみた系の記事書こうと思います。

最近話題のProgressive Web Appをやってみたいなと思いまして色々読んでたんですが範囲広くて爆死しそうになったのでとりあえず Service Woker 触ってみました。

## やること
Service Workerで

1. 静的ファイルキャッシュ
1. 動的リクエストキャッシュ

ができたらゴール。

## 対象サンプルアプリ
Reactでブログっぽいもの作ってfirebase に hosting しました。
この辺もやってみたいなーと思ってやってみた感じです。

## できたもの
- ソースコード: https://github.com/n-kurasawa/sandbox-blog
- 実際のサンプルアプリ: ~~https://blog-77588.firebaseapp.com/~~
~~アクセスするとService Workerが登録され、キャッシュされます。いやな人はChromeシークレットモードでアクセスすれば、ブラウザを閉じるとService Workerもキャッシュも削除されます。開発中便利。~~
(~~firebaseは多分そのうち落とします。~~)
落としました。(別のことに使ってます。)

## やったこと

### 1. service worker の登録
登録はこんな感じです。

```js
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(function(registration) {
    // 登録成功
    console.log('ServiceWorker registration successful with scope: ', registration.scope);
  }).catch(function(err) {
    // 登録失敗 :(
    console.log('ServiceWorker registration failed: ', err);
  });
}
```
1. まずブラウザが Service Worker に対応しているかチェック
    - 対応していない場合は何もしません。これも多分 Progressive Enhancement。
2. 対応していた場合、Service Workerのファイルを指定して登録
    - ```/sw.js```が用意したファイルです。
    - このファイルの配置位置は Service Worker のスコープを決めます。
    - ドメインのルートに置けばService Worker のスコープが origin 全体になり, origin全体で各イベントが取得できます。
3. あとは成功/失敗時のログを出しています。

<img width="1424" alt="スクリーンショット 2017-12-11 2.48.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/56898/a8c18980-e0aa-edd5-1811-80b93750a2cb.png">
登録されるとchrome developer tools の Applicationタブで確認できます。

### 2. 静的ファイルキャッシュ
Service Worker の処理は前述した /sw.js に書いていきます。(名前はなんでもいいです。)

```js:sw.js
var CACHE_NAME = 'cache-v1';
var urlsToCache = [
  '/',
  '/index.html',
  '/bundle.js',
  '/material.min.css',
  '/material.min.js',
  '/favicon.ico'
];

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});
```
1. ここでは Service Woker が登録された時に発火される```install```イベントを使います。
2. キャッシュ名を指定して open し、キャッシュするURLを指定します。
  - `/` と `/index.html` は同じものを取得しますが別々に指定する必要があります。

<img width="979" alt="スクリーンショット 2017-12-11 2.56.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/56898/8a355ec3-0d49-6095-5d36-cb3663e34e96.png">

キャッシュされたリクエストには歯車の印がつきます。

### 3. キャッシュされたレスポンスを返す

```js
self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // キャッシュがあったのでそのレスポンスを返す
        if (response) {
          return response;
        }
        return fetch(event.request);
      }
    )
  );
});
```
1. `fetch` イベントはリクエストがあった場合に発火し、 ```caches.match(event.request)``` でキャッスしているリクエストかどうかチェックできます。
2. キャッシュがある場合それを返し
3. キャッシュがない場合、そのまま `fetch(event.request)` でリクエストします。

<img width="1133" alt="スクリーンショット 2017-12-11 3.04.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/56898/c3bdd589-3e78-ad01-7f7f-6bcf086e3ef7.png">

Service Workerからキャッシュを返せた場合には Size 列に ```from ServiceWoker``` と表示されます。

### 4. 動的リクエストキャッシュ
リクエストをキャッシュしたい場合は上記と同じ fetch イベント内で以下のようにします。

```js
// 重要：リクエストを clone する。リクエストは Stream なので
// 一度しか処理できない。ここではキャッシュ用、fetch 用と2回
// 必要なので、リクエストは clone しないといけない
var fetchRequest = event.request.clone();

return fetch(fetchRequest).then(
  function(response) {
    // レスポンスが正しいかをチェック
    if(!response || response.status !== 200 || response.type !== 'basic') {
      return response;
    }

    // 重要：レスポンスを clone する。レスポンスは Stream で
    // ブラウザ用とキャッシュ用の2回必要。なので clone して
    // 2つの Stream があるようにする
    var responseToCache = response.clone();

    caches.open(CACHE_NAME)
      .then(function(cache) {
        cache.put(event.request, responseToCache);
      });

    return response;
  }
);
```
- request, response 共にcloneしているのは Stream は中身を1度しか使えない為です。
- ```response.type !== 'basic'```は 同じ origin からのリクエストということを表します
私のサンプルアプリではリクエストが cors だったので外してあります。

### 5. オフライン
以上のキャッシュのおかげで1度アクセスしたページはオフラインでも開くことができるようになります。

<img width="1434" alt="スクリーンショット 2017-12-11 3.19.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/56898/8472a79d-d990-db1f-11b0-1590c5d16f7d.png">

Applicationタブでオフラインにできます。オフラインにすると Networkタブに警告が出ます。

伝わるか微妙ですがオフラインでも動いてる図
<img width="1433" alt="スクリーンショット 2017-12-11 3.22.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/56898/2dfefc03-cb9c-d863-8023-013fcd410522.png">

### 6. その他: Service Workerのライフサイクルについて
Service Worker がインストールされ、 `fetch`等のイベントを受け取れるようになると `activate` イベントが発火されます。
ただし、デフォルトでは、ページリクエスト自体が Service Worker を通過するまでそのページの `fetch` イベント等は処理されません。
ややこしいですが、簡単に言うとはじめにService Workerを登録したページ(register('/sw.js')したページ)は、再度読み込むまで Service Workerの処理(fetchイベントの処理等)が走りません。
もしregisterしたページをすぐに有効にしたい場合は ```clients.claim()```を activate で実行すればいいです。

```js
self.addEventListener('activate', function(event) {
  clients.claim();
});
```

またService Workerが更新されると新しくinstallがされますが、古いService Workerが登録されている場合はすぐに activate されません。
古いService Worker が動いているブラウザを全て落ちるまで待つ必要があります。
開発中は Applicationタブの ```Upldate on reload``` にチェックを入れておくと install されるとすぐに更新されるので便利です。

<img width="1418" alt="スクリーンショット 2017-12-11 8.17.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/56898/69656f05-db29-52d4-e08e-ace08b5f2d16.png">



## まとめ
他にも細かい部分ありますがざっくりまとめるとこんな感じです。
入門する場合はデバッグが結構、肝な気がするので後述するデバッグの資料はみといた方がいいかもです。
あと、多分プロダクションで使う場合はライブラリとか使うと思うのでその辺調べた方が良さそう。

ちゃんと見てないけど[workbox](https://github.com/GoogleChrome/workbox) と言うやつですかね。
https://developers.google.com/web/tools/workbox/


全体的な感想としては

- firebase便利
- Reactとかのビルド環境自分で作るの大変
- もっと早く準備すべきでした

以上です。
ありがとうございました:bow:

## 参考資料
こちらを参考にさせていただきました。
[Service Worker の紹介](https://developers.google.com/web/fundamentals/primers/service-workers/)
[Service Worker のライフサイクル](https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle)
[Service Worker のデバッグ](https://developers.google.com/web/fundamentals/codelabs/debugging-service-workers/?hl=ja)