非同期処理の応用

このカリキュラムでは、JavaScriptの非同期処理について深く掘り下げ、その設計パターンや実践的な使い方を学びます。
Promiseやfetch APIWebSocketなどを駆使したリアルタイムアプリの開発や、エラーハンドリングと再試行ロジックまで、即戦力となるスキルを身につけることを目的としています。

1.非同期処理の応用

1.1 非同期処理の設計パターン

Promiseベースの設計

Promiseを使った設計では、複数の非同期処理を効率的に組み合わせることができます。

Promise.all() の活用

複数の非同期処理を並列実行し、全ての処理が成功した後に次の処理を実行します。

  • 例:

    const promise1 = new Promise((resolve) => setTimeout(() => resolve('タスク1完了'), 1000));

    const promise2 = new Promise((resolve) => setTimeout(() => resolve('タスク2完了'), 2000));

    Promise.all([promise1, promise2]) .then((results) => console.log(results)) // ["タスク1完了", "タスク2完了"] .catch((error) => console.error(error));
  • 用途:
    • 複数のAPIリクエストを同時に処理する場合。
    • 並列タスクの進行が求められる場面。

Promise.race() の活用

複数の非同期処理のうち、最初に完了した処理の結果を使用します。

  • 例:
    const promise1 = new Promise((resolve) => setTimeout(() => resolve('タスク1完了'), 1000));

    const promise2 = new Promise((resolve) => setTimeout(() => resolve('タスク2完了'), 500));

    Promise.race([promise1, promise2]) .then((result) => console.log(result)) // "タスク2完了" .catch((error) => console.error(error));
  • 用途:
    • 最速で応答を得たい場合(例: 予備のAPIリクエスト)。

1.2 エラーハンドリングの考え方

.catch() と try-catch の違いと使いどころ

  • .catch(): Promiseチェーンの中でエラーをキャッチする。

    fetch('https://api.example.com/data')
      .then((response) => response.json())
      .then((data) => console.log(data))
      .catch((error) => console.error('エラー発生:', error));
  • try-catch: async/await構文内でエラーをキャッチする。

    async function fetchData() {
     try {
       const response = await fetch('https://api.example.com/data');
       const data = await response.json(); console.log(data);
     } catch (error) {
      console.error('エラー発生:', error);
     }
    }

非同期処理におけるエラー伝播の仕組み

  • Promiseチェーン内でエラーが発生すると、そのエラーは次の.catch()に伝播されます。
  • async/awaitでは、awaitで発生したエラーはtry-catchでキャッチ可能です。

2.Web APIとの連携

2.1 fetch APIの詳細

fetch APIとは?

fetch APIは、ブラウザ内でネットワークリクエストを行うためのJavaScriptのモダンなインターフェースです。
HTTPリクエストを送信し、サーバーからデータを取得したり、送信したりする用途で使われます。

fetch APIの特徴:

  • Promiseベース:
    非同期処理をPromiseとして扱うため、可読性が高く、thencatchで簡単に処理を記述できます。
  • 柔軟な設計:
    HTTPメソッド(GET, POST, PUT, DELETEなど)の指定や、リクエストヘッダー、ボディの柔軟な設定が可能。
  • 標準対応:
    モダンブラウザのほとんどでサポートされており、軽量です。

2.2 XMLHttpRequestとの違い

fetch APIと従来のXMLHttpRequest (XHR)の違いを以下に比較します。

特徴fetch APIXMLHttpRequest
非同期処理Promiseベースで扱いやすいコールバックが必要で複雑
API設計シンプルで直感的冗長なコードになる
Stream対応対応非対応
ブラウザサポートモダンブラウザ向け古いブラウザでも対応

例えば、同じデータを取得するコードも、以下のようにfetch APIのほうが簡潔です。

fetch APIの例

fetch('https://api.example.com/data')
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error('エラー:', error));

XMLHttpRequestの例

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data');
xhr.onload = function () {
if (xhr.status === 200) {
console.log(JSON.parse(xhr.responseText));
} else {
console.error('エラー:', xhr.status);
}
};
xhr.onerror = function () {
console.error('ネットワークエラー');
};
xhr.send();

2.3 基本的なfetch APIの使い方

1. GETリクエスト: データ取得の例

GETリクエストはサーバーからデータを取得するために使用します。

javascriptコピーする編集するfetch('https://api.example.com/data')
    .then((response) => {
        if (!response.ok) {
            throw new Error(`HTTPエラー: ${response.status}`);
        }
        return response.json();
    })
    .then((data) => console.log(data)) // 取得したデータを表示
    .catch((error) => console.error('エラー:', error));

2. POSTリクエスト: サーバーへのデータ送信例

POSTリクエストは、サーバーにデータを送信するときに使用します。

javascriptコピーする編集するfetch('https://api.example.com/data', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({ name: 'John', age: 30 }), // 送信するデータ
})
    .then((response) => {
        if (!response.ok) {
            throw new Error(`HTTPエラー: ${response.status}`);
        }
        return response.json();
    })
    .then((data) => console.log('送信成功:', data))
    .catch((error) => console.error('エラー:', error));

3. ヘッダーの設定と認証トークンの利用

APIによっては、認証トークンをヘッダーに付与する必要があります。

javascriptコピーする編集するfetch('https://api.example.com/protected-data', {
    method: 'GET',
    headers: {
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
    },
})
    .then((response) => {
        if (!response.ok) {
            throw new Error(`HTTPエラー: ${response.status}`);
        }
        return response.json();
    })
    .then((data) => console.log(data))
    .catch((error) => console.error('エラー:', error));

2.4 fetch APIのエラーハンドリング

1. statusコードによるエラー判定

HTTPレスポンスのstatusコードをチェックして、エラーを判定します。

  • 200系:
    成功
  • 400系:
    クライアントエラー(例: 404 Not Found)
  • 500系:
    サーバーエラー(例: 500 Internal Server Error)

fetch('https://api.example.com/data')
.then((response) => {
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
return response.json();
})
.catch((error) => console.error('エラー:', error));

2. ネットワークエラーとHTTPエラーの違い

  • ネットワークエラー:
    サーバーとの接続に失敗した場合に発生(例: サーバーダウンや接続タイムアウト)。
  • HTTPエラー:
    サーバーが返すHTTPステータスコードによって判断。

2.5 エラーハンドリングと再試行ロジック

再試行ロジックの実装

ネットワーク通信は、不安定な環境下でエラーが発生しやすいため、一定回数再試行する仕組みを取り入れることで、ユーザー体験を向上させることができます。

1. ネットワークエラーが発生した際のリトライ戦略

ネットワークエラーが発生した場合、指定回数だけリトライを試みます。
以下は、再試行ロジックの基本例です。

javascriptコピーする編集するasync function fetchWithRetry(url, options, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            const response = await fetch(url, options);
            if (!response.ok) {
                throw new Error(`HTTPエラー: ${response.status}`);
            }
            return await response.json();
        } catch (error) {
            if (i === retries - 1) throw error; // リトライ回数を超えたらエラーをスロー
            console.warn(`リトライ中 (${i + 1}/${retries})...`);
        }
    }
}

2. 最大リトライ回数の設定例

retriesパラメータを指定して、最大リトライ回数をコントロールします。

fetchWithRetry('https://api.example.com/data', {}, 5) // 最大5回再試行
.then((data) => console.log(data))
.catch((error) => console.error('最終的に失敗:', error));

3. リトライ間隔の工夫(指数バックオフなど)

再試行時に、単純に再実行するのではなく、再試行間隔を増やしていく「指数バックオフ」を実装することで、サーバーへの負荷を軽減できます。

async function fetchWithExponentialBackoff(url, options, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
return await response.json();
} catch (error) {
if (i === retries - 1) throw error;
const backoffDelay = delay * Math.pow(2, i); // 指数バックオフ
console.warn(`リトライ中 (${i + 1}/${retries}), ${backoffDelay}ms 待機`);
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
}
}
}

3.リアルタイムアプリ開発に向けた非同期処理

3.1 WebSocketの基本

WebSocketとは?

WebSocketは、ブラウザとサーバー間での双方向通信を可能にするプロトコルです。
従来のHTTPリクエスト/レスポンスモデルに比べ、低遅延で効率的なデータ通信が可能です。

  • 特徴:
    • 双方向通信が可能で、サーバー側からもデータを送信できる。
    • 接続が一度確立されると、データ交換時にヘッダー情報を含む必要がないため、通信量が削減される。
    • チャットアプリ、リアルタイム通知、ゲーム、ストリーミングなどで利用される。

HTTP通信との違い

項目HTTPWebSocket
通信モデルリクエスト-レスポンス双方向通信
接続の仕組み毎回新しい接続を確立する接続を1回確立し、それを維持する
通信量リクエストごとにヘッダーを送信ヘッダーが不要、効率的な通信
ユースケースAPIリクエスト、静的データ取得チャット、通知、リアルタイム更新

双方向通信が可能な仕組み

WebSocketは、以下のように動作します。

  1. クライアント(ブラウザ)とサーバー間で初期的にHTTPリクエストを送信し、接続を確立します(ハンドシェイク)。
  2. 接続が確立されると、HTTPからWebSocketプロトコルに切り替わります。
  3. その後、接続が切れるまでサーバー・クライアント間で自由にメッセージを送受信できます。

WebSocketの基本的な使い方

サーバーとの接続、メッセージ送信、受信

以下は、基本的なWebSocketのコード例です。

// WebSocket接続を確立
const socket = new WebSocket('wss://example.com/socket');

// 接続が確立したとき
socket.addEventListener('open', () => {
console.log('WebSocket connection opened');
// サーバーにメッセージを送信
socket.send('Hello Server!');
});

// サーバーからメッセージを受信したとき
socket.addEventListener('message', (event) => {
console.log('Message from server:', event.data);
});

// 接続が閉じられたとき
socket.addEventListener('close', () => {
console.log('WebSocket connection closed');
});

// エラーが発生したとき
socket.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
});


WebSocketのイベント

イベント名説明
open接続が確立された際に発生します。ここで初期メッセージをサーバーに送信できます。
messageサーバーからのメッセージを受信したときに発生します。
close接続が終了した際に発生します。サーバーまたはクライアント側で終了をトリガー可能です。
error接続中にエラーが発生した際に発生します。エラーの詳細を確認し、再接続を試みるきっかけになります。

3.2 WebSocketの応用

非同期処理を活用したリアルタイム通信

WebSocketを利用すると、非同期処理を活用して効率的なリアルタイム通信を実現できます。
例えば、複数の非同期メッセージを送信・受信する際のキュー管理や、接続が切れた際の再接続ロジックが重要なポイントになります。


メッセージのキュー管理

リアルタイム通信では、通信が不安定な場合や接続が一時的に切れた場合に備え、メッセージを一時的に保存(キューイング)する仕組みが役立ちます。

キュー管理の例:

以下は、送信メッセージをキューに格納し、接続が復旧した際に再送信する例です。


let messageQueue = []; // 送信キュー
let isConnected = false; // 接続状態を追跡

const socket = new WebSocket('wss://example.com/socket');

// 接続が確立した際に、キューのメッセージを送信
socket.addEventListener('open', () => {
isConnected = true;
console.log('WebSocket connection established');

// キューに溜まったメッセージを送信
while (messageQueue.length > 0) {
const message = messageQueue.shift(); // キューから取得
socket.send(message);
}
});

// メッセージを送信する関数
function sendMessage(message) {
if (isConnected) {
socket.send(message);
} else {
console.log('Connection lost. Queuing message:', message);
messageQueue.push(message); // キューに追加
}
}

通信の再接続ロジック

WebSocketの接続が切れることはよくあるため、自動的に再接続する仕組みを設計する必要があります。

再接続ロジックの例:

指数バックオフを用いて、再接続の待機時間を段階的に増やす戦略を取り入れます。


let retryAttempts = 0; // 再接続試行回数
const maxRetries = 5; // 最大再接続回数
let socket;

function connectWebSocket() {
socket = new WebSocket('wss://example.com/socket');

// 接続が確立されたとき
socket.addEventListener('open', () => {
console.log('WebSocket connection established');
retryAttempts = 0; // 再接続カウンターをリセット
});

// 接続が閉じられたとき
socket.addEventListener('close', () => {
console.log('WebSocket connection closed');
if (retryAttempts < maxRetries) {
const retryDelay = Math.min(1000 * 2 retryAttempts, 30000); // 指数バックオフ
retryAttempts++;
setTimeout(connectWebSocket, retryDelay);
} else {
console.error('Max retries reached. Could not reconnect.');
}
});

// エラーが発生したとき
socket.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
});
}

// 初回接続
connectWebSocket();

リアルタイム通信のユースケース

  • チャットアプリ:
    WebSocketを用いたリアルタイムメッセージングアプリ。
  • 通知システム:
    サーバーからのリアルタイム通知(例: 新しいメッセージやアラート)。
  • データストリーミング:
    株価、ライブスコア、SNSフィードの更新。

まとめ

リアルタイムアプリ開発では、非同期処理を活用した効率的なデータ通信が不可欠です。
本記事では、WebSocketの基本から応用までを解説し、双方向通信の仕組みや、接続維持のためのキュー管理、再接続ロジックについて学びました。

重要なポイントを振り返ると…

  1. WebSocketの基本
    WebSocketは、双方向通信を可能にするプロトコルであり、HTTP通信よりも効率的なデータ交換を実現します。
    基本的なイベント (open, message, close, error) を理解することで、リアルタイム通信の基盤を構築できます。
  2. 非同期処理の応用
    メッセージのキュー管理により、接続が不安定な場合でもデータを損失せず、再接続ロジックを活用して安定した通信を確保できます。
  3. 応用例とユースケース
    チャットアプリ、通知システム、ストリーミングデータの表示など、現実のユースケースを想定した設計が可能になります。
SHARE