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

  • 17
    いいね
  • 2
    コメント

アイスタイル Advent Calendar 2016 1o日目は@kubotakがお届けします。
サーバーサイドエンジニアをやってますが、今年もフロントエンドよりな記事を書きます。

ServiceWorker

HTML5Conference2015でもこれについて触れている講義が多かった(個人比)のでご存じの方も多いと思います。
通常のjavascript実行とは異なりブラウザに登録することでバックグラウンドで動作する仕組みです。
この仕組を用いることでブラウザで以下の機能を実現することができます

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

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

  • httpsもしくはlocalhost(開発用)
  • 対応ブラウザが限られている(2016/12現在)

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

serviceworker.jsを登録する

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

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でページや画像をキャッシュさせる

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をブラウザに返却する必要があります。

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を削除します。

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したファイルをブラウザに返却しているため、オフラインでの動作が可能となります。

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サイトを用意しました。
github.io(GitHubPages)を利用してます。httpsなのでServiceWorkerを手軽に動作させることができます。
ぜひAndroid4.4以降の最新のChromeやFireFoxでご確認ください(safari非対応なので)

qr.png

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

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

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

Next Istyle Advent Calendar 2016

fagai presents 「Laravelのソースを追えるようになるにはどうしたらよいのか?」
ご期待下さい!