Service Workerの基本とそれを使ってできること

# Service Workerとは
ブラウザが Web ページとは別にバックグラウンドで実行するスクリプト
オフラインのアプリを実現・サポートするために作られたものです

ちなみに、ブラウザの対応状況はこんな感じ
http://caniuse.com/#search=service%20workers

# 特徴

 - DOM にアクセスできない
  - DOM を操作したい場合は、Service Worker がコントロールしているページ(js)と postMessage でメッセージのやり取りをして行う
 - リクエストをプロキシすることが可能
 - Service Worker はブラウザが必要に応じて起動・終了するので、変数の値を保持しておけない
  - Cache、IndexedDB 等で値を保存して、必要になった時に取り出すようにする
 - Promise を多用する
 - https か localhost 上でしか動作しない

# ライフサイクル

Web ページとは全く異なるライフサイクルで動作する

![ServiceWorkerLifeCycle.png](https://qiita-image-store.s3.amazonaws.com/0/54850/3d998a7a-d0c1-bc87-cc27-645c6d92d34b.png)

赤文字はその時に発火するイベント

## 登録

Service Worker を使うにはまず `register()`関数を呼びだして登録する
すでに登録されているかどうかはブラウザがチェックしてくれるので気にせず呼べばいい

```js
navigator.serviceWorker.register('/service-worker.js');
```

登録時に重要なのが**スコープ**
スコープとは Service Worker がコントロールするページのこと
スコープは、service-worker.js が存在する階層が自動的に設定される
また、下記のように明示的に設定することも可能

```js
navigator.serviceWorker.register('/service-worker.js', {scope: '/example'});
```

上記の場合、/example 配下のページが Service Worker にコントロールされる
つまり、「 http://www.example.com/example/page.html 」はコントロールされるが
「 http://www.example.com/index.html 」はコントロールされないということ

また、`register()`で登録し service-worker.js が更新されている場合は`onupdatefound`イベントが発火する（初回登録時も）

## インストール

Service Worker を新規インストール、もしくは、Service Worker が更新されていると installing 状態になる
この時、oninstall イベントも発火する
インストール時に何か処理させたい場合は、下記のように Service Worker 内で oninstall イベントを監視する

```service-worker.js
self.addEventListener('install', (event) => {
// 何かの処理
});
```

## 更新

ブラウザが保持している Service Worker と、これからダウンロードしようとしている Service Worker に
1byteでも違いがあれば更新されたと判断される

ブラウザをコントロールしている Service Worker が存在しなければ installing 状態のあと
すぐに、active 状態になるが、コントロールしている Service Worker が存在する場合は
waiting 状態に移行する

<img width="361" alt="ServiceWorker更新時.png" src="https://qiita-image-store.s3.amazonaws.com/0/54850/70b54747-f07d-da9b-6b90-2fafa0124d6b.png">

なぜすぐに active 状態にしないかというと、古い Service Worker が保持しているデータを
そのまま新しい Service Worker が使おうとした場合、不整合が起きる可能性があるからです
その後、安全な状態（ページが閉じられるなど）になると waiting 状態から active 状態に移行します

# Tips

## 更新時に installing 状態からすぐに active 状態に移行させたい

Service Worker 更新時はデータの不整合等を防ぐために waiting 状態に移行し
安全な状態になるのを待つようになっている

すぐに active 状態にしても問題ない場合は、`skipWaiting()`を呼ぶことで active 状態にすることができる

```service-worker.js
self.addEventListener('install', (event) => {
  event.waitUntil(self.skipWaiting());
});
```

`waitUntil()`はこの関数が呼ばれたイベント終了のライフタイムをその処理が終わるまで待つ
（この関数はよく使うので覚えておくといいです）

## active 状態になったらすぐにコントロールさせたい

Service Worker は active 状態になってもすぐにブラウザをコントロールしない
コントロールするのは次にページが表示された時

`claim()`を呼ぶことすぐにコントロールさせることができる

```service-worker.js
self.addEventListener('activate', (event) => {
  event.waitUntil(self.clients.claim());
});
```

---

ここまで、Service Worker の基本を説明してきました
ここからは Service Worker を使ってできることを説明していきます

サンプルコードは[ここ](https://github.com/kanatapple/service-worker)にあるので見てください
動作は、Google Chrome 52.0.2743.116 m (64-bit)で確認済みです

# Cache編
Cache API を使用することでオフライン状態でもリソースを取得できるようにします

動作サンプルは[こちら](https://kanatapple.github.io/service-worker/cache/)
一度オンライン状態でページを取得し、その後オフライン状態で再読み込みしてみてください
オフライン状態でもリソースが取得できることが確認できるはずです

## ページ

```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Cache Test</title>
    <link rel="stylesheet" href="./styles/main.css">
</head>
<body>
    <h1>Service Worker Cache Test</h1>
    <img src="./images/image.jpg">
    <script src="./script/jquery-3.1.0.js"></script>
    <script src="./script/main.js"></script>
</body>
</html>
```

CSS、イメージ、スクリプトのリクエストをするだけの簡単なページです
main.js は Service Worker を登録するスクリプトです

## Service Worker 登録スクリプト

```main.js
navigator.serviceWorker.register('./service-worker.js')
                       .catch(console.error.bind(console));
```

Service Worker の登録を行うスクリプトです
register で登録をしてエラーがあった時はコンソール表示するだけの簡単なものです

## Service Worker

ちょっと長いです
いったん、全ソースを紹介して、その後細かく説明していきます

```service-worker.js
'use strict';

const CACHE_NAME = 'cache-v1';
const urlsToCache = [
    './',
    './styles/main.css',
    './images/image.jpg',
    './script/main.js'
];

self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(CACHE_NAME)
              .then((cache) => {
                  console.log('Opened cache');
                  
                  // 指定されたリソースをキャッシュに追加する
                  return cache.addAll(urlsToCache);
              })
    );
});

self.addEventListener('activate', (event) => {
    var cacheWhitelist = [CACHE_NAME];

    event.waitUntil(
        caches.keys().then((cacheNames) => {
            return Promise.all(
                cacheNames.map((cacheName) => {
                    // ホワイトリストにないキャッシュ(古いキャッシュ)は削除する
                    if (cacheWhitelist.indexOf(cacheName) === -1) {
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
});

self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request)
              .then((response) => {
                  if (response) {
                      return response;
                  }
            
                  // 重要：リクエストを clone する。リクエストは Stream なので
                  // 一度しか処理できない。ここではキャッシュ用、fetch 用と2回
                  // 必要なので、リクエストは clone しないといけない
                  let fetchRequest = event.request.clone();
            
                  return fetch(fetchRequest)
                      .then((response) => {
                          if (!response || response.status !== 200 || response.type !== 'basic') {
                              return response;
                          }
                    
                          // 重要：レスポンスを clone する。レスポンスは Stream で
                          // ブラウザ用とキャッシュ用の2回必要。なので clone して
                          // 2つの Stream があるようにする
                          let responseToCache = response.clone();
                    
                          caches.open(CACHE_NAME)
                                .then((cache) => {
                                    cache.put(event.request, responseToCache);
                                });
                    
                          return response;
                      });
              })
    );
});
```

### install時

```js
const CACHE_NAME = 'cache-v1';
const urlsToCache = [
    './',
    './styles/main.css',
    './images/image.jpg',
    './script/main.js'
];

self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(CACHE_NAME)
              .then((cache) => {
                  // 指定されたリソースをキャッシュに追加する
                  return cache.addAll(urlsToCache);
              })
    );
});
```

CACHE_NAME はキャッシュに保存する時の名前です
<img width="237" alt="Cacheキャッシュ名１.png" src="https://qiita-image-store.s3.amazonaws.com/0/54850/a514597c-bbb7-9470-b1ef-40b15d4c7bc7.png">
DevTools の Application タブを開くと左側に Cache という項目があり
↑で指定した名前で保存されていることがわかります

CacheStorage.open('キャッシュ名')でキャッシュを開き
urlsToCache で指定されているリソースを Cache.addAll でキャッシュに保存します

### activate時

```js
self.addEventListener('activate', (event) => {
    var cacheWhitelist = [CACHE_NAME];

    event.waitUntil(
        caches.keys().then((cacheNames) => {
            return Promise.all(
                cacheNames.map((cacheName) => {
                    // ホワイトリストにないキャッシュ(古いキャッシュ)は削除する
                    if (cacheWhitelist.indexOf(cacheName) === -1) {
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
});
```

この activate 時の処理ですが、新規インストール時は何も処理されません

ここでは、キャッシュ名が`cache-v2`になった時の処理を説明します

1. キャッシュ名が変わったので Service Worker が更新されたと判断され、install イベントが発火します  
そして、キャッシュ名`cache-v2`で保存されます  
<img width="233" alt="Cacheキャッシュ名２.png" src="https://qiita-image-store.s3.amazonaws.com/0/54850/43404cf7-78b5-03b9-44af-d6229f5042f6.png">  
このようにキャッシュが２つある状態になります
2. 安全な状態になり、再度ページを開いた時に activate イベントが発火します  
`CACHE_NAME`は`cache-v2`になっているので`cacheWhitelist`には`cache-v2`が設定されます  
`cache.keys`によって保存されているキャッシュの名前が列挙され、ホワイトリストにない`cache-v1`（古いキャッシュ）が削除されます  
<img width="229" alt="Cacheキャッシュ名３.png" src="https://qiita-image-store.s3.amazonaws.com/0/54850/b214ee5b-33bb-1e25-8e47-df52a46844d7.png">  
↑`cache-v1`が削除されていることがわかる

### fetch時

```js
self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request)
              .then((response) => {
                  if (response) {
                      return response;
                  }
            
                  // 重要：リクエストを clone する。リクエストは Stream なので
                  // 一度しか処理できない。ここではキャッシュ用、fetch 用と2回
                  // 必要なので、リクエストは clone しないといけない
                  let fetchRequest = event.request.clone();
            
                  return fetch(fetchRequest)
                      .then((response) => {
                          if (!response || response.status !== 200 || response.type !== 'basic') {
                              return response;
                          }
                    
                          // 重要：レスポンスを clone する。レスポンスは Stream で
                          // ブラウザ用とキャッシュ用の2回必要。なので clone して
                          // 2つの Stream があるようにする
                          let responseToCache = response.clone();
                    
                          caches.open(CACHE_NAME)
                                .then((cache) => {
                                    cache.put(event.request, responseToCache);
                                });
                    
                          return response;
                      });
              })
    );
);
```

Service Worker がブラウザをコントロールしている時にリソースのリクエストが発生すると
fetch イベントが発火します
fetch イベントで何もしなければ、普通にネットワーク経由でリクエストが処理されます

ここで行っていることは単純で、リクエストが来たリソースがキャッシュに保存されていればそれを返し、なければサーバーにリクエストを投げ、返って来たリソースをキャッシュに保存しつつ、クライアントに返しているだけです
install 時にキャッシュに保存していなくても、こうすることでキャッシュすることが可能です

重要なことは、リクエストとレスポンスはStreamなので使い回すことができないようです
フェッチしたり、キャッシュしたりする場合は clone する必要があるようです

Cacheについての説明はこんな感じです
実際に、オフライン状態でページを読み込んでみると
<img width="756" alt="CacheDevTools.png" src="https://qiita-image-store.s3.amazonaws.com/0/54850/45ec82e9-671d-0fed-fb67-72fc99e80f3a.png">
Service Worker を使ってリソースを取得できてるのがわかります

# Push Notification編

ネイティブアプリのようにプッシュ通知をブラウザに送る方法を説明します

プッシュ通知を試したい場合は、[ここ](https://github.com/kanatapple/service-worker)にあるリポジトリをクローンし
下記を参考に適宜修正してください
修正が必要なのは、manifest.json の gcm_sender_id と push.js の APIキー、エンドポイントなどです

## ページ

```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Push Test</title>
    <link rel="manifest" href="./manifest.json">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/primer/2.0.2/primer.css">
</head>
<body>
    <h1>Service Worker Push Test</h1>
    <dl class="form">
        <dt><label>Endpoint URL</label></dt>
        <dd><textarea id="subscription-endpoint" class="input-block"></textarea></dd>
        <dt><label>Auth</label></dt>
        <dd><textarea id="subscription-auth" class="input-block"></textarea></dd>
        <dt><label>Public Key</label></dt>
        <dd><textarea id="subscription-public-key" class="input-block"></textarea></dd>
    </dl>
    <script src="./script/main.js"></script>
</body>
</html>
```

ページは manifest.json の読み込み以外、大したことはしていません
プッシュ通知に必要な情報を表示するための要素があるぐらいです

## manifest.json

```manifest.json
{
  "name": "SW Push Notification",
  "short_name": "SW Push Notification",
  "icons": [{
    "src": "https://kanatapple.github.io/service-worker/push/images/image.jpg",
    "sizes": "192x192",
    "type": "image/jpg"
  }],
  "start_url": "./index.html",
  "display": "standalone",
  "gcm_sender_id": "621388437768"
}
```

プッシュ通知する際は必要なので用意します(詳細は[ここ](https://developer.mozilla.org/ja/docs/Web/Manifest)を参考に)
重要なのは`gcm_sender_id`です
`gcm_sender_id`には後述する GCM(Google Cloud Messaging)、FCM(Firebase Cloud Messaging)を使用するプロジェクトID（送信者ID）を指定します


## Service Worker 登録スクリプト

今回のサンプルではメッセージをペイロードするために必要な情報を表示するようにしています
メッセージをペイロードする必要がない時は、エンドポイントだけでOKです

```main.js
document.addEventListener('DOMContentLoaded', () => {
    let endpoint = document.querySelector('#subscription-endpoint');
    let key = document.querySelector('#subscription-public-key');
    let auth = document.querySelector('#subscription-auth');
    
    navigator.serviceWorker.register('./service-worker.js');
    navigator.serviceWorker.ready
             .then((registration) => {
                 return registration.pushManager.subscribe({userVisibleOnly: true});
             })
             .then((subscription) => {
                 var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
                 key.value = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
        
                 var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
                 auth.value = rawAuthSecret ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';
        
                 endpoint.value = subscription.endpoint;
                 console.log(`GCM EndPoint is: ${subscription.endpoint}`);
             })
             .catch(console.error.bind(console));
}, false);
```

重要なのは `registration.pushManager.subscribe({userVisibleOnly: true})`
subscribe することでプッシュ通知を購読するようになります

subscribe には `{userVisibleOnly: true}` を渡す必要があると記載があったんですが
false で渡しても、引数を渡さなくても通知されるので、ちょっと謎です・・
(何かわかったら追記します)

subscribe が終わったら、公開鍵、鍵生成に使用する乱数、エンドポイントを画面上に表示してます
これらの値をプッシュ通知の際に使用します

## Service Worker

```service-worker.js
/*
    fetch以外は省略。リポジトリのコードを見てください
*/

self.addEventListener('push', (event) => {
    console.info('push', event);
    
    const message = event.data ? event.data.text() : '(・∀・)';
    
    event.waitUntil(
        self.registration.showNotification('Push Notification Title', {
            body: message,
            icon: 'https://kanatapple.github.io/service-worker/push/images/image.jpg',
            tag: 'push-notification-tag'
        })
    );
});
```

メッセージをペイロードしている場合は、event.data にデータが入っているので
`text()`を呼ぶとメッセージを取得できます

あとは、`showNotification`に必要なデータを渡すと通知が表示されます

![プッシュ通知.png](https://qiita-image-store.s3.amazonaws.com/0/54850/5f488fa8-54cc-1829-d07d-e85d8aae5e81.png)

`showNotification`のシグネチャはこんな感じ

```
Promise<void> showNotification(DOMString title, optional NotificationOptions options);

dictionary NotificationOptions {
  NotificationDirection dir = "auto";
  DOMString lang = "";
  DOMString body = "";
  DOMString tag = "";
  USVString icon;
  USVString badge;
  USVString sound;
  VibratePattern vibrate;
  DOMTimeStamp timestamp;
  boolean renotify = false;
  boolean silent = false;
  boolean noscreen = false;
  boolean requireInteraction = false;
  boolean sticky = false;
  any data = null;
  sequence<NotificationAction> actions = [];
};
```

この辺を参考に
https://notifications.spec.whatwg.org/#dom-serviceworkerregistration-getnotificationsfilter
https://developer.mozilla.org/ja/docs/Web/API/ServiceWorkerRegistration/showNotification

## プロジェクト登録

プッシュ通知を送信する場合は、Google Developers Console、Firebase Console に
プロジェクトを作成する必要があります
現在は Firebase の使用を推奨してるらしいですが、両方説明しときます

### Google Developers Console

https://console.developers.google.com/

１．左上のプロジェクト作成を選択
![Googleプロジェクト作成１.png](https://qiita-image-store.s3.amazonaws.com/0/54850/e57a476f-0355-387b-c28d-10634518a2e2.png)

２．プロジェクト名を設定して、作成
![Googleプロジェクト作成２.png](https://qiita-image-store.s3.amazonaws.com/0/54850/f6e62ad2-bac0-485e-2aef-7ea7551db5b0.png)

３．APIの有効化
![GoogleAPI１.png](https://qiita-image-store.s3.amazonaws.com/0/54850/79459874-123d-aab4-3111-88f24c75645f.png)
ライブラリから「Google Cloud Messaging」を探して

![GoogleAPI２.png](https://qiita-image-store.s3.amazonaws.com/0/54850/bde90863-e7d1-c68d-45bc-5559eaf9bc19.png)
有効化します

４．認証情報の作成
![Google認証情報作成１.png](https://qiita-image-store.s3.amazonaws.com/0/54850/2fb30ec3-fcd9-1143-0569-fa7483304b60.png)
プロジェクトを使用するには認証情報が必要らしいので、「認証情報に進む」をクリック

![Google認証情報作成２.png](https://qiita-image-store.s3.amazonaws.com/0/54850/2df1ee18-1c26-d00f-283d-c817708267f8.png)
APIを呼び出す場所で「ウェブブラウザ(Javascript)」を選択して

![Google認証情報作成３.png](https://qiita-image-store.s3.amazonaws.com/0/54850/e7fac683-e3d1-83db-4235-3ff74d3374bc.png)
APIキーに適当な名前を付けて「APIキーを作成する」をクリック

**完了**

### Firebase

https://console.firebase.google.com/

Firebase の場合、新規作成するか Google Developers Console に作成したプロジェクトをインポートすることができます
今回は新規作成を説明します

１．プロジェクト作成をクリック
![Firebaseプロジェクト作成１.png](https://qiita-image-store.s3.amazonaws.com/0/54850/907154d5-62b8-cfd0-db48-a2397ab741a2.png)

２．プロジェクト名を設定して、作成
![Firebaseプロジェクト作成２.png](https://qiita-image-store.s3.amazonaws.com/0/54850/b538f00c-6930-5626-c130-f699f4df415a.png)

**完了**

Firebaseで作成すると、プロジェクトを作成すると
自動的にクラウドメッセージング用のAPIと送信者IDを作成してくれるので簡単です

## 通知を送信

それでは実際に通知を送信してみます
通知には送信者IDとAPIキーが必要なので、Google Developers Console、Firebase で調べます

### Google Developers Console

#### 送信者ID

![Google送信者ID.png](https://qiita-image-store.s3.amazonaws.com/0/54850/8ce0ae81-241e-1e9c-252b-ef8ab9b6d3d1.png)

#### APIキー

![GoogleAPIキー.png](https://qiita-image-store.s3.amazonaws.com/0/54850/e9391552-30bb-2dfa-6d3e-027166122384.png)

### Firebase

#### 送信者IDとAPIキー

![Firebase情報.png](https://qiita-image-store.s3.amazonaws.com/0/54850/1fe5d78a-129b-4b9f-b4d2-b3c89a0db778.png)

### manifest.jsonの修正

`gcm_sender_id`に送信者IDを設定します

```manifest.json
{
  "name": "SW Push Notification",
  "short_name": "SW Push Notification",
  "icons": [{
    "src": "https://kanatapple.github.io/service-worker/push/images/image.jpg",
    "sizes": "192x192",
    "type": "image/jpg"
  }],
  "start_url": "./index.html",
  "display": "standalone",
  "gcm_sender_id": "118577160855"
}
```

### 送信

今回、送信用のライブラリに[web-push](https://github.com/web-push-libs/web-push)を使います
push.js を用意します

```push.js
'use strict';

const push = require('web-push');

const GCM_API_KEY = '********';
push.setGCMAPIKey(GCM_API_KEY);

const data = {
    'endpoint': '********',
    'userAuth': '********',
    'userPublicKey': '********'
};

push.sendNotification(data.endpoint, {
    payload:       'push test for service worker',
    userAuth:      data.userAuth,
    userPublicKey: data.userPublicKey,
})
    .then((result) => {
        console.log(result);
    })
    .catch((err) => {
        console.error('fail', err);
    });
```

`GCM_API_KEY`に上記で調べたAPIキーを記述します
`endpoint`、`userAuth`、`userPublicKey`にページを表示した時に表示される情報を設定します

設定が終了したら下記コマンドを実行します

```
node push.js
```

![プッシュ通知.png](https://qiita-image-store.s3.amazonaws.com/0/54850/29a991b3-7c91-0c5a-2d7b-5adade075ac7.png)

こんな感じで通知が来ます

# Background Sync編

オフライン時にデータを保持しておいて、オンラインになった時にバックグラウンドでデータを送信する方法を説明します

[デモ動画](https://youtu.be/l4e_LFozK2k)

今回はどういう仕組みで動くのかだけ説明します

## ページ

```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Sync Test</title>
</head>
<body>
    <h1>Service Worker Sync Test</h1>
    <button id="button">sync</button>
    <script src="./script/main.js"></script>
</body>
</html>
```

syncボタンがあるだけで、他は特殊なものはありません

## Service Worker 登録スクリプト

```main.js
navigator.serviceWorker.register('./service-worker.js');
navigator.serviceWorker.ready
         .then((registration) => {
             document.getElementById('button').addEventListener('click', () => {
                 // ここでIndexedDBなどにデータを保存しておく
                 // 保存が終わったら、↓を呼ぶ
                 registration.sync.register('sync-test')
                             .then(() => {
                                 console.log('sync registerd');
                             })
                             .catch(console.error.bind(console));
             }, false);
         })
         .catch(console.error.bind(console));
```

登録については他と一緒です

ボタンをクリックした時にサーバに送信したいデータを IndexedDB などに保存します
Service Worker はブラウザが必要に応じて終了させてしまうので、変数の値を保持しておけないためです
（グローバル変数に入れておいてもダメということ）

データの保存が終わったら`registration.sync.register`を呼びます
引数にはタグ名を設定します（このタグ名を IndexedDB に保存するキーとかにしておくといい）

## Service Worker

```service-worker.js
/*
    sync以外は省略。リポジトリのコードを見てください
*/

self.addEventListener('sync', (event) => {
    console.info('sync', event);
    
    // ここでIndexedDBからデータを取得して、サーバに送信する
});
```

sync が登録されて、Service Worker がサーバと Sync できる状態にあると判断した場合
この sync イベントが発火します
オンライン状態で sync が登録されたら、すぐに sync イベントが発火します
オフライン状態で sync が登録されたら、オンライン状態になった時に sync イベントが発火します

event.tag に sync 登録時に設定したタグ名が設定されています
<img width="356" alt="BackgroundSyncTag.png" src="https://qiita-image-store.s3.amazonaws.com/0/54850/5a3e5d9a-f0f9-1483-795c-7ecc20f90de4.png">

このタグ名を使って IndexedDB に保存しておいたデータを取得しサーバにデータを送信します
こんな感じで、確実にデータを送信することができます

---

仕組みはこんな感じです
[ここ](https://kanatapple.github.io/service-worker/sync/)にサンプルがあるので、Developer Toolsを開きながら
オフラインの時にsyncボタン、オンラインの時にsyncボタンを押して挙動を確認してみてください

# 参考
[http://www.html5rocks.com/ja/tutorials/service-worker/introduction/](http://www.html5rocks.com/ja/tutorials/service-worker/introduction/)
[https://blog.jxck.io/entries/2016-04-24/service-worker-tutorial.html](https://blog.jxck.io/entries/2016-04-24/service-worker-tutorial.html)