2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Service Workerとは何か理解する

Posted at

JavaScriptのシングルスレッドの本質

JavaScriptは「シングルスレッド」で動作します。スレッドとは、プログラムが命令を実行する「実行の流れ」のことです。シングルスレッドということは、一度に一つの処理しか実行できないということを意味します。

ブラウザのメインスレッドでは、DOM操作、イベント処理、JavaScriptコードの実行、画面の描画更新がすべて同じ一本の流れで順番に処理されます。そのため、重い処理が始まると、他のすべての処理が待機状態になってしまいます。

Blobとは何か

Web Workerを理解する前に、Blobという重要な概念を理解する必要があります。

Blob(Binary Large Object)は、ブラウザ上でバイナリデータやテキストデータを扱うためのオブジェクトです。ファイルのようなデータの塊を表現し、そのデータにアクセスするためのURLを生成できます。

// テキストデータからBlobを作成
const textData = "console.log('Hello World');";
const blob = new Blob([textData], { type: 'application/javascript' });

// BlobからURLを生成(このURLでデータにアクセス可能)
const blobUrl = URL.createObjectURL(blob);
console.log(blobUrl); // blob:http://localhost:3000/12345678-1234-1234-1234-123456789abc

このBlobURLは、まるで実際のファイルのURLのように扱えますが、実際にはブラウザのメモリ上に存在するデータを指しています。Web WorkerはJavaScriptファイルのURLを必要とするため、文字列のコードをBlobに変換してURLを生成することで、別ファイルを作らずにWorkerを作成できます。

Web Workerの仕組みと実装

Web Workerは、メインスレッドとは別の独立したスレッドでJavaScriptを実行する仕組みです。メインスレッドとWorkerスレッドは完全に分離されており、変数の直接共有はできません。代わりにpostMessageonmessageを使ってデータを交換します。

実際に動作する例を見てみましょう:

<!DOCTYPE html>
<html>
<head>
    <title>Web Worker完全解説</title>
</head>
<body>
    <h1>Web Worker デモ</h1>
    <button id="heavyTaskBtn">重い処理(メインスレッド)</button>
    <button id="workerTaskBtn">重い処理(Web Worker)</button>
    <button id="testBtn">反応テスト用ボタン</button>
    
    <div id="status">待機中</div>
    <div id="counter">カウンター: 0</div>

    <script>
        // 指定時間だけ同期的にブロックする関数
        function blockTime(timeout) {
            const startTime = Date.now();
            while (true) {
                const diffTime = Date.now() - startTime;
                if (diffTime >= timeout) {
                    return;
                }
            }
        }

        // UIが生きているかを確認するカウンター
        let counter = 0;
        setInterval(() => {
            counter++;
            document.getElementById('counter').textContent = `カウンター: ${counter}`;
        }, 1000);

        // メインスレッドでの重い処理
        document.getElementById('heavyTaskBtn').addEventListener('click', () => {
            document.getElementById('status').textContent = '重い処理中...(メインスレッド)';
            
            // この処理中はブラウザが完全に固まる
            blockTime(3000);
            
            document.getElementById('status').textContent = '処理完了!(メインスレッド)';
        });

        // 反応テスト用
        document.getElementById('testBtn').addEventListener('click', () => {
            alert('ボタンが反応しました!');
        });

        // Web Workerでの重い処理
        document.getElementById('workerTaskBtn').addEventListener('click', () => {
            document.getElementById('status').textContent = '重い処理中...(Web Worker)';
            
            // Step 1: Workerで実行するコードを文字列として定義
            const workerCode = `
                // Worker内で使用する重い処理関数
                function blockTime(timeout) {
                    const startTime = Date.now();
                    while (true) {
                        const diffTime = Date.now() - startTime;
                        if (diffTime >= timeout) {
                            return;
                        }
                    }
                }
                
                // メインスレッドからのメッセージを受信するリスナー
                self.onmessage = function(event) {
                    const { timeout } = event.data;
                    
                    // 重い処理を実行(別スレッドなのでメインスレッドに影響なし)
                    blockTime(timeout);
                    
                    // 処理完了をメインスレッドに通知
                    self.postMessage({ 
                        result: '処理完了!',
                        processedTime: timeout 
                    });
                };
            `;
            
            // Step 2: 文字列コードをBlobに変換
            const blob = new Blob([workerCode], { 
                type: 'application/javascript' 
            });
            
            // Step 3: BlobからURLを生成
            const workerUrl = URL.createObjectURL(blob);
            
            // Step 4: URLを使ってWorkerを作成
            const worker = new Worker(workerUrl);
            
            // Step 5: Workerに処理開始を指示
            worker.postMessage({ timeout: 3000 });
            
            // Step 6: Workerからの完了通知を受信
            worker.onmessage = function(event) {
                const { result, processedTime } = event.data;
                document.getElementById('status').textContent = 
                    `${result}${processedTime}ms処理、Web Worker使用)`;
                
                // Step 7: 使用後のクリーンアップ
                worker.terminate(); // Workerを終了
                URL.revokeObjectURL(workerUrl); // BlobURLを解放
            };
            
            // エラーハンドリング
            worker.onerror = function(error) {
                console.error('Worker Error:', error);
                document.getElementById('status').textContent = 'エラーが発生しました';
            };
        });
    </script>
</body>
</html>

実行の流れと仕組みの詳細解説

Blobを使ったWorker作成の流れ:

  1. コード文字列の準備: Worker内で実行したいJavaScriptコードを文字列として定義
  2. Blob作成: new Blob([コード], {type: 'application/javascript'})でJavaScriptとして実行可能なBlobを作成
  3. URL生成: URL.createObjectURL(blob)でBlobにアクセス可能なURLを生成
  4. Worker作成: 生成されたURLを使ってnew Worker(url)でWorkerインスタンスを作成

メッセージ通信の仕組み:

  • メインスレッド → Worker: worker.postMessage(data)でデータを送信
  • Worker → メインスレッド: self.postMessage(data)でデータを送信
  • 受信: 両側でonmessageイベントリスナーでメッセージを受信

重要なポイント:

  • Workerは完全に独立したスレッドで動作するため、メインスレッドの変数に直接アクセスできません
  • データは「コピー」として送受信されるため、巨大なオブジェクトの送信は注意が必要です
  • 使用後はworker.terminate()URL.revokeObjectURL()でリソースを適切に解放します

実際にこのHTMLファイルをブラウザで開いて、「重い処理(メインスレッド)」と「重い処理(Web Worker)」の違いを体験してください。メインスレッドでは3秒間ブラウザが固まりますが、Web Workerでは処理中もカウンターが動き続け、ボタンも反応することが確認できます。

この基本的な仕組みを理解できれば、Service Workerがどのようにネットワーク制御に特化した機能を提供するのかも理解しやすくなります。

Web Workerの仕組みが理解できたので、Service Workerについて学習しましょう。Service Workerは、Web Workerの特殊な形態で、ネットワーク通信の制御に特化した機能を持っています。

Service WorkerとWeb Workerの根本的な違い

Web Workerは計算処理を別スレッドで実行するためのものでしたが、Service Workerは「ブラウザとサーバーの間に割り込んで、ネットワーク通信を制御する」ための仕組みです。

Web Workerは作成したページが開いている間だけ動作しますが、Service Workerはページが閉じられても背景で動き続けることができます。また、Web Workerは一つのページに対して動作しますが、Service Workerは「スコープ」と呼ばれる範囲内のすべてのページを管理できます。

最も重要な違いは、Service Workerが「fetchイベント」を監視できることです。ページがfetchやaxiosでAPI通信を行ったり、画像やCSSファイルを読み込んだりする際、Service Workerがその通信を「横取り」して、独自の処理を行えます。

Service Workerのライフサイクル

Service Workerには明確なライフサイクルがあります。

Install(インストール)フェーズでは、Service Workerが初めて登録されたときに実行されます。主にキャッシュの準備や初期設定を行います。

Activate(アクティベート)フェーズでは、Service Workerが有効になるときに実行されます。古いバージョンのキャッシュ削除や、新しいバージョンへの移行処理を行います。

Fetch(実行)フェーズでは、実際にネットワークリクエストを監視し、キャッシュ戦略に基づいて応答を返します。

実際の実装で理解する

Service Workerの動作を実際に体験できる例を作成しました:

index.html

<!DOCTYPE html>
<html>
<head>
    <title>Service Worker デモ</title>
</head>
<body>
    <h1>Service Worker デモ</h1>
    <button id="registerBtn">Service Worker登録</button>
    <button id="fetchBtn">データ取得テスト</button>
    <button id="unregisterBtn">Service Worker解除</button>
    
    <div id="status">Service Worker未登録</div>
    <div id="response">レスポンス結果が表示されます</div>

    <script>
        // Service Worker登録
        document.getElementById('registerBtn').addEventListener('click', async () => {
            if ('serviceWorker' in navigator) {
                try {
                    // 実際のJavaScriptファイルからService Workerを登録
                    const registration = await navigator.serviceWorker.register('./sw.js');
                    console.log('Service Worker registered:', registration);
                    
                    document.getElementById('status').textContent = 
                        'Service Worker登録完了(スコープ: ' + registration.scope + '';
                        
                } catch (error) {
                    console.error('Service Worker registration failed:', error);
                    document.getElementById('status').textContent = 
                        'Service Worker登録失敗: ' + error.message;
                }
            } else {
                document.getElementById('status').textContent = 'Service Workerはサポートされていません';
            }
        });

        // データ取得テスト
        document.getElementById('fetchBtn').addEventListener('click', async () => {
            try {
                document.getElementById('response').textContent = 'データ取得中...';
                
                // Service Workerが監視するAPIリクエスト
                const response = await fetch('/api/test?timestamp=' + Date.now());
                const data = await response.json();
                
                document.getElementById('response').innerHTML = 
                    '<strong>取得データ:</strong><br>' + 
                    '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
                    
            } catch (error) {
                document.getElementById('response').textContent = 'エラー: ' + error.message;
            }
        });

        // Service Worker解除
        document.getElementById('unregisterBtn').addEventListener('click', async () => {
            if ('serviceWorker' in navigator) {
                const registrations = await navigator.serviceWorker.getRegistrations();
                for (let registration of registrations) {
                    await registration.unregister();
                    console.log('Service Worker unregistered');
                }
                document.getElementById('status').textContent = 'Service Worker解除完了';
                
                // キャッシュも削除
                const cacheNames = await caches.keys();
                for (let cacheName of cacheNames) {
                    await caches.delete(cacheName);
                    console.log('Cache deleted:', cacheName);
                }
            }
        });

        // Service Workerの状態変化を監視
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.addEventListener('controllerchange', () => {
                console.log('Service Worker controller changed');
            });
            
            // 既に登録されているService Workerをチェック
            navigator.serviceWorker.ready.then(registration => {
                if (registration.active) {
                    document.getElementById('status').textContent = 
                        'Service Worker既に登録済み(スコープ: ' + registration.scope + '';
                }
            });
        }
    </script>
</body>
</html>

sw.js(Service Worker本体)

// キャッシュ名とバージョン管理
const CACHE_NAME = 'demo-cache-v1';
const urlsToCache = [
    '/',
    './index.html'
];

// インストールイベント:初回登録時に実行
self.addEventListener('install', event => {
    console.log('Service Worker: Install event');
    
    // キャッシュの準備
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => {
                console.log('Cache opened');
                // 静的リソースをキャッシュに保存
                return cache.addAll(urlsToCache);
            })
            .catch(error => {
                console.error('Cache setup failed:', error);
            })
    );
    
    // 新しいService Workerをすぐに有効化
    self.skipWaiting();
});

// アクティベートイベント:Service Workerが有効になるときに実行
self.addEventListener('activate', event => {
    console.log('Service Worker: Activate event');
    
    // 古いキャッシュの削除
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cacheName => {
                    if (cacheName !== CACHE_NAME) {
                        console.log('Deleting old cache:', cacheName);
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
    
    // すべてのクライアントでService Workerを有効化
    return self.clients.claim();
});

// フェッチイベント:ネットワークリクエストを監視
self.addEventListener('fetch', event => {
    console.log('Service Worker: Fetch event for', event.request.url);
    
    // 特定のAPIリクエストに対する処理
    if (event.request.url.includes('/api/test')) {
        event.respondWith(
            // キャッシュファーストの戦略
            caches.match(event.request)
                .then(response => {
                    if (response) {
                        console.log('Cache hit:', event.request.url);
                        // キャッシュのレスポンスに追加情報を付与
                        return response.clone().json().then(data => {
                            data.source = 'Service Worker Cache';
                            data.cachedAt = new Date().toISOString();
                            return new Response(JSON.stringify(data), {
                                headers: { 'Content-Type': 'application/json' }
                            });
                        });
                    }
                    
                    // キャッシュになければダミーデータを生成
                    console.log('No cache, creating dummy response for:', event.request.url);
                    const dummyData = {
                        data: 'Service Workerからのダミーデータ',
                        source: 'Service Worker Generated',
                        timestamp: new Date().toISOString(),
                        url: event.request.url,
                        method: event.request.method
                    };
                    
                    const response = new Response(
                        JSON.stringify(dummyData),
                        {
                            status: 200,
                            headers: { 'Content-Type': 'application/json' }
                        }
                    );
                    
                    // レスポンスをキャッシュに保存
                    caches.open(CACHE_NAME)
                        .then(cache => {
                            cache.put(event.request, response.clone());
                        });
                    
                    return response;
                })
                .catch(error => {
                    console.error('Fetch error:', error);
                    // エラー時のフォールバック
                    return new Response(
                        JSON.stringify({ 
                            error: 'Service Worker処理中にエラーが発生しました',
                            message: error.message,
                            timestamp: new Date().toISOString()
                        }),
                        {
                            status: 500,
                            headers: { 'Content-Type': 'application/json' }
                        }
                    );
                })
        );
    }
});

// エラーハンドリング
self.addEventListener('error', event => {
    console.error('Service Worker error:', event.error);
});

self.addEventListener('unhandledrejection', event => {
    console.error('Service Worker unhandled rejection:', event.reason);
});

Service Workerの動作を体験する

このHTMLファイルをブラウザで開いて、以下の順序で操作してください:

  1. 「Service Worker登録」ボタンをクリック

    • Service Workerがバックグラウンドで登録されます
    • ブラウザの開発者ツール(F12)→Applicationタブ→Service Workersで状態を確認できます
  2. 「データ取得テスト」ボタンを数回クリック

    • 最初のクリック:ネットワークから取得してキャッシュに保存
    • 2回目以降:キャッシュから高速で取得
    • 開発者ツールのConsoleでログを確認してください
  3. ネットワークを無効にして「データ取得テスト」をクリック

    • 開発者ツール→Networkタブ→「Offline」にチェック
    • Service Workerがオフライン用のダミーデータを返します

Service Workerの重要な特徴

ネットワークプロキシとしての機能:すべてのネットワークリクエストを監視し、キャッシュから返すかネットワークから取得するかを判断できます。

永続性:ページを閉じてもService Workerは背景で動作し続け、プッシュ通知やバックグラウンド同期が可能です。

スコープベースの制御:登録されたパス以下のすべてのページを管理し、一元的なキャッシュ戦略を適用できます。

セキュリティ制限:HTTPSでのみ動作し(localhost除く)、セキュアな環境でのみ利用可能です。

これがService Workerの基本的な仕組みです。Vue.jsでの開発経験と照らし合わせると、axiosでのAPI通信をService Workerが監視し、オフライン対応やキャッシュ戦略を自動的に適用できることが理解できるでしょう。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?