0
1

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技術要素解説】HttpClientとRxJSでサーバー通信を理解する

0
Posted at

はじめに

Angular技術要素解説シリーズ、第6弾です。

今回は、サーバーとのHTTP通信を担うHttpClientと、その裏側で使われているRxJSを解説します。

以前解説したRxJavaによるリアクティブプログラミングのJavaScript版にあたるライブラリで、AngularのHTTP通信はこのRxJSの考え方を土台にしています。

この記事で扱う技術要素

  • HttpClient
  • Observable(オブザーバブル)
  • RxJSの基本オペレーター(map・filter・catchErrorなど)
  • Interceptor(インターセプター)

HttpClientの基本

Angularは、HTTP通信のためのHttpClientを標準搭載しています。

まずは基本設定から見ていきます。

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app.component';

bootstrapApplication(AppComponent, {
  providers: [provideHttpClient()], // HttpClientを使えるようにする
});

GETリクエストの例

// user.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

interface User {
  id: number;
  name: string;
  email: string;
}

@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);
  private apiUrl = '/api/users';

  getUsers(): Observable<User[]> {
    // HttpClientのget()は、即座にObservableを返す(この時点ではまだ通信は始まっていない)
    return this.http.get<User[]>(this.apiUrl);
  }
}
// user-list.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-list',
  standalone: true,
  template: `
    <ul>
      @for (user of users; track user.id) {
        <li>{{ user.name }} ({{ user.email }})</li>
      }
    </ul>
  `,
})
export class UserListComponent implements OnInit {
  private userService = inject(UserService);
  users: User[] = [];

  ngOnInit(): void {
    // subscribe()を呼んで初めて、実際のHTTPリクエストが送信される
    this.userService.getUsers().subscribe(users => {
      this.users = users;
    });
  }
}

http.get()を呼んだ時点では、まだ実際の通信は始まっていません。

Observableは「値が将来流れてくる約束」を表しているだけで、subscribe()(購読)して初めて実際の処理が実行されるという点が、Promiseとの大きな違いです。

Observableとは

Observableは、RxJSにおける「時間とともに値が流れてくるストリーム」を表す型です。

以前解説したRxJavaのObservableと、考え方は基本的に同じです。

import { Observable } from 'rxjs';

// 手動でObservableを作る例(実際にはHttpClient等が内部で生成してくれる)
const numberStream$ = new Observable<number>(subscriber => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  subscriber.complete(); // ストリームの終了を通知する
});

numberStream$.subscribe({
  next: (value) => console.log('値:', value),
  complete: () => console.log('完了しました'),
});

実行結果:

値: 1
値: 2
値: 3
完了しました

変数名の末尾に$(ドル記号)を付けるのは、「この変数はObservableである」ことを示すRxJSコミュニティの慣習です。

必須ではありませんが、Angularプロジェクトのコードでもよく見かけます。

パイプとオペレーターでストリームを加工する

RxJSの真価は、Observableをオペレーターでつなぎ合わせて加工できる点にあります。

pipe()メソッドで、複数のオペレーターを連結します。

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, filter, catchError } from 'rxjs/operators';
import { of } from 'rxjs';

interface User {
  id: number;
  name: string;
  isActive: boolean;
}

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

  getActiveUserNames(): Observable<string[]> {
    return this.http.get<User[]>('/api/users').pipe(
      // filterで有効なユーザーだけに絞り込む(配列全体に対する処理はこの時点ではまだできないので、実際はArray側で行うことが多いが、概念として例示)
      map(users => users.filter(user => user.isActive)),
      // mapで、ユーザーオブジェクトの配列を名前の配列に変換する
      map(users => users.map(user => user.name)),
      // catchErrorでエラー発生時に、空配列を返してストリームを継続させる
      catchError(error => {
        console.error('ユーザー取得に失敗しました', error);
        return of([]); // ofは「単一の値をそのままObservableにする」関数
      })
    );
  }
}
オペレーター 役割
map 流れてくる値を、別の値に変換する
filter 条件に合う値だけを通過させる
catchError エラーが発生した際に、代わりの値やストリームを返す
tap 値の中身は変えずに、ログ出力などの副作用だけを行う
switchMap 前のストリームの結果をもとに、新しいストリームに乗り換える(検索候補の逐次取得など)
debounceTime 一定時間、値の発行がなければ待ってから次に進む(入力補完の連打防止など)

実践例:検索ボックスの入力に応じてAPIを呼ぶ

switchMapとdebounceTimeを組み合わせた、実務でよくある例を見てみます。

import { Component, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';

@Component({
  selector: 'app-search-box',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <input [formControl]="searchControl" placeholder="ユーザーを検索">
    <ul>
      @for (result of searchResults; track result.id) {
        <li>{{ result.name }}</li>
      }
    </ul>
  `,
})
export class SearchBoxComponent {
  private http = inject(HttpClient);
  searchControl = new FormControl('');
  searchResults: { id: number; name: string }[] = [];

  constructor() {
    this.searchControl.valueChanges
      .pipe(
        debounceTime(300),          // 入力が300ms止まってから処理する(連打防止)
        distinctUntilChanged(),     // 直前と同じ値なら何もしない
        switchMap(keyword =>
          // 新しい検索が来たら、前の検索リクエストは自動的にキャンセルされる
          this.http.get<{ id: number; name: string }[]>(`/api/users?q=${keyword}`)
        )
      )
      .subscribe(results => {
        this.searchResults = results;
      });
  }
}

このコードは、debounceTime・distinctUntilChanged・switchMapという3つのオペレーターを組み合わせるだけで、「入力のたびに無駄なAPI呼び出しをしない」「古い検索結果が新しい結果を追い越して表示される事故を防ぐ」という、地味に実装が面倒な処理を簡潔に実現しています。

これは、コールバックやasync/awaitだけで同じことを書こうとすると、キャンセル処理などがかなり複雑になる典型例です。

Interceptor(インターセプター)

インターセプターは、すべてのHTTPリクエスト・レスポンスに対して、共通の処理を差し込む仕組みです。

認証トークンの付与や、エラーの一括処理によく使われます。

// auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const token = authService.getToken();

  if (token) {
    // すべてのリクエストのヘッダーに、自動的に認証トークンを付与する
    const clonedRequest = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` },
    });
    return next(clonedRequest);
  }

  return next(req);
};
// main.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor])), // インターセプターを登録
  ],
});

このインターセプターを登録しておくと、アプリ内のあらゆるhttp.get()・http.post()呼び出しに対して、個別にトークンを付与するコードを書かなくても、自動的に認証ヘッダーが付与されるようになります。

同様の仕組みで、通信エラー発生時のログ記録や、共通のエラーダイアログ表示なども一箇所に集約できます。

Signalsとの使い分け

以前の記事で解説したSignalsが普及したことで、「単純なコンポーネントの状態管理」はSignalへの置き換えが進んでいますが、HTTP通信のような非同期のストリーム処理では、依然としてRxJSの役割が大きいのが実情です。

近年は、toSignal()関数を使ってObservableの結果をSignalに変換し、両者を橋渡しする書き方もよく使われます。

import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-user-list',
  standalone: true,
  template: `
    <ul>
      @for (user of users(); track user.id) {
        <li>{{ user.name }}</li>
      }
    </ul>
  `,
})
export class UserListComponent {
  private http = inject(HttpClient);

  // ObservableをSignalに変換する。以後はテンプレートでusers()として扱える
  users = toSignal(this.http.get<User[]>('/api/users'), { initialValue: [] });
}

まとめ

用語 役割
HttpClient Angular標準搭載の、HTTP通信を行うためのサービス
Observable 時間とともに値が流れてくるストリームを表す、RxJSの中心的な型
オペレーター(map/filter/switchMap等) Observableを加工・変換するための関数群
Interceptor すべてのHTTPリクエスト・レスポンスに共通処理を差し込む仕組み
toSignal() ObservableをSignalに変換し、両者を連携させるための関数

次回は、ユーザー入力を扱うフォームについて解説します。

学習に使った書籍

本記事の内容整理にあたっても、以下の書籍を参考にしました。

※上記リンクはアフィリエイトリンクを含みます。

参考

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?