Angular In Memory Web API を使ってモックサーバを立てる
Angular アプリの開発中、Web API サーバをモック化する時は、angular-in-memory-web-api
を使うと簡単にモック API サーバが用意できる。
少々つまづいたところがあったので、実装の仕方を確認していこう。
目次
- プロジェクトを用意する
- HTTP 通信する画面を作る
- Web API モックを構成するサービスクラスを作成する
- ID を指定して特定データのみ取得する
- モックデータオブジェクト・URL に制約ができる
- 2階層以上のパスを解釈させる方法
- 実際にデータを取得してみる
- レスポンスデータをカスタマイズする
- まとめ
プロジェクトを用意する
まずは Angular CLI の ng new
コマンドで Angular プロジェクトの雛形を作ろう。今回は @angular/cli@1.6.4
を使用して、Angular5 系の雛形を作成した。
$ ng new api-example
雛形ができたら、Angular In Memory Web API をインストールする。
$ npm install angular-in-memory-web-api --save
コレでひとまず、$ npm start
でサーバが起動するか確認しておこう。
HTTP 通信する画面を作る
今回は AppComponent
にて、POST 送信を行う簡単なプログラムを用意する。今回は HttpClient で実装したが、以前の Http モジュールを利用しても問題ない。
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
/** 通信結果を表示するための項目 */
public result: any;
/**
* コンストラクタ
*
* @param http HttpClient
*/
constructor(private http: HttpClient) {}
/**
* GET 通信する
*
* @param path ドメイン以下のパス
*/
doGet(path: string): void {
// サーバ URL : 環境変数によって実際のサーバ URL にするか、モックサーバを示す文字列にするか切り替える
const serverUrl = environment.production ? 'http://example.com/' : 'mock-server/';
// サーバ URL とパスを結合して URL を生成し GET 通信する
this.http.get(serverUrl + path)
.toPromise()
.then((response) => {
this.result = response;
})
.catch((error) => {
this.result = error;
});
}
}
HTML 側は以下のとおり。
<h1>Angular In Memory Web API Example</h1>
<ul>
<li><button type="button" (click)="doGet('users')">ユーザ情報を取得する</button></li>
</ul>
<div *ngIf="result">
<h2>結果</h2>
<p>{{ result | json }}</p>
</div>
今回は簡単にするため、サービスクラスも作っていないし、HTML 中に doGet('users')
と URL 文字列の一部をもたせたりしていて、かなりお行儀の悪いコードだが、お許しいただきたい。
ポイントは、HttpClient で通信するサーバ URL を開発中だけ http://example.com/
ではなく mock-server/
となるよう切り替えているところ。コレにより、「ユーザ情報を取得する」ボタンを押下すると、開発中は mock-server/users
という URL に HTTP 通信を試みるワケである。
コンポーネントができたら、AppModule
の imports
に HttpClientModule
を追加しておく。コレでひとまず HTTP 通信を行おうとする画面ができあがる。
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule // ← 追加
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Web API モックを構成するサービスクラスを作成する
次に、Web API のモックとして、URL やレスポンスデータを定義するサービスクラスを作成する。
$ npm run ng generate service mock-web-api
このコマンドで src/app/mock-web-api.service.ts
を作成したら、以下のように実装する。
import { Injectable } from '@angular/core';
import { InMemoryDbService } from 'angular-in-memory-web-api';
@Injectable()
export class MockWebApiService implements InMemoryDbService {
/** モックデータ : 標準的な Web API の URL と対応させるため、データは配列で定義し、各要素は id プロパティが必須 */
private api: any = {
// ユーザ情報とか
users: [
{ id: 1, name: 'Marty' },
{ id: 2, name: 'Jennifer' }
]
};
/**
* InMemoryDbService から継承 : モックデータを作成する
*
* @return モックデータ
*/
public createDb(): any {
return this.api;
}
}
ポイントは InMemoryDbService
を継承している点だ。createDb()
もオーバーライドしているメソッドで、このメソッドで返却したオブジェクトが Web API の URL と自動的に対応するようになる。
つまり、この設定だけで以下の URL でそれぞれデータをアクセスできるようになる、ということだ。
mock-server/users
→this.api.users
の配列データmock-server/users/1
→this.api.users
の配列のうち、id
プロパティが1
のデータ
この「オブジェクトと URL の自動マッピング」については、あとで詳しく詳細する。
サービスクラスができたら、コレをモックサーバとして動作させるために、AppModule
に以下のように追加する。
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { AppComponent } from './app.component';
import { MockWebApiService } from './mock-web-api.service';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule,
HttpClientInMemoryWebApiModule.forRoot(MockWebApiService) // ← 追加
],
providers: [
MockWebApiService // ← 追加
],
bootstrap: [AppComponent]
})
export class AppModule { }
コレで完了。
開発中は mock-server/users
にアクセスすることになり、MockWebApiService
にて定義した users
のデータが返却されるようになる。
「ユーザ情報を取得する」ボタン
↓ 以下が取得・画面表示される
[ { "id": 1, "name": "Marty" }, { "id": 2, "name": "Jennifer" } ]
ID を指定して特定データのみ取得する
さて、標準的な RESTful API の URL 設計に基づけば、/users
はデータを一覧形式で取得でき、/users/2
のように ID を指定すれば、個別の ID が取れるはずだ。
Angular In Memory Web API はこうした RESTful API の URL を自動的に判別してデータを返してくれるので、いきなり /users/2
にアクセスして Jennifer
のデータのみを取得することができる。
実際にやってみよう。HTML に以下の1行を追加する。
<h1>Angular In Memory Web API Example</h1>
<ul>
<li><button type="button" (click)="doGet('users')">ユーザ情報を取得する</button></li>
<li><button type="button" (click)="doGet('users/2')">ID : 2 のユーザ情報を取得する</button></li> <!-- ←追加 -->
</ul>
<div *ngIf="result">
<h2>結果</h2>
<p>{{ result | json }}</p>
</div>
コレだけで、「ID : 2 のユーザ情報を取得する」ボタンを押すと、以下のようなデータが取得できる。
{ "id": 2, "name": "Jennifer" }
その他にも、クエリパラメータなども受け付けてくれるようだ。詳しくは以下の「HTTP request handling」を参照。
コレは便利だ。しかし問題が…。
モックデータオブジェクト・URL に制約ができる
createDb()
で返却したオブジェクトが自動的に URL とマッピングされるのは便利だが、この機能のためにオブジェクトの内容に以下のような制約が発生する。
this.api
のトップレベルのキー名が URL となる- ここでは
users
というキー名がそのまま/users
というパスになる
- ここでは
- 各キーは配列でデータを持たなくてはならない
users: { name: 'Marty' }
というようにいきなりオブジェクトを1つ返却する作りにはできない- 後述するカスタムハンドリングでレスポンスデータは調整可能だが、オブジェクトの型をチェックしているので、定義時点では配列化が必須。
- 配列内の各データは URL で指定できる ID となる
id
プロパティを持たなくてはならない
そして、このようなオブジェクトの制約により、http://example.com/book/genres
のように2階層以上くだるパスが扱えないという問題が発生している。標準的な RESTful API の URL 設計に基づいていればこのような URL になることは珍しいかもしれないが、それにしてもモックサーバ用ライブラリの都合で URL が決まってしまうのはつらい。
api
オブジェクトのキー名を'book/genres'
と指定してもダメで、Collection 'book' not found"
というエラーになってしまう。book: { genres: { ... } }
のように入れ子のオブジェクトにしたら…?と思ったが、コレだとbook
が配列でないためにエラーになる。
2階層以上のパスを解釈させる方法
では、どうやって /book/genres
のような2階層以上のパスを解釈させるか、というと、InMemoryDbService
が持つ parseRequestUrl()
というメソッドをオーバーライドし、リクエスト URL を内部的に1階層のパスに変換することで対処できる。
実際にやってみる。MockWebApiService
に以下のように bookGenres
データと parseRequestUrl()
メソッドを追加する。
import { InMemoryDbService, ParsedRequestUrl, RequestInfoUtilities } from 'angular-in-memory-web-api';
@Injectable()
export class MockWebApiService implements InMemoryDbService {
/** モックデータ : 標準的な Web API の URL と対応させるため、データは配列で定義し、各要素は id プロパティが必須 */
private api: any = {
// ユーザ情報とか
users: [ /* …中略… */ ],
// 本のジャンル名 : '/book/genres' でアクセスされた時に対応させるデータ
bookGenres: [
{ id: 1, genre: 'Sience Fiction' },
{ id: 2, genre: 'Drama' },
{ id: 3, genre: 'History' }
],
};
// …中略…
/**
* InMemoryDbService から継承 : リクエスト URL を変換する
*
* @param url リクエスト URL
* @param utils リソース情報のユーティリティ
* @return 変換された URL 情報
*/
public parseRequestUrl(url: string, utils: RequestInfoUtilities): ParsedRequestUrl {
// 'book/genres' という URL を 'bookGenres' に変換する
const replacedUrl = url.replace('book/genres', 'bookGenres');
// リクエスト情報を変換する
return utils.parseRequestUrl(replacedUrl);
}
parseRequestUrl()
の第1引数の URL 文字列を replace()
で変換し、第2引数の RequestInfoUtilities
を使って ParsedRequestUrl
型に変えてやれば良い。
ココでの URL 置換処理は全ての通信に影響するので、慎重に変換したい。例えば以下のような汎用的な置換処理でも同等の結果は得られるが、こうすると今度は /users/2
といった URL が /users2
のようになってしまうため、あまり使えない。
const replacedUrl = url
.replace('mock-server/', '') // 先頭の 'mock-server/' を除去する : これによりトップレベルのパスは置換対象外にする
.replace(/\/./g, (match) => {
// '/a' 部分を取得し、'A' と大文字化する : これにより 'book/genres' は最終的に 'bookGenres' と置換される
return match.replace(/\//g, '').toUpperCase();
})
.replace(/^/, 'mock-server/'); // 先頭に 'mock-server/' を再度付与する
面倒だが、必要なパスだけ置換するようにした方が良いかも。
実際にデータを取得してみる
それでは実際にデータを取得してみよう。コンポーネントの HTML に以下を追加する。
<li><button type="button" (click)="doGet('book/genres')">本のジャンル情報を取得する</button></li>
これで mock-server/book/genres
への HTTP 通信が発生するが、parseRequestUrl()
が URL を /mock-server/bookGenres
に変換するため、api
に宣言された bookGenres
オブジェクトが取得され、以下の結果が得られる。
[ { "id": 1, "genre": "Sience Fiction" }, { "id": 2, "genre": "Drama" }, { "id": 3, "genre": "History" } ]
思ったように動いてくれた。
レスポンスデータをカスタマイズする
今度は全く別の例。
次は、/blood-type
というパスにアクセスした時に、配列形式ではなく単一のオブジェクトを返却するようにしたい。
Angular In Memory Web API の標準仕様では、前述の /users
のように、配列形式でしかデータを取得できないはずだ。しかし、responseInterceptor()
というメソッドをオーバーライドすると、特定のパスへのアクセス時にレスポンスデータをカスタマイズできるのだ。
実際に実装してみる。MockWebApiService
に以下のように blood-type
モックデータと responseInterceptor()
メソッドを追加する。
import { Injectable } from '@angular/core';
import { InMemoryDbService, ParsedRequestUrl, RequestInfoUtilities } from 'angular-in-memory-web-api';
@Injectable()
export class MockWebApiService implements InMemoryDbService {
/** モックデータ : 標準的な Web API の URL と対応させるため、データは配列で定義し、各要素は id プロパティが必須 */
private api: any = {
users: [ /* 中略 */ ],
bookGenres: [ /* 中略 */ ],
// 血液型情報 : '/blood-type' で内部のオブジェクト部分のみ返却させる
'blood-type': [
{
id: 0,
// この部分を返却する
data: {
A: 'A型',
B: 'B型',
AB: 'AB型',
O: 'O型'
}
}
]
};
// …中略…
/**
* InMemoryDbService から継承 : データを返却する処理
*
* @param responseOptions レスポンスデータを含むオブジェクト
* @param requestInfo リクエスト情報
*/
public responseInterceptor(responseOptions: any, requestInfo: any): any {
// '/blood-type' へのアクセス時は内部のオブジェクトデータを返却する
if(requestInfo.collectionName === 'blood-type') {
responseOptions.body = this.api['blood-type'][0].data;
}
return responseOptions;
}
responseInterceptor()
で responseOptions.body
にお好みのデータを代入し直してやれば、レスポンスデータを変更できる。ココでは、キー blood-type
の配列0番目の要素が持つ data
プロパティの値、つまり連想配列オブジェクトを返却するようにした。
なお、ココで参照している requestInfo.collectionName
は、parseRequestUrl()
メソッドで変換した後の URL となる。つまり、先程の書籍一覧のレスポンスデータを操作したい場合は、'book/genres'
ではなく 'bookGenres'
であるかどうかでチェックしないといけない。
HTML 側にこのパスを叩くボタンを追加して、実際に動作させてみよう。
<li><button type="button" (click)="doGet('blood-type')">血液型情報を取得する</button></li>
これで、以下のような結果が得られるはずだ。
{ "A": "A型", "B": "B型", "AB": "AB型", "O": "O型" }
まとめ
angular-in-memory-web-api
というパッケージを使うと、Angular4 以降のアプリでモック API を実現できる。InMemoryDbService
を継承したクラスを用意し、createDb()
メソッドをオーバーライドすることで、URL と返却データを定義する。parseRequestUrl()
をオーバーライドして URL 文字列を変換すれば、2階層以上のパスでもアクセスできる。responseInterceptor()
をオーバーライドすると、特定のパスでレスポンスデータをカスタマイズできる。
かなり機能が多く、まだまだ紹介しきれていないノウハウも多々ある。設定の癖も強いので、一つずつ調べながら実装しよう。