Intro
Service Worker は、その立ち位置が非常に特殊なため、仕様を策定する上でも「どう振る舞うべきか?」を決定するのが難しい場合がある。
時には、開発者は「どう振る舞うだろう」と考えるか、感覚的な部分についてアンケートを行い、フィードバックを得ることが過去にもあった。
今回は、 Service Worker が絡んだ場合の BaseURI について、同様のアンケートが行われているので紹介する。ぜひ考えてみてほしい。
Service Worker
略
BaseURI
基本
一応解説しておくと、相対パスで記述された URI が、 FullPath としてどう解決されるかのベースになる。
例えば http://example.com/ で取得した HTML 内に以下のリンクがあったら。
<a href="/foo">foo</a>
リンクをクリックすれば http://example.com/foo にリクエストが飛ぶ。
これは、このリソースの BaseURL が http://example.com
だからだ。
サブリソース
これはサブリソースにも影響する。
html が http://example.com/ だったとする。
<link rel="stylesheet" href="/foo.css">
css から、さらにイメージを読み込む。
html {
background: url('bar.jpg');
}
この場合、 html が http://example.com だったら、 css で読まれるイメージは http://example.com/bar.jpg になる。
base タグ
あまり使われてはいないと思うが、 BaseURL は <base>
タグなどによって変えることができる。
以下のように記述していると、ページを取得した URL と関係なく、 BaseURL を指定できる。
<base href="http://example.ne.jp">
<a href="/foo">foo</a>
リンクをクリックすると http://example.ne.jp/foo にリクエストが飛ぶ。
リダイレクト
リダイレクトした場合の挙動も注意したい。
html が http://example.com/ だったとする。
<link rel="stylesheet" href="/foo.css">
そこから http://example.com/foo.css へのリクエストが飛び、それが https://static.example.com/foo.css にリダイレクトされたとする。
html {
background: url('bar.jpg');
}
CSS の BaseURI は、それ自身のレスポンス URI となるため、このイメージへのリクエストは http://static.example.com/bar.jpg になる。
ここまでが前提となる。
Service Worker が絡む場合
ここまでを踏まえて以下を考える。
CSS の場合
html が http://example.com/ だったとする。
<link rel="stylesheet" href="/foo.css">
しかし、このページで以下のような SW が動いていたとする。
self.addEventListener('fetch', event => {
if (event.request.url.endsWith('/foo.css')) {
event.respondWith(
fetch('https://static.example.com/foo.css')
);
}
});
そして、取得された foo.css
が以下だった場合。
html {
background: url('bar.jpg');
}
bar.jpg
のリクエストはどうなるべきだろうか?とういうのが一つ目の問いかけである。
なお、これは「どう挙動するのが直感的か?」という問いかけであり、 今の仕様や実装がどうか は関係なく答えてほしいとしている。
Q1. bar.jpg の URL はどうなるべきか?
http://example.com/bar.jpg or http://static.example.com/bar.jpg もしくはその他
Q2. もし "CSS の BaseURI はそれ自身のレスポンス URL" という原則と違う答えを選ぶ(つまり http://static.example.com/bar.jpg を選ぶ) なら、その理由は何か?
なお、現在 SW におてい event.respondWith(fetch(event.request))
は、透過的(あってもなくても挙動が変わら無いべき)という方針で策定が行われている。
Q3. もし fetch()
が caches.match()
に置き換えられても、同じ挙動となるべきか?
つまり
self.addEventListener('fetch', event => {
if (event.request.url.endsWith('/foo.css')) {
event.respondWith(
fetch('https://static.example.com/foo.css')
);
}
});
が
self.addEventListener('fetch', event => {
if (event.request.url.endsWith('/foo.css')) {
event.respondWith(caches.match(event.request));
}
});
のように、すでに https://static.example.com/foo.css がキャッシュされていて、それが返された場合はどうなるべきかということ。
ページの場合
前述の通り HTML 自体も BaseURI を持っている。
そこで http://example.com に以下の SW が動いており、 http://example.com/next へのリクエストを発生される。
self.addEventListener('fetch', event => {
if (event.request.url.endsWith('/next/')) {
event.respondWith(
fetch('http://example.ne.jp/next.html')
);
}
});
レスポンスは http://example.ne.jp/next.html から取得したものに置き換えられるが、その html が以下だったとする。
<a href="/foo">foo</a>
Q4. http://example.com から http://example.com/next へのリクエストを発生させた後に URL バーに表示される URL は何か?
つまり http://example.com/next が表示されるのか、 http://example.ne.jp/next.html が表示されるのか?それ以外か?
Q5. 遷移した後のリンク (foo) をクリックした後に遷移する URL は何か?
http://example.com/foo か http://example.ne.jp/foo か?それ以外か?
どちらを BaseURL ととるかによる。
Q6. 選んだ答えが CSS の挙動と違う場合、その理由はなぜか?
サブリソースの場合とページ遷移の場合が違うことについての妥当な理由が必要。
Q7. fetch()
が cache.match()
だった場合どうなるべきか?
Q3 同様、キャッシュされていた場合の挙動について。
アンケート
https://jakearchibald.com/2016/service-workers-and-base-uris/ では、上記についての回答をあつめている。
CSS:
Q1. bar.jpg の URL はどうなるべきか?
Q2. もし "CSS の BaseURI はそれ自身のレスポンス URL" という原則と違う答えを選ぶ(つまり http://static.example.com/bar.jpg を選ぶ) なら、その理由は何か?
Q3. もし `fetch()` が `caches.match()` に置き換えられても、同じ挙動となるべきか?
Page:
Q4. http://example.com から http://example.com/next へのリクエストを発生させた後に URL バーに表示される URL は何か?
Q5. 遷移した後のリンク (foo) をクリックした後に遷移する URL は何か?
Q6. 選んだ答えが CSS の挙動と違う場合、その理由はなぜか?
Q7. `fetch()` が `cache.match()` だった場合どうなるべきか?
回答
自分なりに考えてみた。
そもそも SW には二つの使い方があると考えている。
- キャッシュを管理する透過的な Proxy
- 積極的にインタラクションするバックグラウンドワーカ
前者は、透過であるがゆえに SW が有っても無くてもページが成立する ことになる。あることで、オフラインでも動いたり、キャッシュが効いて速度的なメリットが得られたりする。無けれ、 SW が無かった時代と同じ動きになるだけだ。
後者は、 SW をかなり積極的に使うことで、それがないと成り立た無いアプリも作ることができる。
例えば onfetch
で動的にリソースを生成して返すワーカであったり、 Push API でインタラクションをすること自体が目的のものだ。インストールしてなければ動かないし、対応してないブラウザは対象外。ブラウザ拡張などと近い立ち位置と言える。
どちらの使い方も全く問題ないし、用途に合わせて使えばいい。
ただし、今回の問題は SW をどちらの用途で使うかによって、利便性がかなり変わるかと思う。
例えば透過 Proxy として動くためには、 Proxy が無い場合と同様の挙動であることが求められる。そうでないと透過という前提が崩れる。
一方、バックグラウンドワーカの場合、リソースを差し替えたのなら、差し替えた BaseURL が継承されてくれないと、アプリの設計としては難しくなることも想定される。
しかし、後者の場合はその求められる挙動は要件次第だし、ドメインロジックの問題だと考える。理想的な SW は、やはり「透過的」に挙動で切ることを大原則とし、一方で様々なフックで好みの挙動にしていくことができるレイヤであることだと考える。BaseURI を書き換えたいなら、それは暗黙的ではなく、明示的な API によって行われるべきなんじゃないだろうか。
そもそもリバースプロキシが、 DC 内にある別の URL に読み替えていたりするのと同じように考えると別に自然なんじゃないだろうか。
よって、回答はこんな感じになるだろうか。
CSS:
Q1. bar.jpg の URL はどうなるべきか?
http://example.com/bar.jpg
Q2. もし "CSS の BaseURI はそれ自身のレスポンス URL" という原則と違う答えを選ぶ(つまり http://static.example.com/bar.jpg を選ぶ) なら、その理由は何か?
-
Q3. もし `fetch()` が `caches.match()` に置き換えられても、同じ挙動となるべきか?
ブラウザが発行したリクエストは、あくまで http://example.com/bar.jpg なので、同じ。
Page:
Q4. http://example.com から http://example.com/next へのリクエストを発生させた後に URL バーに表示される URL は何か?
http://example.com/next
Q5. 遷移した後のリンク (foo) をクリックした後に遷移する URL は何か?
http://example.com/foo
Q6. 選んだ答えが CSS の挙動と違う場合、その理由はなぜか?
-
Q7. `fetch()` が `cache.match()` だった場合どうなるべきか?
同じ
懸念点としては、例えば Q1 の場合、そのリソースの本来の URL をオリジン含め付け替えていることになる。これによって、そのリソースが意図しない状態に置かれるセキュリティリスクが発生したりしないか、ちょっと不安でもある。
なので、本家に投げる前にもうちょっと煮詰めたい。
あと、このあたりはセキュリティ専門家の意見を聞きたい。