昨日、「CDN のエッジで実行する系」として、Vercel Edge Functions や Cloudflare Workers、Faslty Compute@Edge などを紹介した。
これらが提供する機能とそれを書くための API は、当然ながらプラットフォームごと異なる。 そのため我々は今まで触れたことのないプラットフォームのスクリプトを書くとなれば API リファレンスを読む必要がある。 ただ、共通する点が 2 つある。
1 つはほとんどのエッジサービスは JavaScript で記述できるという点だ。 Fastly Compute@Edge は WebAssembly へ最終的にコンパイルされるが、とにかく JavaScript で書ける。 あるいはトランスコンパイルの環境があれば TypeScript で書ける。
もうひとつ… たいていのプラットフォームはService Workerの API を踏襲しているという点だ。 もともと Web ブラウザで動くことを想定していた Service Worker のことだ。 各プラットフォームが Service Worker のそれを採用することにより、似たようなコードが Cloudflare でも Fastly でも Vercel でも動く。我々は覚えることが少なくなる。 API リファレンスを読む時間を短縮することが出来るだろう。コーヒーでも飲もう!
さて、今回は、エッジで使われている Service Worker API が実際どのようなもので、どう作動するかを例を交えて見ていく。
そもそも Service Worker とは
そもそも Service Worker とはなんぞや。Google Developers のService Worker の紹介というページから引用する。
Service Worker はブラウザが Web ページとは別にバックグラウンドで実行するスクリプトで、Web ページやユーザーのインタラクションを必要としない機能を Web にもたらします。 既に現在、プッシュ通知やバックグラウンド同期が提供されています。
この API にとてもわくわくするのは、それがオフライン体験をサポートし、そして開発者がその体験を完全に制御できるからです。
そう、Service Worker 自体は Web ブラウザのためのものなのだ。
Service Worker でミニサーバーを書く
エッジでは Service Worker の 「API」 が使われている。 あくまで「インターフェース」を踏襲しているわけだ。それを各プラットフォームが実装することになる。
Worker Environmentsという取り組みがある。こう言っている。
Worker Environments are an adaptation of the Service Workers API, which is a browser standard for offline web applications. To give web developers more freedom over offline experiences, the specification includes a (minimal) HTTP server. Since it was published, other vendors have implemented this API for servers that run in the cloud
では、minimal HTTP serverを書くとしたらどう書くか。
self.addEventListener("fetch", (event) => {
event.respondWith(new Response("Hello World"))
})
これでだけである。驚くべきことにこれは Chrome でも Cloudflare Workers でも動く! 一方は Web ブラウザがアクセスをハイジャックするために。一方はミニマルな HTTP サーバーとして。
Service Worker で書けるプラットフォーム
Service Worker API でスクリプトを書けるプラットフォームには以下がある。
- Vercel Edge Functions
- Cloudflare Workers
- Fastly Compute@Edge
- Deno Deploy
先日発表された Next.js 12 の Vercel Edge Functions も含まれている。 ちなみに Edge Functions は Service Worker のものを拡張したものになる。後述する。
主な API
エッジにおける Service Worker でよく使う API を紹介する。
Example
その前に題材となるスクリプトはこちら。
addEventListener("fetch", (event) => {
const request = event.request
const url = new URL(request.url)
const message = url.searchParams.get("message") || " NOTHING "
const response = new Response("Hello", {
status: 200,
headers: {
"x-message": message,
},
})
event.respondWith(response)
})
これを Cloudflare Workers の Yet Another な実装である miniflare で動かしてみよう。といっても、簡単だ。
適当なディレクトリに index.js
と名付けたファイルに上記のコードを記載。
$ miniflare index.js
とやれば http://localhost:8787
にサーバーが立つ。
アクセスをして Response ヘッダを覗くとヘッダにx-message: NOTHING
と出てる。
ではhttp://localhost:8787/?message=hello-service-woker
にアクセスすると GET のクエリパラムを見て
x-message
ヘッダの値が変わるのが分かるだろう。
FetchEvent/Request/Response
基本的な流れとしては
fetch
イベントをリッスンするaddEventListener
からFetchEvent
を受け取るevent.request
として Reqeust オブジェクトをハンドルする- Cloudflare の場合「URL」使えるので、
url
の文字列を一度 URL オブジェクトにする - URL の
searchParams
を使って値を取得する new Response
で新規レスポンスを作る- 本文、ステータス、ヘッダなどを記載して、返却する
である。なので
- FetchEvent
- Request
- Reponse
オブジェクトが肝である。
Next.js Middlware の場合
Next.js の Middleware では、それぞれを拡張したものを使うことが出来る。
NextRequest
ではcookie
や Geo Location を表すgeo
、UserAgent を示すua
といったショートカットプロパティが用意されている。また、NextFetchEvent
ではwaitUntil
が実装されており、Response を返しつつ、処理を待つ、なんてことが出来る。NextResponse
にもredirect()
とrewrite()
といったショートカットメソッドがある。
その他の API
その他にも、プラットフォームによっては、CDN の Cache を扱うため、URL を扱うため、ロギングのための機能・API が用意されている。
例
実装例を見ていこう。
JSON を返す
addEventListener("fetch", (event) => {
const data = {
hello: "world",
}
const json = JSON.stringify(data, null, 2)
const response = new Response(json, {
headers: {
"content-type": "application/json;charset=UTF-8",
},
})
event.respondWith(response)
})
外部の Web ページを取得してそのまま表示
addEventListener("fetch", (event) => {
const response = fetch("https://yusukebe.com/")
event.respondWith(response)
})
パスやパラムを保持したままリダイレクト
addEventListener("fetch", (event) => {
const base = "https://yusukebe.com"
const statusCode = 301
const url = new URL(event.request.url)
const { pathname, search } = url
const destinationURL = base + pathname + search
const response = Response.redirect(destinationURL, statusCode)
event.respondWith(response)
})
Server Push
(HTTP/2 じゃないと動かない?)
const handleRequest = (request) => {
const CSS = `body { color: red; }`
const HTML = `
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/test.css">
</head>
<body>
<h1>Server push test page</h1>
</body>
</html>
`
if (/test\.css$/.test(request.url)) {
return new Response(CSS, {
headers: {
"content-type": "text/css",
},
})
}
return new Response(HTML, {
headers: {
"content-type": "text/html",
Link: "</test.css>; rel=preload; as=style",
},
})
}
addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request))
})
昨日書いたエントリーにはエッジサービスのユースケースが載っているので、そちらも参考にしてもらいたい。
その他所感
HTTP サーバーのインターフェースという意味では、Ruby における Rack や Perl における PSGI を彷彿とさせる、と感じたのは僕だけでしょうか。
同じ Service Worker を使っているので、Web ブラウザでもエッジサービスでも共通で使えるライブラリとかできるのかな、ってかあるのかな。
まとめ
以上、エッジサービスで Service Worker の API が使われている件を紹介した。Web ブラウザで動く Service Worker をサーバーサイドに応用したという点が面白かった。シンプルな構造になるので、エッジサービスで使いやすい。昨日のエントリに引き続き「CDN のエッジで動く系」は気になるトピックですね。