サーバーが自分自身と同じプログラムを配信して、それをブラウザがロードして、どちらでも同じコードが実行され、サーバーだけではなくブラウザからもレスポンスを返す魔法「Service Worker Magic」を紹介します。
Service Worker Magic
こういうことです。
- サーバーはCloudflare Workers、ブラウザはService Workerのプログラムを指す
- サーバーのプログラムはsw.js、ブラウザで動くプログラムもsw.js
- 全く同じ内容かつ同じリソースを参照して、同じように動く
- サーバーsw.jsが自分自身のコードsw.jsを/sw.jsというパスで配信する
- /にアクセスすると- sw.jsがService Workerとして登録される
- /sw/*をService Workerのスコープにする
- /server/helloにアクセスするとサーバーからレスポンスが返る
- /sw/helloにアクセスするとService Workerがインターセプトして、ブラウザからレスポンスが返る
デモ
実際に動いているデモがこちらになります。Chrome devToolsなどでNetworkやConsoleを確認しながら遷移させると挙動がよく分かります。
Service Workerが動かなくなることがあります。その場合は一度トップ/にアクセスするか、それでも動かなければ、ブラウザのService Workerのキャッシュをクリアしたら動きます。
/sw/helloが「Service Worker」から配信されています。
スクリーンキャスト
以下がスクリーンキャストです。「From Server」が/server/helloへの、「From Service Worker」が/sw/helloへのリンクとなっています。
/server/helloに対するリクエストは通常のGETになりますが、Service Workerのスコープ内である/sw/helloはService Workerがインターセプトします。よって、「Hello! From Service Worker!」と表示されます。また、/server/helloだとターミナルのサーバーログへ、/sw/helloはdevToolsへログが吐き出されています。
繰り返しますが、サーバーとService Workerは全く同じリソースを参照しています。sw.jsは一個だけです。
種明かし
種明かしをすると、Cloudflare WorkersではService Worker互換のAPIを使ってプログラムを書くため、全く同じコードがService Workerでも動くというわけです。
コード
以下が抽象化したショートバージョンです。 フレームワークにはHonoを使っています。 繰り返しますがこれがサーバーでもブラウザでも実行されるのです。
// フレームワークにHonoを使っています
import { Hono } from './hono.js'
// Middlewareのimport
import { serveStatic } from './hono.serve-static.js'
import { logger } from './hono.logger.js'
let from = 'Service Worker'
try {
  // Cloudflare Workersの環境変数でFROMの値を"Server"としている
  // サーバーで立ち上がった場合はFROMがServerに、クライアントの場合はService Workerになる
  from = FROM
}
const app = new Hono()
// ロガーMiddleware
// `/sw/*`のログはサーバーではなくdevToolに出る
app.use('/sw/*', logger())
// `/server/*`のログはサーバー側のターミナルで出る
app.use('/server/*', logger())
// 静的ファイル配信のためのMiddleware
// *.jsファイルをそのまま配信する
app.use('/:name{.+.js}', serveStatic({ root: './' }))
// トップページはベタ書きHTMLを出力
// Service Workerを登録する
// `scope: '/sw/'` としているので `/sw/hello`へのアクセスをService Workerがインターセプトする
app.get('/', (c) => {
  const html = `<html><body>
  <script>
    navigator.serviceWorker.register('/sw.js', { scope: '/sw/', type: 'module' })
  </script>
  </body></html>`
  return c.html(html)
})
// ハンドラー
const handler = (c) => {
  const text = `Hello! from ${from}!`
  return c.text(text)
}
// ルーティング
// `/server/hello`はサーバーにアクセスがいく
app.get('/server/hello', handler)
// `/sw/hello`へのアクセスはService Workerがインターセプトする
app.get('/sw/hello', handler)
// addEventListener('fetch'...を発火
app.fire()
まとめ
この「Service Worker Magic」、すごく面白いんですが、面白さが伝わったでしょうかね。 コードは以下の通りです。ローカルでも実行可能なので、試してみてはいかがでしょうか。
loading...
宣伝
僕がスーパーバイザー(謎)を務めるトラベルブックでもテックブログやってます。