そろそろはじめる Service Worker @ SpeeeKaigi #3

ご無沙汰しております。エンジニア組織推進室の服部 (Github: yhatt) です。今年は Minecraft で交通計画を考え、鉄道路線(高架線)を引くことがマイブームでした。石レンガを使うと映えます。

今夏に開催された SpeeeKaigi #3 において、『そろそろはじめる Service Worker』の題で発表させていただきましたので、簡単にご紹介させていただきます。

tech.speee.jp

Service Worker?

以前まで Worker といえば、Web ページの中の範疇で使用できる技術でした(Web Worker)。呼び出されたページにおいて JavaScript をバックグラウンドで動作させ、マルチスレッド的な動きを実現する仕組みです。

Service Worker は、ブラウザにインストールする形のワーカーで、オフラインキャッシュ・プッシュ通知など、各種 API により バックグラウンドでリッチな機能を提供できるようになる Web 標準技術です。詳しくは以下のリンク先をどうぞ。

昨今の Google が激推しクンの、 PWA (Progressive Web Apps) を根底で支える技術でもあります。

対応ブラウザ

Service Worker 対応状況

-- Can I use... Support tables for HTML5, CSS3, etc

対応ブラウザは Chrome (Android 含)、Firefox、Opera となっており、Edge も現在開発中の機能1となっています。

これまで唯一 Safari (iOS 含) が動きを見せていませんでしたが、今年8月3日 In development にステータスが変わった旨の報告がなされ、足並みが整った形になります。2

Service Worker はもともと、 AppCache というオフラインキャッシュの仕組みを置き換える目的で実装されたものであり、ネットワークが不完全な環境になりやすい Android / iOS などの携帯端末では重要な技術の 1 つになり得ます。

加えて、Service Worker で使用できるリッチコンテンツ実現のための機能群は、PWA がネイティブアプリを脅かす存在になり得ると言われるほどです(無論、全てが置き換わるわけではないと思いますが)。

社内ではまだ Service Worker を実際に採用しているプロダクトは無く、今回の SpeeeKaigi でそのパワーを確かめようと、この題材での発表に至りました。

ワーカー登録方法

対応ブラウザには navigator.serviceWorker が定義されますので、 register() を呼び出してワーカーを登録します。

// https://example.com/main.js
navigator.serviceWorker.register(
  '/worker.js',
  {
    scope: '/hoge/',
  }
)

上記のスクリプトを HTTPS 環境で動作させると、https://example.com/worker.js というワーカースクリプトが https://example.com/hoge/ を対象スコープとして動作するように、ブラウザにインストールされます。

Service Worker は HTTPS もしくは localhost でしか動作しません。実践投入時は十分注意しましょう。

何ができるのか?

yhatt/service-worker-playground  
── GitHub - yhatt/service-worker-playground: Service worker playground

今回、上記のリポジトリに 3 種類のサンプルを構築し、デモを実施しました。

リポジトリは WTFPL ライセンスですので、煮るなり焼くなりしていただいて構いません。

オフラインキャッシュとして使う

オフラインキャッシュ は、Service Worker の一般的な使い方と言えます。ワーカー内での利用を想定して実装された Cache API を使い、ネットワークリクエストのキャッシュを JavaScript でプログラマブルに提供できます。

// worker.js (例)

self.addEventListener('install', (e) => {
  e.waitUntil(
    caches.open('v1').then((cache) => {
      // 画像をキャッシュ対象に追加
      cache.addAll([
        'https://robohash.org/1',
        'https://robohash.org/2',
        'https://robohash.org/3',
        // ...
      ])
    })
  )
})

// ネットワークリクエスト時に呼び出される
self.addEventListener('fetch', (e) => {
  e.respondWith(
    caches.match(e.request).then(response =>
      // キャッシュがあればそれを返却し、無ければ従来通りリクエスト
      response ? response : fetch(e.request)
    })
)

実際にリポジトリで http://127.0.0.1:8080/offline_cache/ を動かしてみると、初回は通常通りネットワークから画像を読み込みます。ブラウザの更新など、 2 回目の読み込みを行うと、画像がキャッシュから読み込まれます。

キャッシュは、表示の高速化はもちろんのこと、不安定なネットワーク環境で UX を担保するためにも重要です。初回の読み込みが終わった後、Wi-Fi などのネットワークを切断し、オフライン状態でページを更新してみてください。

初回 2回目以降
Service Worker Disabled Service Worker Enabled

Service Worker はプログラマブルなので、AppCache よりも柔軟なオフラインキャッシュのアプローチを試みることが可能です。The offline cookbook - JakeArchibald.com では、多くの例が紹介されています。なお、上記のコードは、 Network falling back to cache を採用した例です。

Network falling back to cache

 
── The offline cookbook - JakeArchibald.com

ちなみに、Service Worker 内では Promise が多用されます。ES2017 な環境が整っているのであれば、async/await を使うのも手でしょう。

ローカルプロキシとして使う

応用として、Service Worker をローカルプロキシのように使ってみるサンプルも作成してみました。こっちの方が Service Worker の動作を把握するのはわかりやすいかもしれません。

リポジトリで http://127.0.0.1:8080/pie_chart_image_proxy/ を開いてワーカーをインストールした後、ページを更新すると、円グラフが現れます。テキストボックスの GET パラメータを変更すると、その値に合わせて円グラフがリアクティブに変化します。

Pie chart image proxy demo

この円グラフの正体は、何の変哲もない <img> タグで、グラフを構成するデータの GET パラメーターがついた画像を src 属性で呼び出しているだけです。

<img src="chart.gif?a=150&b=200&c=30&d=60" />

もちろん、サーバーサイドでクエリを解析して円グラフを作っているわけではありません。Service Worker が 画像に対するリクエストをフックし、パラメーターに応じた内容の SVG 画像を動的に返す ように設定することで、グラフの生成を実現しています。

// worker.js (例)
import url from 'url'
import PieChart from './pie_chart'

self.addEventListener('fetch', (e) => {
  const requestUrl = url.parse(e.request.url)

  // 特定の画像ファイルがリクエストされたら...
  if (requestUrl.hostname === '127.0.0.1' &&
      requestUrl.pathname === '/chart.gif') {

    // クエリの情報をグラフ生成クラスに渡す
    const chart = new PieChart(requestUrl.query)

    // レスポンスとして返すSVG画像を生成
    e.respondWith(
      new Response(
        chart.renderAsSVG(),  // SVG 画像生成処理
        { headers: { 'Content-Type': 'image/svg+xml' } }
      )
    )
  }
})

このように Service Worker と Fetch API を組み合わせることで、Web サイトのリクエストを、高い柔軟性を持ってコントロールすることが可能になります。

これによって、例えば

  • 画像のA/Bテストをクライアントサイドで完結する
  • ブラウザが表示できない画像を、クライアントサイドで変換して表示する
  • クライアントサイドで、テンプレートエンジンを動作させる
  • etc...

などのアイデアを実現することが可能になります。最も、こういった実装は従来サーバーサイドの仕事なので、実装が必要なケースに遭遇するかどうかは別問題ですが…

プッシュ通知

Service Worker の特徴は、ページに滞在していなくても、必要なタイミングでブラウザがバックグラウンドでワーカーを実行できる ことにあります。

Push API を使用すれば、スマートフォンのような プッシュ通知 をブラウザで実現することが可能です。Web アプリケーションによる通知はもう珍しくなくなってきましたが、Service Worker は 1 度許可すれば、ページを開いていなくても通知を受け取れる 状態にできるのが大きな特徴です。

f:id:yuki-hattori:20170905130229p:plain

http://127.0.0.1:8080/push_notification/ を開き、プッシュ通知を許可すると、以下のような通知送信テスト用のコマンドが表示されます。

yarn webpush -- \
  --endpoint https://fcm.googleapis.com/fcm/send/xxxxxxxxxxxxxxxx \
  --auth xxxxxxxxxxxxxxxx== \
  --p256dh xxxxxxxxxxxxxxxxxxxxxxxxxx= \
  --payload 'Test notification!!'

受け取ったパラメータの情報をサーバーに渡し、プッシュ通知をサーバーサイドで制御するのが一般的な使い方ですが、このリポジトリではテスト用に npm script でパラメータを渡して通知を送れるようにしました。

表示されたコマンドをコンソールで実行すると、ブラウザに通知が飛んできます。ページのタブを閉じていたり、他のページを閲覧しているなどしていた状態でも同様です。

f:id:yuki-hattori:20170905131048g:plain

// worker.js (例)
self.addEventListener('push', (e) => {
  e.waitUntil(
    self.registration.showNotification(
      'Service worker notification',
      { body: e.data.text() },
    )
  )
})

self.addEventListener('notificationclick', (e) => {
  e.notification.close()
  e.waitUntil(
    clients.openWindow('http://127.0.0.1:8080/push_notification/')
  )
})

プッシュ通知をブラウザが検出すると、Service Worker の push イベントが発行されます。ここでは、受け取ったメッセージを Notifications API で表示し、さらに notificationClick イベントで通知クリック時の挙動(ページを開く)を定義しています。

今回は Service Worker にフォーカスしているため、 Push API の詳細な実装 までは説明しませんが、Web ページの外側で Service Worker が動作しているのがお分かり頂けると思います。

おわりに

f:id:yuki-hattori:20171211224832j:plain:w480

駆け足で Service Worker ができることを紹介しましたが、今回のサンプル実装を通じ、 アイデア次第でさまざまな応用が可能 な、非常に強力な機能であると感じました。

ローカルプロキシの例などは、一般的にはサーバーサイドで行うべき機能であるため、そのようなアイデアを Web アプリケーションで使える場面は限られてくると思われます。一方で、Electron のようなデスクトップアプリケーション向け開発など、実用的に使えるケースも多いでしょう。

何より、各ブラウザベンダーの足並みが揃ったことで、今後の Web アプリケーションにおける Service Worker、ひいては PWA の存在感がより増すと思われます。今から習得しておいて損は無いと思いますので、トライしてみてはいかがでしょうか。

参考事例


  1. つい先日、12月19日に公開された Windows Insider Build に含まれる Edge にて、Service Worker が使用できるようになっています。» Service Workers: Going beyond the page - Microsoft Edge Dev BlogMicrosoft Edge Dev Blog 

  2. つい先日、12月20日に公開された Safari Technology Preview にて、Service Worker が使用できるようになっています。 » Release Notes for Safari Technology Preview 46 | WebKit