当初は better AppCache1 として開発が始まった Service Worker2 ですが、ページとは独立したライフサイクルを持つことでイベント駆動型のサービス3実行基盤としての色合いが強くなっています。本記事では、イベント駆動型のサービス実行基盤とは何なのか、そこへと発展していった流れについて紹介します。
なお本記事は Service Worker の使い方を紹介するものではありません。Service Worker をある程度理解している開発者を想定読者としています。また、本記事はすべて私の個人的な意見や調査に基づくものであり、所属する組織、団体とは一切関係ありません。前置きおわり。
AppCache、そして Service Worker へ
冒頭でも述べた通り、Service Worker は当初 better AppCache として開発が始まりました4 5 6。AppCache はマニフェストによってキャッシュしたいリソースを宣言的に記述でき、手軽に(!= 簡単に)アプリをオフライン対応できるのがメリットでした。一方、キャッシュ内容を細かく制御する術がなく、またキャッシュの更新ロジックを適切に作っておかないとキャッシュを更新できなくなってしまうこともあり、扱いがとても難しいものでした7。
そこで「より良いオフラインアプリの API を作ろう」というモチベーションから仕様策定が始まったのが Service Worker です8。Service Worker の登場により、AppCache は実装・仕様レベルで非推奨機能になりました9 10 11。
この辺りの話は mozaic.fm で @kinu さんが話してたのでそちらも是非。
オフライン対応アプリのモデル
ここで一旦 Service Worker から離れ、ウェブページがオフライン対応するには何が必要か考えてみましょう。あるページがサーバ上にあるリソース(JS ファイル)を読み込むとします。
このウェブページをオフライン対応する素朴な方法は、ページとリソースをブラウザ内にキャッシュすることです。これでネットワークにアクセスせずともページを読み込めるようになりました。
しかし、このままではキャッシュされたリソースをいつまでも更新できません。更新を可能にするため、ページ側にキャッシュ内容を検証するロジックを追加します。ネットワークリクエストに先立ち、リソースが更新されていないかサーバに確認するようにし、更新があればリソースを取得し直します。もし更新がなかったり、ネットワークに接続できなかったりした場合は、キャッシュされたリソースを使うようにします。
この機構は恐らくうまく動きます。しかし、あらゆるネットワークリクエストに対してキャッシュの有無、リソース更新の有無、そしてネットワークコネクションの有無をページ側で確認しなければならないのが厄介です。キャッシュロジックがアプリケーションロジックにべったりなコードができてしまいます。これはつらい。
そこで、ページからはキャッシュ・リソース更新・ネットワークコネクションの有無に関わらず常にリクエストを発行するようにし、実際にブラウザからインターネットへリクエストが飛んで行く前にそれらキャッシュ処理を実行してくれるようなプロキシを考えます。
プロキシは先程ページ側に追加したキャッシュロジックと同じことをします。ページ側でネットワークリクエストが発行されるとそれを横取りし、キャッシュの有無、リソース更新の有無、ネットワークコネクションの有無を確認して、適切に処理します。この機構により、ページ側からキャッシュロジックを追い出すことができ、コードの見通しも良くなります。アプリの特性に応じて、より柔軟なキャッシュストラテジーを導入することもできるでしょう12。
また、プロキシが動かない環境ではリクエストがネットワークに飛んで行くだけなので、ページ側に改修を加えずともそのような環境でもとりあえず動く(オフライン対応していない)ページを作ることができます13。
このプロキシ機構が最初期の Service Worker で、キャッシュを管理するためのストレージが同じ仕様内で定義されている Cache Storage API です14。後のサービス実行基盤としての Service Worker と区別するために、本記事ではこれを「初期の Service Worker」と呼ぶことにします。
Service Worker のライフサイクルモデル
初期の Service Worker は単なるネットワークプロキシなので、ネットワークリクエストが発行された時に起動すれば十分です。常に起動している必要はありません。リソースは Cache Storage に永続化されています15。これにより、Service Worker は必要になったら起動し、不要になったら終了できる、というモデルになりました16。
ただし、ページ遷移のたびに Service Worker を起動したり終了したりするのは効率がよくありません。そこで、同一オリジン内のページでは同じ Service Worker を使いまわせるようにしました。これにより、Service Worker は特定のページの生存期間 (ページを開いてから別のページに移動するまで) に依存しないモデルになりました17。
以上により「Service Worker はページの有無に関わらず任意のタイミングで起動・終了でき、かつページとは独立したライフタイムを持つことができる」モデルになりました。
リッチウェブアプリへの機運
ここでまた話が変わって、ウェブアプリの機能性についての話です。スマートフォン用のアプリ(いわゆるネイティブアプリ)はウェブアプリよりも高機能であると言われています。その例としてよく挙げられるのが、オフライン対応、プッシュ通知、バックグラウンド同期の存在です。オフライン対応は AppCache や初期の Service Worker で利用可能になったので、ここではプッシュ通知とバックグラウンド同期を実現する上でウェブに足りないものを考えてみます。
プッシュ通知はサーバ側から送られた情報をクライアント側で処理する機能です。ウェブの場合はウェブアプリ(ブラウザ)がクライアントになります。プッシュ通知を従来のウェブアプリで実現するには問題がありました。というのも、一般的にプッシュ通知はクライアント側の状況に関わらず非同期に送信されてくるため、もしウェブアプリが開かれていないときに通知が送られてきたとすると、誰がそれをハンドルしたら良いか判断できません。バックグラウンド同期も同様で、ウェブアプリを閉じた後では同期処理を継続することができません。
これらを整理すると、プッシュ通知やバックグラウンド同期をウェブで実現するには、ウェブアプリを閉じた後にも処理を行える仕組みが必要だと分かります。例えば、通知が届いたら誰がそれを処理するかあらかじめ登録しておく仕組みが必要そうです。
イベント駆動型のサービス実行基盤への進化
ここで話を初期の Service Worker に戻します。初期の Service Worker はネットワークリクエストに応じて起動し、キャッシュ処理を行い、処理が終わって不要になると終了しました。これを抽象化すると「何らかのイベントに応じて Service Worker を起動し、何らかの処理を行い、処理が終わって不要になると終了する」となります。
同様のモデリングはプッシュ通知とバックグラウンド同期にも適用できます。プッシュ通知なら「通知に応じてハンドラを起動し、通知処理を行い、処理が終わって不要になると終了する」となり、バックグラウンド同期なら「オンラインになったらハンドラを起動し、バックグラウンド同期を行い、処理が終わって不要になると終了する」となります。
この「ハンドラ」を「Service Worker」に置き換えると・・・?そう、プッシュ通知やバックグラウンド同期を実現するために必要だったものは、すべて Service Worker のモデルで実現可能でした。そこで、Service Worker を単なるネットワークプロキシとして使うのではなく、イベントに応じてバックグラウンド処理を行うための基盤としての活用が見出され、整備されるようになったのでした。
現在の Service Worker はサービス構築のための基盤であり、イベントハンドラを登録しておくことで様々なイベントをバックグラウンド処理できるようになっています18。ハンドラを将来の仕様で追加可能なように Service Worker の仕様で拡張ポイントが定義されています19。Push API20 や Background Sync API21 はこの拡張ポイントを使ってハンドラを追加定義しています。
当初の目的であったオフライン対応は、今やネットワークリクエストイベントの onfetch ハンドラの応用例の一つに過ぎません。onfetch ハンドラは Service Worker の仕様内で定義されていますが、onpush ハンドラや onsync ハンドラと同格なものです(ただし歴史的な理由により Service Worker の仕様に深く紐付いてしまっているため、実際には綺麗に分離できていません。例えばスコープという概念はネットワークリクエストイベントでしか使いませんが、Service Worker の識別子となっているため登録時に必須となっています。この辺りはちょっと残念なところです。)
まとめ
本記事では better AppCache として開発がスタートした Service Worker が、イベント駆動型のサービス実行基盤へと発展していった流れを紹介しました。必ずしも説明したとおりの流れで仕様が発展したわけではありませんが、整理するとおおよそこんな感じだったんじゃないかなと思っています。
Service Worker はあくまでもサービス構築の基盤であり、それ自体が何かの機能を提供するわけではありません。「オフライン対応・プッシュ通知・バックグラウンド同期などはその上のレイヤーの話である」というように意識しておくことは、今後リッチなウェブアプリ (Progressive Web Apps) を設計する上で重要なポイントだと思います。おわり。
謝辞
本記事の草稿を @horo さんに確認していただきました。ありがとうございます。なお、文責はすべて私 (nhiroki) にあります。
-
AppCache を拡張する DataCache という仕様も検討されていました / Programmable HTTP Caching and Serving (W3C Working Group Note 29 March 2011) ↩
-
"Developers using the HTML5 Application Cache have also reported that several attributes of the design contribute to unrecoverable errors. A key design principle of the service worker is that errors should always be recoverable. Many details of the update process of service workers are designed to avoid these hazards." / Service Workers Nightly (Editor's Draft, 2 February 2017) - 1. Motivations ↩
-
"This feature is in the process of being removed from the Web platform. (This is a long process that takes many years.) Using any of the offline Web application features at this time is highly discouraged. Use service workers instead." HTML Living Standard — Last Updated 2 February 2017 ↩
-
Remove AppCache from Insecure Contexts Security - Chrome Platform Status ↩
-
Service Worker と Cache Storage を用いたキャッシュストラテジーについては次のページを参照してください。 / The offline cookbook ↩
-
「新しい機能に対応しているブラウザでは新しい機能を使い、未対応ブラウザでは今まで通りの表示を行う」という考え方はプログレッシブエンハンスメントと呼ばれ、PWA (Progressive Web Apps) の重要な要素となっています。 / Progressive Web Apps: Escaping Tabs Without Losing Our Soul - Infrequently Noted ↩
-
余談ですが、Service Worker と Cache Storage API の組み合わせはブラウザキャッシュ(HTTP キャッシュ)の構造と類似しています。ブラウザの内部では、ネットワークリクエストが発行されるとまずブラウザキャッシュに問い合わせ、キャッシュが使用可能な場合はそれを返却してサーバへの問い合わせを行いません(厳密には Cache-Control ヘッダ次第です)。まったく同じではありませんが、このブラウザキャッシュの機能を JS レベルに提供したという見方もできるでしょう。このあたりは「ブラウザの挙動を再定義し、プリミティブを提供していく」という Extensible Web 的な考えだと見ることもできます。 ↩
-
厳密に言うと、Cache Storage に保存しただけでは永続化が保証されません。デフォルトでは Cache Storage のデータはディスク容量が逼迫した場合に消される可能性があります (これを "best-effort" モードと呼びます)。ストレージ容量が逼迫した場合にも永続性を保証するためには、Persistent Storage API を使用してオリジン毎に "persistent" モードのパーミッションを取得する必要があります。 / Persistent Storage ↩
-
任意のタイミングで起動終了できるモデルは、Shared Worker や Chrome Background Pages での経験が生かされています。 / "Service workers are started and kept alive by their relationship to events, not documents. This design borrows heavily from developer and vendor experience with Shared Workers and Chrome Background Pages. A key lesson from these systems is the necessity to time-limit the execution of background processing contexts, both to conserve resources and to ensure that background context loss and restart is top-of-mind for developers. As a result, service workers bear more than a passing resemblance to Chrome Event Pages, the successor to Background Pages." / Service Workers Nightly (Editor’s Draft, 2 February 2017) - 1. Motivations ↩
-
同様の 1 対多のモデルを Shared Worker が先に導入しており、Service Worker のモデルの参考になっています。Shared Worker の場合はコネクションを持つすべてのページが閉じられたときに終了します。 / SharedWorker - Mozilla Developer Network ↩
-
"The service worker is a generic entry point for event-driven background processing in the Web Platform that is extensible by other specifications." / Service Workers Nightly (Editor’s Draft, 2 February 2017) - Abstract ↩
-
Service Workers Nightly (Editor’s Draft, 2 February 2017) - 8. Extensibility ↩