(Qiitaから移植)Angular 4.3で追加されたHttpClientModuleについてのメモ
本記事は2017年7月19日にQiitaに投稿した記事を移行したものです。
はじめに
先日Angular 4.3がリリースされました。予定されているリリーススケジュール通りであれば、
Angular4系では最後のマイナーバージョンリリースになります。
(これから9月のバージョン5のリリースまでパッチリリースのみの予定です。)
リリーススケジュールが若干遅れ、代わりに4.xのマイナーバージョンアップとして4.4がリリースされました。
5.0のリリースは2017/10/4現在、10/23の予定です。
このAngular 4.3では、大きな機能追加の一つとして 新しいHttpクライアントモジュールが追加されています。 新しい、とはいえこれまでのHttpモジュールと全く違うものというわけではなく、既存のHttpモジュールの改良版という位置付けのようです。 既存のHttpモジュールも使いやすく気に入っていたのですが、今回追加されたHttpクライアントではさらなる改善が加えられています。
以下、公式の解説に従いながら新機能を解説していきます。
※サンプルではAngular CLIの利用を前提に記載しています。
インストール
新しいHttpクライアントモジュールは@angular/common/http
として、これまでの@angular/http
とは別のモジュールとして提供されています。
既存のHttpモジュールと新しいHttpクライアントモジュールでは基本的な使い方は似ていますが、一部互換性のない構文が含まれています。そのためいきなり@angular/http
を置き換えるのではなく別モジュールとすることで、徐々に移行できるようにしようという意図があるようです。
まずはプロジェクトを作成しましょう。@angular/common/http
を使う場合でも特に追加設定は不要です。
(この時Angular 4.3以上でないと@angular/common/httpは提供されていないので、4.3以上がインストールされていることを確認してください。)
ng new
続いて、モジュールに新しいHttpクライアントモジュールを追加します。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
// HttpClientModuleをインポート
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [
BrowserModule,
// HttpClientModuleを追加
HttpClientModule,
],
})
export class AppModule {}
これでAppModule内のクラスに対してHttpClient
のDIが可能になります。
Httpモジュール同様、コンストラクタでhttp: HttpClient
と記載しておけばインスタンスの生成と同時に初期化されます。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable()
export class SampleService {
// コンストラクタの引数に指定してDI
constructor(private http: HttpClient) {}
}
改善ポイント
それでは早速、HttpClient
の機能を見ていきます。
JSON形式のパースがデフォルトに
これまでのget
メソッドはObservable<Response>
を返しており、レスポンスとして返されたJSONを処理するためには以下のように記述する必要がありました。
// @angular/http
this.http.get(url).map(response => response.json()).subscribe(json => ...);
しかし近年ではREST APIなどJSONによるデータのやり取りが一般的になっており、リクエストのたびに上のように書くのは冗長です。そうした流れを受けて、HttpClient
ではJSON形式のパースをデフォルトで行うように変更されました。
get
などのメソッドは特に型の指定がない場合、これまでのObservable<Response>
ではなくObservable<Object>
が戻り値になります。
// @angular/common/http
// ここでjsonはResponseではなくObject
this.http.get(url).subscribe(json => ... );
とはいえ、必ずしもすべての通信がJSONで行われるわけではないと思います。そういった場合に開発者が任意にレスポンスのフォーマットを指定するにはresponseType
オプションを指定します。
http.get(url, { responseType: 'text' })
// レスポンスはテキストとしてsubscribeに渡される
.subscribe(text => console.log(text));
レスポンスの型指定
上記サンプルのようにresponse.json()
でJSONをパースすると、subscribe
で受け取る型はおのずとobject
になります。JavaScriptであればobject
で受け取れればあとはよしなに処理すれば良いのですが、TypeScriptではobject内のプロパティは宣言の時点で明示されているもの以外はobj.foo
形式でアクセスできないという制約があります。
// JavaScript
this.http.get(url)
.subscribe(response => {
console.log(response.foo); // OK
console.log(response['foo']); // OK
});
// TypeScript
// この書き方だとresponseオブジェクト内部のプロパティが定義されていない
this.http.get(url)
.subscribe(response => {
console.log(response.foo); // NG
console.log(response['foo']); // OK
});
JavaScriptとTypeScriptで全く同じコードなのにresponse.foo
の呼び出しがエラーになってしまいました。これを回避するためには、interfaceを用いて内部のプロパティを定義する必要があります。
import 'rxjs/add/operator/map';
// プロパティ: fooを持つinterfaceを定義
interface FooResponse {
foo: string;
}
// パターン1:mapでキャスト
this.http.get(url)
// FooResponseにキャスト
.map(response => response as FooResponse)
.subscribe(response => {
console.log(response.foo); // OK
console.log(response['foo']); // OK
});
// パターン2:subscribe内でキャスト
this.http.get(url)
.subscribe(response => {
const fooResponse = response as FooResponse;
console.log(fooResponse.foo); // OK
console.log(fooResponse['foo']); // OK
});
上記のコードでプロパティ呼び出しは実現できましたが、せっかくなくせたと思っていたmap
メソッドが復活していたり、わざわざas
でキャストしていたりと冗長になってしまいました。
新しいHttpクライアントモジュールでは、各メソッドに戻り値の型が型パラメータとして渡せるようになっています。
// プロパティ: fooを持つinterfaceを定義
interface FooResponse {
foo: string;
}
// getメソッドの型パラメータでレスポンスの型を指定
this.http.get<FooResponse>(url)
// subscribeの時点でFooResponseとして受け取れる
.subscribe(response => {
console.log(response.foo); // OK
console.log(response['foo']); // OK
});
map
メソッドもas
によるキャストもなくなり、コードがすっきりしました。
完全なレスポンスの取得
これまでの書き方では、subscribe
は直接レスポンスボディの内容を受け取っていました。しかしAPIの内容によってはカスタムヘッダが含まれているなど、レスポンスヘッダを参照したいことがあるかもしれません。
そのような場合はメソッドにobserve
オプションを設定することでヘッダなどを含めた完全なレスポンスを受け取ることができます。
// プロパティ: fooを持つinterfaceを定義
interface FooResponse {
foo: string;
}
// getメソッドにobserveオプションを指定
this.http.get<FooResponse>(url, { observe: 'response' })
.subscribe(response => {
// ヘッダ情報はresponse.headersに格納。getメソッドで取得。
console.log(response.headers.get('X-My-Header'));
// レスポンスボディはresponse.bodyに格納。型指定も有効。
console.log(response.body.foo);
});
Interceptor
HttpClient
の目玉の一つです。
例えば、認証が必要なREST APIにリクエストを送信する際、ヘッダにAuthorization: Bearer XXXXXXXXXX
を設定したり、レスポンスから不要な値を削除したりといった共通的な処理を挟み込みたい場合、これまではHttp
モジュールをラップするサービスを作って処理を記述する他ありませんでした。
Interceptorを利用すれば、これまでのようにHttpClient
のラッパを作らずとも共通処理が実現できます。
以下はリクエストをそのまま転送しレスポンスをそのまま返却するだけのInterceptorです。
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class SimpleInterceptor implements HttpInterceptor {
// リクエストの変換処理。ここに共通処理を記述。
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request);
}
}
InterceptorはHttpInterceptor
インタフェースを実装し、intercept
メソッドを持つクラスとして定義します。
intercept
メソッドは受け取ったHttpRequest
をObservable<HttpEvent>
に変換して返すメソッドとして定義します。request: HttpRequest<>
が処理対象となるリクエストデータです。ヘッダやボディなどを含みます。next.handle
はintercept
メソッド同様にHttpRequest
を受け取りObservable<HttpEvent>
を返すメソッドで、Interceptorが複数ある場合は後続のInterceptorを呼び出し、後続の処理がなければリクエストを送信します。基本的にintercept
メソッドの処理完了時の決まり文句と捉えて貰えれば良いと思います。
作成したInterceptorを利用する場合はサービスと同様にproviderに設定する必要があります。(複数のInterceptorを利用する場合、ここで指定した順序通りに処理が実行されます。)
import { NgModule } from '@angular/core';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { SimpleInterceptor } from './simple-interceptor.service';
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: SimpleInterceptor,
// 必須:HTTP_INTERCEPTORSが配列であることを示す
multi: true
}
]
})
export class AppModule {}
リクエストの処理
リクエストを処理するには、intercept
メソッドに渡されたHttpRequest
オブジェクトを利用して処理を行います。
リクエストを加工する場合も同様にHttpRequest
オブジェクトを利用しますが、HttpRequest
オブジェクトは基本的にimmutableになっており、そのままではリクエストを加工することができません。値を加工したい場合、HttpRequest
に定義されているclone
メソッドを利用します。clone
メソッドは引数なしで実行するとそのオブジェクトのコピーを返しますが、オブジェクトを引数に渡すと、指定された値を上書きしたオブジェクトを返します。これを利用して値の書き換えを行います。
// そのまま複製するサンプル
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const req = request.clone();
return next.handle(req);
}
// fooの値を書き換える場合
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const req = request.clone({ foo: 'Foo' });
return next.handle(req);
}
// 複数の値も可
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const req = request.clone({ foo: 'Foo', bar: 'Bar' });
return next.handle(req);
}
リクエストヘッダの書き換えも上記と同じ要領で行うことができます。書き換えるプロパティはheaders
です。
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = 'Bearer XXXXXXXXXX';
const req = request.clone({ headers: request.headers.set('Authorization', token) });
return next.handle(req);
}
上記サンプルではrequest.headers.set('Authorization', token)
が使われています。request.headers
がオリジナルのリクエストヘッダです。headers
は複数値のプロパティなので、そのままheaders: [token]
としてしまうと必要なリクエストヘッダの情報が欠落してしまいます。ヘッダに限らず、複数値のプロパティでは注意してください。
ヘッダに関しては、上記の処理についてショートハンドが提供されています。
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = 'Bearer XXXXXXXXXX';
const req = request.clone({ setHeaders: { Authorization: token }});
return next.handle(req);
}
レスポンスの処理
レスポンスの値を利用して処理を行う場合、HttpHandler.handle
(これまでのサンプルにおけるnext.handle
)の戻り値を利用します。先述したとおりhandle
メソッドはObservable<HttpEvent>
を返しますので、RxJSのメソッドを利用して加工して貰えればOKです。
以下はレスポンスの値をWebStorageに格納するサンプルです。
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const storage = window.sessionStorage;
return next.handle(request)
.do(event => {
if (event instanceof HttpResponse) {
storage.setItem('cache', event.body);
}
});
}
Progress Events
大容量のファイルアップロードやダウンロードを行う場合など、処理の進捗(ex. 今どこまでアップロードが進んでいるのか)
を確認したい場合があります。HttpClient
では、そのようなユースケースに対応するため、処理の進捗を示すイベントの実行をサポートしています。
処理の進捗をイベントで受け取る場合は、HttpRequest
オブジェクトを生成して、reportProgress
オプションを設定する必要があります。
import { HttpRequest } from '@angular/common/http';
const request = new HttpRequest('POST', '/upload/files', file, {
reportProgress: true,
});
HttpRequest
オブジェクトをPOST用に生成したので、これをHttpClient.request
メソッドでPOSTします。
subscribe
で取得されるオブジェクトは進捗を示すイベント、もしくはレスポンスになりますので、オブジェクトの型を頼りに処理を分岐します。
import { HttpEventType, HttpResponse, HttpEventType } from '@angular/common/http';
this.http.request(request).subscribe(event => {
if (event.type === HttpEventType.UploadProgress) {
// 進捗状況の出力
const percentDone = Math.round(100 * event.loaded / event.total);
console.log(`File is ${percentDone}% uploaded.`);
} else if (event instanceof HttpResponse) {
// HttpResponseを取得した場合は処理完了
console.log('File is completely uploaded!');
}
});
XSRF対策
上で紹介した Interceptor
を利用した機能として、XSRF 対策がサポートされています。
クッキーに XSRF-TOKEN
が設定されている場合、その値をリクエストヘッダ X-XSRF-TOKEN
に設定して通信します。
この Intercepter は、HttpClient を使用した通信のうち、
- リクエストメソッドが
GET
,HEAD
以外 - リクエスト先 URL が相対パス
であるリクエストに適用されます。
Cookie 名 / ヘッダ名を指定する。
デフォルトでは、クッキー名に XSRF-TOKEN
を、ヘッダ名に X-XSRF-TOKEN
を使用します。これらの名前を変更するには、@NgModule
にインポート HttpClientXsrfModule.withConfig
を追加します。
imports: [
HttpClientModule,
HttpClientXsrfModule.withConfig({
cookieName: 'My-Xsrf-Cookie', // クッキー名を指定
headerName: 'My-Xsrf-Header', // ヘッダ名を指定
})
]
その他機能
上記の他
- HTTP リクエストのテスト機能
も追加されています。 (これについても時間があれば追記したいと思います。)
終わりに
新しいHttpクライアントモジュールについて、大まかに説明してみました。 個人的にInterceptorのサポートは非常に嬉しいポイントです。 既存の資産については互換性のない変更もありますので、既存のHttpモジュールを利用している資産を今すぐ置き換えるものではないですが、これから開発するものについては活用していきたいと思います。
※ 間違いなどあればご指摘いただけると助かります!