余白

https://blog.lacolaco.net/ に移転しました

Angularとaxiosを使ったHTTP通信

題して「頼りすぎないAngular」ということで、Angularの層をなるべく薄くアプリケーションを作るにはどうすればいいかというのを考えるシリーズです。 Angular良さそうなんだけどロックインされて捨てにくそう、という人々向けに、コードのモジュール性とフレームワーク非依存性を重視した実装パターンを試行錯誤します。

第一回目はAngularのHttpClientを覚えずに、人気のnpmモジュールである axios を使ってAngularアプリでAjaxする例を紹介します。 axiosはTypeScriptの型定義を同梱していて、インターセプターなどAngularのHttpClientと同じような機能が揃っています。

Live Example

今回の完成形はこちらです。

stackblitz.com

Random User Generatorからユーザー情報のJSONを取得し、画面に表示するアプリケーションです。

HttpClient

さて今回はAngular公式のHttpClientモジュールを使わずフレームワーク非依存のaxiosを使ってHttpClientを作ります。 次のようなファイルでアプリケーション用のカスタムインスタンスを生成してexportします。 今回は何もしませんが実際はデフォルトのヘッダを追加したりインターセプターを追加したりいろいろすると思います。

import axios from 'axios';

const instance = axios.create();

export default instance;

UserRepository

次に、作成したHttpClientを使ってAPI呼び出しを行うためのサービスクラスを作ります。 単純にimportして使うだけです

import { Injectable } from '@angular/core';
import httpClient from '../infrastructure/http-client';

@Injectable()
export class UserRepository {

  async random() {
    const { data } = await httpClient.get('https://randomuser.me/api/');
    const { results: [user] } = data;
    return user;
  }
}

作成したサービスクラスをAppModuleに登録します。

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { UserRepository } from './repository/user';

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
  providers: [UserRepository]
})
export class AppModule { }

AppComponent

最後にコンポーネントからサービスを利用します。ここはAngularのDIを使います。

import { Component } from '@angular/core';
import { UserRepository } from './repository/user';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  user: User | null = null;

  constructor(private userRepo: UserRepository) { }

  ngOnInit() {
    this.fetchUser();
  }

  async changeUser() {
    this.user = null;
    await this.fetchUser();
  }

  private async fetchUser() {
    this.user = await this.userRepo.random();
  }
}

テンプレートでは名前と写真を表示して、ボタンを押すとchangeUser()メソッドをトリガーするようにしています。これで完成です。

<h2>Angular with axios</h2>

<ng-container *ngIf="user as user">
    <h2>{{ user.name.first + ' ' + user.name.last | titlecase }}</h2>
    <img src="{{user.picture.large}}">
</ng-container>
<button (click)="changeUser()">Change User</button>

テストとDI

サービスクラスのUserRepositoryでは直接httpClientをimportして参照しましたが、AppComponentではDI経由でUserRepositoryを参照しました。 この違いは、ユニットテストをどう書くかという観点で分かれています。

axiosはmoxiosというパッケージを使うことで簡単にaxiosのインスタンスをモック化できます。 そのため、httpClientの初期化のテストにおいても、UserRepositryの振る舞いのテストにおいても、DIは必要ありません。

しかしAppComponentからUserRepositoryを直接参照すると、簡単にはモックできません。 なのでテスト時にUserRepositoryのモックを提供できるようにDI経由で参照しています。

利点・欠点

利点

  • axiosは有名で人気なライブラリなので学習しやすい
  • HttpClient部分はAngularじゃなくても使える

欠点

  • RxJSの恩恵を受けられない
    • 遅延実行
    • 複数値の返却

RxJSの恩恵については、UserRepositoryの層でfromPromise関数などを使ってObservableを返すようにすれば少しだけ解決します。 しかしFlux的な設計をするとなるとObservableなのはストアだけで良くて、fetch自体は単発で終わるほうが扱いやすいので特に欠点ではない気もしています。

まとめ

  • AngularのHttpClientは必須ではない
  • axiosのようなライブラリを使ってフレームワーク非依存の独自HttpClientを作れる
  • DIするかどうかはテストしやすさを考える

次回は未定です。