天然パーマです。

エッジにおけるService Worker API

昨日、「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

基本的な流れとしては

  1. fetchイベントをリッスンする
  2. addEventListenerからFetchEventを受け取る
  3. event.requestとして Reqeust オブジェクトをハンドルする
  4. Cloudflare の場合「URL」使えるので、urlの文字列を一度 URL オブジェクトにする
  5. URL のsearchParamsを使って値を取得する
  6. new Responseで新規レスポンスを作る
  7. 本文、ステータス、ヘッダなどを記載して、返却する

である。なので

  • 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 のエッジで動く系」は気になるトピックですね。