EagleLand

2014.12.29

Service Workerに関する仕様とか機能とか

今巷で流行りのService Workerについて調べ物してたので、まとめたメモ。

Service Workerが解決してくれること

Service WorkerはHTML・CSS・JS・画像等などのリソースを、JavaScriptのAPIから命令的にコントロールすることを実現する。Webページのパフォーマンスに関する指標としてネットワークを介して得るリソースをキャッシュさせたりすることが効果的であることは今更改めて挙げないが、Service Workerによって保持されたリソースは、オフライン状態でも返却することが可能という凄さを持っている。つまり、更新性の低いコンテンツであればオフラインでも閲覧させることが可能ということ。

更新性のあるコンテンツでも、回線が不安定な時はローカルに変更を保持して、サーバーに対してデータを遅延で同期するみたいなテクニックは既に存在している。ので、こういったテクニックと組み合わせて、よりクライアントの画面がホワイトアウトすることを減らしていける。はず。

こちらは、Jake Archibald氏とAlex Russell氏によるGoogle I/O 2014でのセッション「Bridging the gap between the web and apps」。@myakura氏による解説記事もある。

Application Cache

先程の動画でも少し触れられているように、Application Cacheとよく対比される。リソースをキャッシュする機能として現れたのがApplication Cacheだったが、キャッシュリソースのコントロールがし難かったり、動的なコンテンツを生成する際、構成がApplication Cache前提になってしまう等、いささか問題を抱えていた。それを解決してくれるのがService Workerでもある。

Application Cacheの問題点については、@kyo_agoさんが執筆したモバイル対応Webアプリケーションのキャッシュ戦略という記事にまとまっている他、TwitterでメンションしてもApplication Cacheの話題であれば何かしらレスをくれる。かもしれない。

ブラウザキャッシュ

ブラウザキャッシュもパフォーマンスを向上させる上で非常に重要な存在であることには間違いなさそうだが、JavaScriptからコントロールすることは不能だし、ブラウザによって挙動もまちまちである。なんせ、ブラウザキャッシュはW3Cに載っているような仕様の類ではなく、ブラウザベンダーが気を利かせて実装している機能に過ぎないからである。

ブラウザキャッシュと言えば、Nicholas Zakas氏によるThe changing role of the browser cacheというブラウザキャッシュの役目の移り変わりについての記事も興味深い。

オフラインアプリケーションの夢

ホワイトアウトを減らすどころか、必要なリソースを全てService Workerでコントロールすればオフラインアプリケーションの作成も可能である(キャッシュするリソースを取得する最初のダウンロードは必要になるが)。

つまりService Workerは、Application Cacheの屍を超えて生まれた今までにないリソースのコントロール機構であると言える。

Service WorkerのAPIと挙動

Service WorkerはWeb Workerなんかと同じように(Web Workerの一種と言ったほうが正確なのかも)、ブラウザの表示とは別スレッドで実行される(だから、DOMのAPIとかを叩いたりすることは出来ない)。Service Workerでは、ページから行われるリソースの要求等に対し、独自の処理を挟むことが出来る。 プロキシを自前で用意出来る と言ったほうがイメージしやすいかも。

リクエストをフックし、Cache APIを介してアレコレする。あるURLへのリクエストに対するレスポンスを受け取った時にそのリソースを保持したり、はたまた再度そのリクエストが発生する時にはCache APIから保持したリソースを引っ張りだしてブラウザに返却する。といったような処理をService Workerにしてもらうことになる。

しれっとCache APIが出てきたが、これもService WorkerのAPIの一環で、Service Workerコンテキストで利用可能なキャッシュリソースを管理するためのAPIである。

もうちょっと実際の処理に近い説明

  1. リクエストされたリソースをキャッシュさせたり、リクエストに割り込んでキャッシュされたリソース等を返却するような処理が記述されているservice-worker.jsを用意
  2. index.htmlservice-worker.jsをService Workerとして登録する(この時、index.html内の評価は行われていない)。
  3. service-worker.jsに定義してあるリクエストがindex.htmlから行われた場合、フックする。既にキャッシュに存在している場合はそれを返却したり、キャッシュされていなければそのままサーバーへリクエストしてあげる。

画像をService Workerでcachesにキャッシュさせるサンプル

実際のコードを動かしてもらって、デバッグしてもらう方がイメージしやすいと思うので簡単なサンプルを作った。

ブラウザの準備

Google Chrome CanaryVersion 41.0.2259.0 canary (64-bit) で動作確認済。フラグをonにしないと動かないのでchrome://flagsで、Enable experimental Web Platform features.Enable support for ServiceWorker background sync event.を有効にしておく。

Service Workerはセキュリティ上、HTTPS環境かローカルホストのみ実行可能になっている。ローカルでのデバッグはpython -m http.serverでOKだが、動くように作ったつもりでもホスト先がHTTPSじゃないと動かない。簡単デプロイの代名詞のGitHub Pagesもダメなので、お手軽に用意出来そうなHTTPS環境はDropboxのPublicっぽい。

※2014/12/29追記

簡単デプロイの代名詞のGitHub Pagesもダメなので、

と書いてあるところに指摘を頂きまして、修正しました。

GitHub PagesのHTTPSサポートについては、以下にも情報があった。

index.html

5枚の画像を表示するだけの、シンプルなHTML。

ブラウザキャッシュだと、URLにアクセスした時に真っ白になってしまうけど、今回はURLに対して、画像5枚とHTMLをService Workerで丸ごとキャッシュさせてインターネットに接続されていない状態でも表示させることを目指す。

<html>
  <head>
    <meta charset="utf-8">
    <title>Service Worker Playground</title>
    <script>
      // navigator.serviceWorkerがある場合
      if (navigator.serviceWorker) {

        // service-worker.jsをService Workerとして登録する
        navigator.serviceWorker.register('./service-worker.js', {
          scope: '.'
        }).then(function onFulfilled () {

          // service-worker.jsがひと通り評価され、インストールが成功した場合
          console.log('Service Worker was installed.');
        }, function onRejected () {

          // service-worker.jsのインストールが失敗した場合
          console.log('Service Worker was not installed.');
        });
      }
    </script>
  </head>
  <body>
    <div><img src="img/1.jpg"></div>
    <div><img src="img/2.jpg"></div>
    <div><img src="img/3.jpg"></div>
    <div><img src="img/4.jpg"></div>
    <div><img src="img/5.jpg"></div>
  </body>
</html>

service-worker.js

先程のindex.htmlからService Workerとして登録しているservice-worker.jsの中身。Service Workerコンテキストはselfで参照し、各種イベントにハンドラを登録している。

また、Chrome 40ではCache APIが一部未実装なのでcoonsta/cache-polyfillをロードする。

// Cache APIが一部未実装なのでポリフィルをロード
importScripts('serviceworker-cache-polyfill.js');

// キャッシュのキーとなる文字列
var CACHE_KEY = 'service-worker-playground-v1';

self.addEventListener('install', function (e) {

  console.log('ServiceWorker.oninstall: ', e);

  e.waitUntil(
    caches.open(CACHE_KEY).then(function (cache) {

      // cacheさせたいリクエストのキーを追加
      return cache.addAll([
        'index.html',
        'img/1.jpg',
        'img/2.jpg',
        'img/3.jpg',
        'img/4.jpg',
        'img/5.jpg'
      ]);
    })
  );
});

self.addEventListener('fetch', function (e) {

  console.log('ServiceWorker.onfetch: ', e);

  e.respondWith(
    caches.open(CACHE_KEY).then(function (cache) {
      return cache.match(e.request).then(function (response) {
        if (response) {

          // e.requestに対するキャッシュが見つかったのでそれを返却
          return response;
        } else {

          // キャッシュが見つからなかったので取得
          fetch(e.request.clone()).then(function (response) {

            // 取得したリソースをキャッシュに登録
            cache.put(e.request, response.clone());

            // 取得したリソースを返却
            return response;
          });
        }
      });
    })
  );
});

self.addEventListener('activate', function (e) {
  console.log('ServiceWorker.onactivate: ', e);
});

Service Workerの登録(navigator.serviceWorker.register)時に発火するinstallイベントで、キャッシュさせたいリソースのパスをキーとして登録定義している。これはRequestInfoという構造体の配列になる。

fetchはブラウザのUIスレッドからリクエストが発生したときに発火する。ここでは、キャッシュオブジェクト(caches)にリクエストに対するリソースが保持されている場合に、サーバーへのリクエストを実行せずキャッシュされたリソースを返却し、キャッシュに保持されていない場合はサーバーにリソースを要求しキャッシュに保持した上でブラウザにリソースを返却している。

Service Workerのデバッグ

index.htmlservice-worker.js、あとはimgフォルダに1.jpg ~ 5.jpgを配置して準備は完了。あとはローカルホストを起動する。

$ python -m http.server

URLに対し登録されたService Workerは、chrome://serviceworker-internalsでどういう状態かを確認することが出来る。 Opens the DevTools window for ServiceWorker on start for debugging. のチェックをオンにしておくと、Service Workerが登録された時にワーカースレッドに対するDevToolsが自動で開くのでデバッグ時はオンにしておくと良さげ。

Service Worker Internalsから起動するDevTools

起動したlocalhost:8000をCanaryで開くとService Workerの登録(service-worker.js)がindex.htmlで行われる。Service WorkerのDevTools上でステップ実行をしていくと、各イベントハンドラが登録されていくのがわかる。最後まで実行されると、index.htmlに実行スレッドが戻ってくるのが確認出来る。

初回登録時にはService Workerで定義しているリソースが保持されていないのでダウンロードが必要だが、2回目以降のアクセス時には<img src='img/1.jpg'>によって発生するリクエストをService Workerが拾って、fetchイベント内でキャッシュからリソースを返却するようになる。

めでたしめでたし。

その他

今回はService Worker内でfetchinstallといった初歩的な部分しかハンドルしてないけど、バックグラウンドでデータの同期(Background Sync)を行ったり、Push APIと連携するpushだったり、ブラウザスレッドからのメッセージ(navigator.serviceWorker.controller.postMessage)をmessageで受け取ることで任意のタイミングでリソースの更新を行ったり出来そう。

まだpushの機能はChromeにも実装されていないけど、chrome.gcmのインフラ使うのかなとか、SafariだったらiOSのプッシュサーバー使うのかなとか色々妄想はある。インフラさえ整えば、pushイベント時にNotification出すとか、本当のプッシュ通知をWebで利用できる日が来そう。

何にせよ、仕様がもっと安定して、ブラウザの実装が進むのを待ちたい。

参考リソース

以下、Jake率高めなService Workerに関する記事とか。