Carpe Diem

備忘録。https://github.com/jun06t

AngularのHttpClientModuleの使い方

概要

Angular 4.3からこれまでのHttpModuleに代わってより軽量かつ使いやすいHttpClientModuleと言うものが出てきました。
今回はその移行作業を書きます。

環境

  • Angular 4.3.1
  • angular-cli 1.2.3

成果物

今回の成果物は以下です。 github.com

比較して見やすいのは以下のdiffです。 github.com

使い方のBefore / After

app.module.ts

Before

@angular/httpHttpModuleを使います。

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

import { AppComponent } from './app.component';
import { ArticleService } from './services';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    HttpModule
  ],
  providers: [ArticleService],
  bootstrap: [AppComponent]
})

After

HttpModuleの代わりに@angular/common/httpHttpClientModuleを使います。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';  // here

import { AppComponent } from './app.component';
import { ArticleService } from './services';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    HttpClientModule  // here
  ],
  providers: [ArticleService],
  bootstrap: [AppComponent]
})
export class AppModule { }

コード

Before

Httpを使います。

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';

import { Article } from './article';

@Injectable()
export class ArticleService {
  constructor(private http: Http) { }

  get(id: string): Observable<Article> {
    return this.http.get('http://jsonplaceholder.typicode.com/posts/' + id)
      .map(res => res.json())
  }
}

After

Httpの代わりにHttpClientを使います。
また.get<Article>()のようにジェネリクスを使って書くことで、.json()でわざわざ変換していた処理が省略できるようになりました。

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

import { Article } from './article';

@Injectable()
export class ArticleService {
  constructor(private http: HttpClient) { }

  get(id: string): Observable<Article> {
    return this.http.get<Article>('http://jsonplaceholder.typicode.com/posts/' + id);
  }
}

テスト

Before

MockBackendやらBaseRequestOptionsでゴニョゴニョとproviderを用意します。

import { TestBed, inject } from '@angular/core/testing';
import { Http, BaseRequestOptions, Response, ResponseOptions, Request, RequestMethod } from '@angular/http';
import { MockBackend } from '@angular/http/testing';

import { ArticleService } from './article.service';
import { Article } from './article';

describe('ArticleService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        ArticleService,
        {
          provide: Http,
          useFactory: (mockBackend, options) => {
            return new Http(mockBackend, options);
          },
          deps: [MockBackend, BaseRequestOptions]
        },
        MockBackend,
        BaseRequestOptions
      ]
    });
  });

  describe('#get', () => {
    const mockResponse: Article = {
      id: 0,
      userId: 1,
      title: 'mock title',
      body: 'mock body',
    }

    it('should get article', inject([ArticleService, MockBackend], (service, mockBackend) => {
      let req: Request;
      mockBackend.connections.subscribe((connection) => {
        connection.mockRespond(new Response(new ResponseOptions({
          body: JSON.stringify(mockResponse)
        })));
        req = connection.request;
      });

      service.get('0').subscribe((resp: Article) => {
        expect(req.url).toBe('http://jsonplaceholder.typicode.com/posts/0');
        expect(req.method).toBe(RequestMethod.Get);

        expect(resp.id).toBe(0);
        expect(resp.userId).toBe(1);
        expect(resp.title).toBe('mock title');
        expect(resp.body).toBe('mock body');
      });
    }));
  });
});

After

テストは書き方がかなり変わります。providers周りなど、以前よりとてもシンプルに書けるようになります。
ポイントをコメントで追記しています。

import { TestBed, inject } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

import { ArticleService } from './article.service';
import { Article } from './article';

describe('ArticleService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        ArticleService,
      ],
      imports: [
        HttpClientTestingModule,
      ]
    });
  });

  describe('#get', () => {
    const mockResponse: Article = {
      id: 0,
      userId: 1,
      title: 'mock title',
      body: 'mock body',
    };

    it('should get article', inject([ArticleService, HttpTestingController], (service, httpMock) => {
      // リクエストの用意。
      service.get('0').subscribe((resp: Article) => {
        expect(resp.id).toBe(0);
        expect(resp.userId).toBe(1);
        expect(resp.title).toBe('mock title');
        expect(resp.body).toBe('mock body');
      });

      // 指定したエンドポイントのリクエストをここでpending。
      const req = httpMock.expectOne('http://jsonplaceholder.typicode.com/posts/0');
      expect(req.request.method).toEqual('GET');

      // ここでレスポンスを返す
      req.flush(mockResponse);

      // 余分なリクエストがないかチェック
      httpMock.verify();
    }));
  });
});

まとめ

基本的な移行作業を書きました。
他にもHeadersHttpHeadersになったり、Response型の代わりにHttpResponseになったりと、ちょこちょこ変更点があります。
それぞれドキュメントのメソッドや例を見て修正すると良いです。

ソース