0
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?

Angularを学ぶ HTTPクライアント

Posted at

ほとんどのフロントエンドアプリは、サーバーと HTTP で通信してデータを取得・送信したり、バックエンドサービスにアクセスしたりする必要がある。Angular では、@angular/common/http モジュールの HttpClient サービスを使って、これらの操作を簡単に行える。

HttpClient の設定

依存性注入による HttpClient の提供

HttpClientprovideHttpClient() を使ってアプリに提供される。多くのアプリでは、app.config.tsproviders にこれを追加して利用可能にしている。

export const appConfig: ApplicationConfig = {
    providers: [
        provideHttpClient(),
    ]
};

NgModuleベースのブートストラップの場合、AppModuleproviders 配列に provideHttpClient() を追加して HttpClient を提供できる。

@NgModule({
    providers: [
        provideHttpClient(),
    ],
    // ...その他のアプリケーション設定
})
export class AppModule {}

その後、HttpClient サービスを、依存関係として注入できる。

@Injectable({providedIn: 'root'})
export class ConfigService {
    private http = inject(HttpClient);
}

HttpClient の設定

provideHttpClientHttpClient を提供すると同時に、クライアントの動作をカスタマイズするオプション設定を受け取れる。以下で触れるオプション以外にもいくつかある。

withFetch

export const appConfig: ApplicationConfig = {
    providers: [
        provideHttpClient(
            withFetch(),
        ),
    ]
};

デフォルトでは HttpClientXMLHttpRequest を使う。withFetch を有効にすると fetch API に切り替え可能だが、進行状況イベントの取得など一部機能に制限がある。

  • XMLHttpRequest (XHR):昔ながらの HTTP 通信 API。進行状況の取得が可能で互換性が高いが、書き方がやや複雑。
  • Fetch API:新しい HTTP 通信 API。Promise や async/await で扱いやすいが、進行状況取得はできず、古いブラウザでは未対応の場合あり。

withInterceptors

withInterceptors は、HttpClient のリクエストやレスポンスを処理するインターセプター関数を設定する機能。インターセプターなどについては後で詳細に触れる。

export const appConfig: ApplicationConfig = {
    providers: [
        provideHttpClient(
            withInterceptors([loggingInterceptor, cachingInterceptor]),
        ),
    ]
};

リクエストの実行

HttpClient は、GET や POST などの HTTP メソッドに対応したメソッドを提供し、それぞれ RxJS の Observable を返す。Observable にサブスクライブするとリクエストが送信され、サーバーの応答が届くと結果が受け取れる。

JSON データの取得

HttpClient.get() は、指定した URL に GET リクエストを送信してデータを取得する。第1引数に URL、第2引数にオプションオブジェクトを指定できる。ジェネリック型引数を使うと、返されるデータの型を明示でき、省略すると Object 型になる。サブスクライブするとリクエストが送信され、サーバーからの応答を受け取れる。

http.get<Config>('/api/config').subscribe(config => {
    // 処理
});

データの構造が不確実な場合や、値が undefinednull になる可能性がある場合は、Object ではなく unknown 型をレスポンス型に使うと安全。

他のタイプのデータの取得

デフォルトでは、HttpClient はサーバーからの JSON データを想定している。JSON 以外を扱う場合は、responseType オプションで返される型を指定できる。

responseType 値 返されるレスポンス型
'json' (デフォルト) 指定されたジェネリック型の JSON データ
'text' 文字列データ
'arraybuffer' 応答バイトの生のデータを格納した ArrayBuffer
'blob' Blob インスタンス

例:JPEG画像を ArrayBuffer で取得する場合

http.get('/images/dog.jpg', {responseType: 'arraybuffer'}).subscribe(buffer => {
    console.log('画像は ' + buffer.byteLength + ' バイトです');
});

サーバーの状態を変更する

変更を伴う操作では POST リクエストでデータを送信する。HttpClient.post() は body 引数で送信データを受け取り、サーバーから結果を受け取れる。

http.post<Config>('/api/config', newConfig).subscribe(config => {
    console.log('更新された構成:', config);
});

HttpClient は、送信する body の型に応じて自動的にデータをシリアル化する。たとえば文字列はプレーンテキスト、オブジェクトや配列は JSON、FormData は multipart/form-data などに変換される。

body 型 シリアル化されるもの
文字列 プレーンテキスト
数値、ブール値、配列、プレーンオブジェクト JSON
ArrayBuffer バッファーからの生データ
Blob Blob のコンテンツタイプを使用した生データ
FormData multipart/form-data エンコードされたデータ
HttpParams または URLSearchParams application/x-www-form-urlencoded 形式の文字列

HttpClient の Observable は サブスクライブしないとリクエストが送信されない。必ず subscribe() を呼び出す必要がある。

URL パラメータの設定

リクエストに含める URL パラメータは、params オプションで指定できる。

簡単には、オブジェクトリテラルを渡してキーと値を設定する方法がある。

http.get('/api/config', {
    params: {filter: 'all'},
}).subscribe(config => {
    // ...
});

パラメータの生成やシリアル化を細かく制御したい場合は、HttpParams のインスタンスを params に渡す。

const baseParams = new HttpParams().set('filter', 'all');
http.get('/api/config', {
    params: baseParams.set('details', 'enabled'),
}).subscribe(config => {
    // ...
});

リクエストヘッダーの設定

リクエストヘッダーを指定する場合は headers オプションを使う。

簡単にはオブジェクトリテラルを渡して設定できる。

http.get('/api/config', {
    headers: {
        'X-Debug-Level': 'verbose',
    }
}).subscribe(config => {
    // ...
});

ヘッダーを細かく制御したい場合は、HttpHeaders のインスタンスを渡す。

const baseHeaders = new HttpHeaders().set('X-Debug-Level', 'minimal');
http.get<Config>('/api/config', {
    headers: baseHeaders.set('X-Debug-Level', 'verbose'),
}).subscribe(config => {
    // ...
});

サーバーレスポンスとの対話

observe: 'response' を設定すると、HttpClient はレスポンスボディだけでなく、ヘッダーやステータスなどを含む HttpResponse オブジェクト全体 を返す。

http.get<Config>('/api/config', {observe: 'response'}).subscribe(res => {
    console.log('レスポンスステータス:', res.status);
    console.log('ボディ:', res.body);
});

進捗イベントを受信する

HttpClient は、レスポンスボディだけでなくリクエストライフサイクルに応じたイベントストリームも返せる。イベントには送信開始、ヘッダー受信、完了などがあり、reportProgress を有効にするとアップロード/ダウンロード進捗も取得できる。ただし進捗イベントはデフォルト無効(性能コストがあるため)。

イベントストリームを観察するには、observe オプションを 'events' に設定する。

http.post('/api/upload', myData, {
    reportProgress: true,
    observe: 'events',
}).subscribe(event => {
    switch (event.type) {
        case HttpEventType.UploadProgress:
            console.log('Uploaded ' + event.loaded + ' out of ' + event.total + ' bytes');
            break;
        case HttpEventType.Response:
            console.log('Finished uploading!');
            break;
    }
});

HttpEvent には、そのイベントの種類を示す type がある。

type イベントの意味
HttpEventType.Sent リクエストがサーバーに送信された
HttpEventType.UploadProgress リクエストボディのアップロード進捗を報告する HttpUploadProgressEvent
HttpEventType.ResponseHeader レスポンスヘッダーを受信(ステータスとヘッダーを含む)
HttpEventType.DownloadProgress レスポンスボディのダウンロード進捗を報告する HttpDownloadProgressEvent
HttpEventType.Response レスポンス全体を受信(レスポンスボディを含む)
HttpEventType.User インターセプターからのカスタムイベント

リクエスト処理の失敗

HTTP リクエストは、主に次の3つの理由で失敗する。

  • ネットワーク/接続エラー
    • リクエストがサーバーに届かない
  • タイムアウト
    • timeout オプションで指定した時間内に応答がなかった
  • バックエンドエラー
    • サーバー側がリクエストを受け取ったが、処理に失敗してエラーレスポンスを返した

HttpClient は、すべての種類の失敗を HttpErrorResponse として返す。ネットワークエラーやタイムアウトの場合は status が 0 となり、error には ProgressEvent が格納される。バックエンドエラーの場合は、サーバーが返した失敗コードが status に入り、error にはエラーレスポンスが格納される。開発者はレスポンスの内容を調べ、原因に応じた適切な処理を実装する必要がある。

catchError 演算子を使うと、エラーレスポンスを UI 向けの値に変換できるため、エラーページの表示やユーザーへのメッセージ提示に利用でき、必要に応じて原因の捕捉も可能になる。また、ネットワークの中断のように一時的な要因でリクエストが失敗する場合は、単純に再試行することで成功することもある。RxJS には失敗した Observable に対して自動的に再購読する仕組みが用意されており、その一つである retry() 演算子を利用すれば、指定した回数だけ自動でリトライを行える。

タイムアウト

リクエストにタイムアウトを設定するには、他のリクエストオプションと同様に timeout にミリ秒単位の値を指定する。バックエンドの処理がその時間内に完了しなければリクエストは中止され、エラーが発行される。

http.get('/api/config', {
    timeout: 3000,
}).subscribe({
    next: config => {
        console.log('設定の取得に成功:', config);
    },
    error: err => {
        // リクエストがタイムアウトした場合
    }
});

高度な fetch オプション

withFetch() を使うと、HttpClient で fetch API の高度な機能を利用でき、処理の高速化やUX向上に役立つ。ただし fetch バックエンド使用時のみ有効。

Keep-alive 接続

keepalive オプションを使うと、リクエストはページを離れた後でも続行されるため、ページ移動後も完了させたい分析やログ送信に便利である。

HTTP キャッシュ制御

cache オプションは、リクエストがブラウザの HTTP キャッシュとどう連携するかを制御し、同じリクエストの繰り返し時にパフォーマンスを大幅に向上させることができる。

// キャッシュされたレスポンスを使用
http.get('/api/config', {
    cache: 'force-cache'
}).subscribe(config => {
    // ...
});
// 常にネットワークから取得、キャッシュをバイパス
http.get('/api/live-data', {
    cache: 'no-cache'
}).subscribe(data => {
    // ...
});
// キャッシュされたレスポンスのみを使用、キャッシュにない場合は失敗
http.get('/api/static-data', {
    cache: 'only-if-cached'
}).subscribe(data => {
    // ...
});

Core Web Vitals のためのリクエスト優先度

priority オプションを使うと、リクエストの重要度を指定でき、ブラウザがリソースの読み込み順序を最適化して Core Web Vitals のスコア向上に役立てることができる。

// 重要なリソースの高優先度
http.get('/api/user-profile', {
    priority: 'high'
}).subscribe(profile => {
    // ...
});
// 重要でないリソースの低優先度
http.get('/api/recommendations', {
    priority: 'low'
}).subscribe(recommendations => {
    // ...
});
// 自動優先度 (デフォルト) はブラウザーが決定
http.get('/api/settings', {
    priority: 'auto'
}).subscribe(settings => {
    // ...
});

利用可能な priority 値:

  • 'high': 高優先度、早期に読み込まれる(例:重要なユーザーデータ、above-the-foldコンテンツ)
  • 'low': 低優先度、リソースが利用可能なときに読み込まれる(例:分析、プリフェッチデータ)
  • 'auto': ブラウザがリクエストコンテキストに基づいて優先度を決定(デフォルト)

リクエストモード

mode オプションは、リクエストがクロスオリジンかどうかの扱いを制御し、ブラウザがレスポンスをどのように処理するか(たとえば CORS の適用やレスポンスの可視性)を決定する。

// 同一オリジンリクエストのみ
http.get('/api/local-data', {
    mode: 'same-origin'
}).subscribe(data => {
    // ...
});
// CORS が有効なクロスオリジンリクエスト
http.get('https://api.external.com/data', {
    mode: 'cors'
}).subscribe(data => {
    // ...
});
// シンプルなクロスオリジンリクエストの No-CORS モード
http.get('https://external-api.com/public-data', {
    mode: 'no-cors'
}).subscribe(data => {
    // ...
});

利用可能な mode 値:

  • 'same-origin': 同一オリジンリクエストのみを許可、クロスオリジンリクエストは失敗
  • 'cors': CORSでクロスオリジンリクエストを許可(デフォルト)
  • 'no-cors': CORSなしでシンプルなクロスオリジンリクエストを許可、レスポンスは不透明

リダイレクト処理

redirect オプションは、サーバーからリダイレクトレスポンスが返されたときに、ブラウザがそのリダイレクトを自動的に追従するかどうか、またはどのように処理するかを指定する。

// リダイレクトを自動的に追跡(デフォルトの動作)
http.get('/api/resource', {
    redirect: 'follow'
}).subscribe(data => {
    // ...
});
// 自動リダイレクトを防ぐ
http.get('/api/resource', {
    redirect: 'manual'
}).subscribe(response => {
    // リダイレクトを手動で処理
});
// リダイレクトをエラーとして扱う
http.get('/api/resource', {
    redirect: 'error'
}).subscribe({
    next: data => {
        // 成功レスポンス
    },
    error: err => {
        // リダイレクトレスポンスはこのエラーハンドラーをトリガーします
    }
});

利用可能な redirect 値:

  • 'follow': リダイレクトを自動的に追跡(デフォルト)
  • 'error': リダイレクトをエラーとして扱う
  • 'manual': リダイレクトを自動的に追跡せず、リダイレクトレスポンスを返す

認証情報の処理

credentials オプションは、クロスオリジンリクエスト時に Cookie や認証ヘッダーなどの認証情報を送信するかどうかを制御する。認証が必要なリクエストでは特に重要な設定である。

// クロスオリジンリクエストに認証情報を含める
http.get('https://api.example.com/protected-data', {
    credentials: 'include'
}).subscribe(data => {
    // ...
});
// 認証情報を送信しない(クロスオリジンのデフォルト)
http.get('https://api.example.com/public-data', {
    credentials: 'omit'
}).subscribe(data => {
    // ...
});
// 同一オリジンリクエストのみに認証情報を送信
http.get('/api/user-data', {
    credentials: 'same-origin'
}).subscribe(data => {
    // ...
});
// withCredentials は credentials 設定を上書きする
http.get('https://api.example.com/data', {
    credentials: 'omit',        // これは無視される
    withCredentials: true       // これにより credentials: 'include' が強制される
}).subscribe(data => {
    // credentials: 'omit' にもかかわらず、リクエストは認証情報を含む
});
// レガシーアプローチ(まだサポートされている)
http.get('https://api.example.com/data', {
    withCredentials: true
}).subscribe(data => {
    // credentials: 'include' と同等
});

利用可能な credentials 値:

  • 'omit': 認証情報を送信しない
  • 'same-origin': 同一オリジンリクエストのみに認証情報を送信(デフォルト)
  • 'include': クロスオリジンリクエストでも常に認証情報を送信

インターセプター

インターセプターは、HTTP リクエストやレスポンスに介入して処理できる関数で、全体の流れや内容をカスタマイズできる。複数設定すると順番に実行され、各インターセプターは次に処理を渡す前に自分の処理を行う。

インターセプターを利用すると、以下のようなことができる。

  • 認証ヘッダーをリクエストに追加
  • 失敗したリクエストを指数関数的バックオフで再試行
  • レスポンスを一定期間キャッシュ
  • レスポンスの解析をカスタマイズ
  • サーバー応答時間を測定してログに記録
  • ネットワーク操作中の UI(ローディングスピナーなど)を制御
  • 一定期間内のリクエストをまとめてバッチ処理
  • タイムアウトや期限を設定して自動的にリクエストを失敗させる
  • サーバーを定期的にポーリングして結果を更新

インターセプターの定義

インターセプターは、送信される HttpRequest と、チェーン内の次の処理を実行する next 関数を受け取る関数として定義される。

たとえば、この loggingInterceptor は、リクエストを転送する前に、送信されるリクエストの URL を console.log に記録する。

export function loggingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
    console.log(req.url);
    return next(req);
}

インターセプターの構成

HttpClient に適用するインターセプターのセットは、withInterceptors を使い、依存性注入を通じて宣言して構成する。

bootstrapApplication(AppComponent, {providers: [
    provideHttpClient(
        withInterceptors([loggingInterceptor, cachingInterceptor]),
    )
]});

構成したインターセプターは、プロバイダーに並べた順番でチェーンされる。例えば、loggingInterceptor が先にリクエストを処理し、その後 cachingInterceptor に処理が渡される。

レスポンスイベントのインターセプト

インターセプターは、next から返される HttpEvent の Observable を変換して、レスポンスにアクセスしたり操作したりできる。この Observable にはすべてのレスポンスイベントが含まれるため、最終的なレスポンスを特定するには各イベントの .type を確認する必要がある場合がある。

export function loggingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
    return next(req).pipe(tap(event => {
        if (event.type === HttpEventType.Response) {
            console.log(req.url, 'returned a response with status', event.status);
        }
    }));
}

リクエストの変更

HttpRequestHttpResponse のほとんどのプロパティは変更不可能で、インターセプターは直接変更できない。変更する場合は .clone() を使ってオブジェクトを複製し、新しいインスタンスで変更したいプロパティを指定して適用する。この方法には、HttpHeadersHttpParams のような値自体に対する不変の更新も含まれる。

たとえば、リクエストにヘッダーを追加するには、次のようにする。

const reqWithHeader = req.clone({
    headers: req.headers.set('X-New-Header', 'new header value'),
});

この変更不可能性のおかげで、ほとんどのインターセプターは同じ HttpRequest がチェーン内で複数回処理されてもべき等性を保てる。これは、リクエストの再試行などで同じリクエストが再送される場合にも適用される。

インターセプターでの依存性の注入

インターセプターは、登録されたインジェクターの注入コンテキスト内で実行され、Angular の inject API を使って依存関係を取得できる。

たとえば、アプリケーションに AuthService があり、送信リクエスト用の認証トークンを生成するとする。この場合、インターセプターは AuthService を注入して、そのトークンをリクエストに追加することができる。

export function authInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn) {
    // 現在の `AuthService` を注入して、認証トークンを取得します。
    const authToken = inject(AuthService).getAuthToken();
    // 認証ヘッダーを追加するようにリクエストを複製します。
    const newReq = req.clone({
        headers: req.headers.append('X-Authentication-Token', authToken),
    });
    return next(newReq);
}

リクエストとレスポンスのメタデータ

バックエンドに送信されない情報や、インターセプター専用のメタデータをリクエストに含めたい場合、HttpRequest.context を使うと便利である。この .contextHttpContext のインスタンスとしてデータを格納し、型付きマップのように動作する。キーには HttpContextToken 型を使用する。

コンテキストトークンの定義

特定のリクエストがキャッシュされるかどうかを .context に保存したい場合は、そのための新しい HttpContextToken を作り、キーとして使う。

export const CACHING_ENABLED = new HttpContextToken<boolean>(() => true);

インターセプターでのトークンの読み込み

インターセプターは、トークンの値を参照して、そのリクエストにキャッシュロジックを適用するかどうかを判断できる。

export function cachingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
    if (req.context.get(CACHING_ENABLED)) {
        // キャッシュロジックを適用する
        return ...;
    } else {
        // このリクエストに対してキャッシュが無効になっている
        return next(req);
    }
}

リクエストの作成時のコンテキストトークンの設定

HttpClient API でリクエストを作成するときに、HttpContextToken の値を設定することができる。

const data$ = http.get('/sensitive/data', {
    context: new HttpContext().set(CACHING_ENABLED, false),
});

インターセプターは、リクエストの HttpContext からこれらの値を取得して利用できる。

リクエストコンテキストは変更可能

HttpRequest の他のプロパティとは異なり、関連付けられた HttpContext は変更可能である。インターセプターが再試行されるリクエストのコンテキストを変更すると、同じインターセプターは再実行時にその変更を認識できる。これにより、複数回の再試行にわたって状態を引き継ぐことが可能になる。

0
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
0
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?