ServiceWorkerを使ったオフラインWebアプリケーション

[アイスタイル Advent Calendar 2016](http://qiita.com/advent-calendar/2016/istyle) 1o日目は@kubotakがお届けします。
サーバーサイドエンジニアをやってますが、今年もフロントエンドよりな記事を書きます。

# [ServiceWorker](https://developer.mozilla.org/ja/docs/Web/API/ServiceWorker_API)
HTML5Conference2015でもこれについて触れている講義が多かった（個人比）のでご存じの方も多いと思います。
通常のjavascript実行とは異なりブラウザに登録することでバックグラウンドで動作する仕組みです。
この仕組を用いることでブラウザで以下の機能を実現することができます

- キャッシュを用いたオフライン表示
- プッシュ通知
- バックグラウンド同期
- etc

とても便利そうでしょう？ただし以下の条件が必要ですので注意が必要です

- httpsもしくはlocalhost(開発用)
- [対応ブラウザ](http://caniuse.com/#feat=serviceworkers)が限られている(2016/12現在)

本稿ではキャッシュを用いたオフライン表示について、実践してみたいと思います。
ちなみに、弊社サービスでも自分の担当しているサイトではひっそり導入をしています（対応ブラウザが増えて、採用が増えるといいな）

# serviceworker.jsを登録する
ServiceWorkerは仕組みの呼称ですので登録させるjavascriptの名前はsw.jsでもhoge.jsでも問題ありません。今回はserviceworker.jsというファイルを作成し、そのjsファイルをServiceWorkerとして登録させます。

```js:register.js
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('serviceworker.js', {scope: './'})
        .then(registration => {
            // Success
            console.log('Service Worker Registered');
        }).catch(error => {
            // Error
            console.error(error);
    });
}
```

`{scope: './'}` とすることにより、後述する`fetch`イベントの際に、このスコープ以下のファイルが対象になる。

# serviceworker.jsでページや画像をキャッシュさせる

```js:serviceworker.js
const VERSION = 1;
const STATIC_CACHE_NAME = `cache_${VERSION}`;
const ORIGIN = `${location.protocol}//${location.hostname}` + (location.port ? ':' + location.port : '');
// Cacheするファイル
const STATIC_FILES = [
  'https://fonts.googleapis.com/css?family=Josefin+Sans',
  'https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css',
  `${ORIGIN}/`,
  `${ORIGIN}/?utm_source=web_app_manifest`,
  // `${ORIGIN}/img/image.png`, // sample
];
let STATIC_FILE_URL_HASH = {};
STATIC_FILES.forEach(x => {
  STATIC_FILE_URL_HASH[x] = true;
});

// install
self.addEventListener('install', e => {
  e.waitUntil(
    caches.open(STATIC_CACHE_NAME).then(cache => {
      return Promise.all(STATIC_FILES.map(url => {
        return fetch(new Request(url)).then(response => {
          if (response.ok)
            return cache.put(response.url, response);
          return Promise.reject(
            `Invalid response.  URL: ${response.url}
             Status: ${response.status}`);
        });
      }));
    }));
});
```

ServiceWorkerが登録される際はこちらの`install`イベントが実行されます。
`self.addEventListener('install', e => {});`
このイベントの中のcache.putで、ブラウザのCacheStorageにレスポンスが格納されます。

## Cacheした情報を返す
Cacheしただけではオフライン時に表示はされません。
既にCache済みのデータがある場合はCacheをブラウザに返却する必要があります。

```js:serviceworker.js
self.addEventListener('fetch', e => {
  if (!STATIC_FILE_URL_HASH[e.request.url]) return;
  e.respondWith(caches.match(e.request, {cacheName: STATIC_CACHE_NAME}));
});
```

## Cacheを削除する
`activate`イベントのタイミングでcacheした内容と`STATIC_CACHE_NAME`が異なっている場合に対象のcacheを削除します。

```js:serviceworker.js
// Delete Cache
self.addEventListener('activate', e => {
  e.waitUntil(
    caches.keys().then(keys => {
      let promises = [];
      keys.forEach(cacheName => {
        if (cacheName != STATIC_CACHE_NAME)
          promises.push(caches.delete(cacheName));
      });
      return Promise.all(promises);
    }));
});
```

## ServiceWorkerからレスポンスを返却する 
`fetch`イベントハンドラで`e.respondWith()`としている箇所では、サーバーからの返却ではなくServiceWorkerからブラウザへデータを返却しています。
ここではcacheしたファイルをブラウザに返却しているため、オフラインでの動作が可能となります。

```js:serviceworker.js
self.addEventListener('fetch', e => {
  if (!STATIC_FILE_URL_HASH[e.request.url]) return;
  e.respondWith(caches.match(e.request, {cacheName: STATIC_CACHE_NAME}));
});
```

※ ES2015で記述しているため公開時にはトランスパイルをしてES5にする

# DEMO
[DEMOサイト](https://kubotak-is.github.io/offline_test/public/)を用意しました。
github.io(GitHubPages)を利用してます。httpsなのでServiceWorkerを手軽に動作させることができます。
ぜひAndroid4.4以降の最新のChromeやFireFoxでご確認ください（safari非対応なので）

![qr.png](https://qiita-image-store.s3.amazonaws.com/0/89213/de0c2b1c-3da2-f92f-4f37-30ddf6871f6e.png)

Chromeでアクセス後はそのページをホーム画面に追加してみてください。
ネイティブアプリのようにホームにアイコンが追加され、タップすることでブラウザ枠を表示しないで立ち上がると思います。
本稿では触れてませんがこれがProgressiveWebAppsと呼ばれるものになります。興味が湧いた方はぜひそちらについても調べていただければと思います。

さて、ページが表示されたでしょうか。続いては機内モード等で通信を切断させてください。
その状態で同じようにまたDEMOサイトを立ち上げてみてください。

・・・表示、できているでしょうか？
これがServiceWorkerのいち機能であるオフライン表示です。


# Next Istyle Advent Calendar 2016
[fagai](http://qiita.com/fagai) presents 「Laravelのソースを追えるようになるにはどうしたらよいのか？」
ご期待下さい！