🛰️

Service Workerについて調べたのでメモ

2022/11/19に公開

最初に

Mock Service Worker というサービスワーカーを利用したモッキングライブラリがあります。これを使っているときに、ChromeのDevToolsのネットワークタブで通信を確認するとやたらと歯車(gear)アイコンが付いた通信履歴が発生していて気になったのがきっかけでちょっと調べたのでその覚書です。

※勉強用に使ったリポジトリ: https://github.com/t-shiratori/study-service-worker

そもそもWorkerとは

ウェブワーカー (Web Worker) とは、ウェブアプリケーションにおけるスクリプトの処理をメインとは別のスレッドに移し、バックグラウンドでの実行を可能にする仕組みのことです。時間のかかる処理を別のスレッドに移すことが出来るため、 UI を担当するメインスレッドの処理を中断・遅延させずに実行できるという利点があります。

ざっと特徴を挙げると以下のような点があります。

  • JavaScriptファイルを実行する
  • バックグラウンドのスレッドで実行される
  • windowとは異なるグローバルコンテキストで実行される
  • ワーカーには複数種類がある
    • 専用ワーカー (dedicated worker)
    • 共有ワーカー (shared worker)
    • サービスワーカー
  • ワーカー内から直接 DOM を操作することはできない
  • window オブジェクトの既定のメソッドやプロパティには使用できないものがある
  • ワーカーとメインスレッドとの間では、メッセージのシステム(postMessage)を通してデータをやりとりできる

参照

ServiceWorkerとは

Workerの一種で、ブラウザーとサーバーとの通信に介在しプロキシサーバーのように振る舞います。これにより通信のリクエストやレスポンスに手を加えることができたり、オフライン時にキャッシュしておいた内容を返したりすることができます。セキュリティ上httpsかlocalhost上でしか動作しないようになっています。また、ワーカーはライフサイクルを持っていてinstallinginstalledactivatingactivatedredundantのイベントが存在します。

ちなみに、ChromeのDevToolsを開いてアプリケーション > Service Workersを見ると保存状態が確認できます。

参照
サービスワーカー API - Web API | MDN

登録して使う

メインスレッドで Navigator.serviceWorker を使ってService Workerとして実行したいjsファイルを登録するところから始まります。登録が成功したらブラウザにダウンロードされ、ユーザーがアクセスした URLのオリジン内全体、または指定したそのサブセット内に対してインストールされ有効化されます。

src/main.ts
const registration = await navigator.serviceWorker.register('/service-worker.js', {
	scope: '/',
})

serviceWorker.registerServiceWorkerRegistration を作成してそれを返します。
ServiceWorkerRegistration の内容をconsoleで表示すると以下のようになっています。

プロパティの activeinstallingwaiting、にそのフェーズのServiceWorkerの内容が格納されます。

一方でサービスワーカー側では登録が成功したらライフサイクルイベントが発生し、そのイベントに合わせて処理を登録することができます。selfServiceWorkerGlobalScope でサービスワーカーのグローバル実行コンテキストを表します。

public/service-worker.js
self.addEventListener('install', (event) => {})
self.addEventListener('activate', (event) => {})
self.addEventListener('fetch', (event) => {})

キャッシュの機能を使う

public/service-worker.js
const CACHE_NAME = 'v1'
const CACHE_ASSETS = ['/', '/src/main.ts', '/vite.svg', '/1000x1000_1.png', '/1000x1000_2.png']

self.addEventListener('install', (event) => {
	// キャッシュの追加処理が完了するまでインストールが終わらないように待つ
	event.waitUntil(
		// キャッシュを開いてキャッシュストレージに追加する
		caches.open(CACHE_NAME).then((cache) => cache.addAll(CACHE_ASSETS)),
	)
})

cachesCacheStorage API です。CacheStorage APIを使ってキャッシュに追加したり削除したりといった操作ができます。

キャッシュストレージの保存内容をChromeのDevToolsを開いてアプリケーション > キャッシュ > キャッシュストレージで確認できます。

リクエストに介入する

サービスワーカーが登録された後で、ブラウザからリクエストが発生すると、それを感知してFetchEventが発火します。FetchEvent.request でリクエスト情報を参照できます。FetchEvent.respondWith() を使うとブラウザー既定のフェッチ処理を抑止しレスポンスをカスタマイズできます。
以下のコードはキャッシュが存在するかチェックし、あればキャッシュを返します。キャッシュがない場合はサーバーにリクエストを投げます。その際特定のAPIのリクエストだけレスポンスをキャッシュに保存するということをやっています。

public/service-worker.js
const handleFetch = async (event) => {
	// キャッシュが存在するかどうかをチェックする
	const cachedResponse = await caches.match(event.request)

	// キャッシュがあればキャッシュをレスポンスとして返す
	if (cachedResponse) return cachedResponse

	// キャッシュがない場合はサーバーに取りに行く
	// APIリクエストの場合はレスポンスをキャッシュする

	try {
		if (!event.request.url.includes(API_DOMAIN)) return fetch(event.request)

		const responseFromNetwork = await fetch(event.request)

		const cache = await caches.open('v1')

		// レスポンスを複製してキャッシュに追加する
		await cache.put(event.request, responseFromNetwork.clone())

		return responseFromNetwork
	} catch (error) {}
}

// サービスワーカ-登録済でリクエストを感知した際に発火するイベント
self.addEventListener('fetch', (event) => {
	// ブラウザー既定のfetchハンドリングを抑止して、 自前でResponse用のPromiseを提供する
	event.respondWith(handleFetch(event))
})

通信を確認する

登録時(サービスワーカー未登録)

ChromeのDevToolsのネットワークタブをで確認すると、同じリソース名で2つ通信が確認できます。まずページからリクエストが発生し、それをサービスワーカーがインターセプトし、サーバーにリクエストを送ります。このとき一方にだけ歯車アイコンが付いています。これはサービスワーカーからサーバーへリクエストされた通信に対して歯車アイコンが付けられているようです。(参照: chrome dev tools の Network タブの歯車アイコンは何を表す? - スタック・オーバーフロー

2回目読み込み時(サービスワーカー登録済)

2回目以降はキャッシュがあるためサービスワーカーからサーバーへの通信は発生しません。また、サービスワーカーからレスポンスが返されているのでステータスコードの後ろに(service worker から)と表記も追加されています。

Discussion