天然パーマです。

サーバーとブラウザで「全く」同じコードを実行する Service Worker Magic

サーバーが自分自身と同じプログラムを配信して、それをブラウザがロードして、どちらでも同じコードが実行され、サーバーだけではなくブラウザからもレスポンスを返す魔法「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」、すごく面白いんですが、面白さが伝わったでしょうかね。 コードは以下の通りです。ローカルでも実行可能なので、試してみてはいかがでしょうか。

宣伝

僕がスーパーバイザー(謎)を務めるトラベルブックでもテックブログやってます。