Angular in-memory-web-api はどのような仕組みで動作しているか

こんにちは、白川です。

最近はバックエンドだけでなくフロントエンドも担当することがあり、Angular を触る機会が増えています。
今回は Angular の勉強がてら、Angular in-memory-web-api がどのような仕組みで動作しているかを見てみました。

Angular in-memory-web-api とは

Angular in-memory-web-api は、REST API による CRUD 操作をエミュレートするインメモリの Web API です。
バックエンドサーバが完成していなくても、CRUD データの永続化操作をシミュレートしてくれるため、Angular アプリの開発時やテスト時に Web API モックとして使用されます。

Angular in-memory-web-api の使い方ですが、
まず、以下のような感じで Web API のモックとして、レスポンスデータ等を定義するデータストアサービスを実装します。

そして、Angular in-memory-web-api が用意した HttpClientInMemoryWebApiModule に上記データストアサービスを登録し、
モックサーバとして動作させるために、AppModule に以下のように追加します。
(AppModule は、Angular アプリが起動するときに、最初に立ち上がるルートモジュールのことです。)
この時、HttpClientInMemoryWebApiModuleHttpClientModule より後にインポートする必要があります。
この理由は後述します。

上記の設定をするだけで、HttpClient 経由でモックデータを取得することが可能となります。
( HttpClient は、Angular アプリケーション用のシンプルなクライアント HTTP API を提供するクラスです。)

では、少し Angular の内部に入って見ていくことにします。

HttpClientModule と Angular の依存性注入(DI)

Angular モジュールとは、Angular アプリを構成する部品をまとめておく仕組みのことで、
Angular も複数のモジュールから構成されており、アプリに必要なモジュールをインポートして利用します。
( Angular モジュールの詳細はこちらをご覧ください。)

Angular in-memory-web-api を利用するため、
Angular のルートモジュールである AppModuleHttpClientModule と、HttpClientInMemoryWebApiModule をモジュールとしてインポートしていました。

まず、HttpClientModule の中身を見ていきましょう。
ここで providers に注目してみます。
providers の中に複数の設定が見られますね。

providers は、ビジネスロジックを含むサービスクラスを Angular の依存性の注入 (DI) インジェクターに登録し、
実際に依存性を注入する際のサービスクラスのインスタンス作成の方法を指定しています。

上記は省略された表記で、省略せずに書くとこんな感じになります。

provide というのは、DI トークンのことで、依存関係を要求されたときに参照する内部トークンプロバイダーマップにおけるルックアップキーです。
ここではクラス型を provide として指定しており、依存関係を要求する側のコンポーネントなどのコンストラクタの引数の型指定をDIトークンと同じにすることで、
DI インジェクターからインスタンスを取得することができます。
以下の例では、インジェクターから HttpClient のインスタンスを取得しています。

useClassuseExisting はインスタンスの作り方を指定しています。

useClass は値としてクラスを受け取り、依存関係を要求されるたびにそのクラスのインスタンスを作り直します。

Angular は階層性をもった DI インジェクターシステムを採用しており、
アプリのコンポーネントツリーと並行する形でインジェクターツリーを持ちます。

AppModule が持つ DI インジェクターはアプリ全体のインジェクター階層のルートです。
依存関係を要求する側であるコンポーネントも DI インジェクターを持つことができますが、
自身の DI インジェクターに要求された DI トークンが存在しない場合は、親の DI インジェクターを参照します。
この場合、インスタンスの生成は親の DI インジェクターが行なうため、useClass を指定していたとしても、
ルートモジュールで、コンポーネントに注入されるサービスクラスを useClass 指定で登録することで、全て同じインスタンスを参照することになります。

useExisting は、値として DI トークンを受け取り、指定されたDIトークンに対するエイリアスを作ります。
異なる DI トークンで、同じインスタンスを得られるようになります。
下記の例だと、HttpBackend という DI トークンで、HttpXhrBackend のインスタンスを得ることができます。

multi は同じ DI トークンで、複数の値を得たい場合に使用します。
省略した場合は、false になるため、HttpClientModule では全て false です。

AppModule の中で HttpClientModule をインポートすることで、
HttpClientHttpInterceptingHandlerHttpXhrBackendなどが DI インジェクターに登録され、
アプリケーション全体で使用できるようになります。

DI のイメージはこんな感じです。


 

多段インジェクション

次に、HttpClientModule で provider 指定されている HttpClient を見ていきます。

コンストラクターの引数に HttpHandler が指定されているため、
自身のコンストラクタが呼ばれた場合、HttpHandler 型のインスタンスを要求し、それを handler という private 変数にインジェクションされます。
HTTP リクエストの実行は、request メソッドで行なっていますが、
実際には HttpHandler に処理を委譲していることが読み取れます。

次に、HttpHandler を見ていきましょう。実体は、HttpInterceptingHandler クラスです。

自身のコンストラクタが呼ばれた場合、HttpBackend 型のインスタンスを要求し、それを backend という private 変数にインジェクションされます。
(インターセプタが指定されていれば)先にインターセプタが実行され、その後に HttpBackend にリクエスト処理を委譲しています。

次に、HttpBackend を見ていきましょう。その実体は、HttpXhrBackend クラスです。

handle メソッド内部で、XMLHttpRequest を生成して、HTTP リクエストを送信していることが分かります。

HttpClient -> HttpHandler -> HttpBackend と多段に関連するサービスクラスがインジェクションされていって、
HTTP リクエスト処理のそのものは、以下の図のように HttpBackend クラスが担っていることが分かりました。


 

HttpClientInMemoryWebApiModule は何をしているのか

では、HttpClientInMemoryWebApiModule は何をしているのでしょうか。
AppModule では、以下のように指定していました。

HttpClientInMemoryWebApiModuleforRoot メソッドの中を見ていきます。

forRoot メソッドの中の第一引数に指定した値を、DI トークン InMemoryDbService として、
第2引数に指定した値を、DI トークン InMemoryBackendConfig としてインジェクターに登録しています。

そして、DI トークン HttpBackend に、useFactory: httpClientInMemBackendServiceFactory を登録しています。

ここで、もう一度 HttpClientModuleproviders を見てみると、HttpBackend という DI トークンが宣言されています。

同じ DI トークンが providers 内で複数回宣言された場合、後勝ちになります。
つまり、以下の図のように HttpBackend という DI トークンで得られるインスタンスが、HttpXhrBackend クラスから、useFactory: httpClientInMemBackendServiceFactory で生成されるインスタンスに置き換えられます。
(HttpClientInMemoryWebApiModuleHttpClientModule より後にインポートする必要がある、という理由がここで分かりました。)


 

useFactory には関数を指定し、依存関係を要求されるたびに関数が実行され、その関数の戻り値がインジェクションされます。
deps には、useFactory で指定した関数に渡す引数を指定します。

httpClientInMemBackendServiceFactory を見てみると、HttpClientBackendService クラスのインスタンスを生成して返しているのが分かります。
この HttpClientBackendService が HTTP リクエストを処理することで Web API モックを実現しているという訳です。

モックの実際の動き

HttpClientBackendService クラスを見ると、処理の実体は handleRequest メソッドにありそうな感じです。

handleRequest メソッドは、親クラス BackendService にあるので、そちらを見てみます。

Request evaluation order に、モック内部でどのように HTTP リクエストの処理が行われているかが記載されていますが、handleRequest メソッドに処理が実装されていることが分かります。
(以下引用です)

  1. If it looks like a command, process as a command
  2. If the HTTP method is overridden, try the override.
  3. If the resource name (after the api base path) matches one of the configured collections, process that
  4. If not but the Config.passThruUnknownUrl flag is true, try to pass the request along to a real XHR.
  5. Return a 404.

また、bind というメソッドで、InMemoryDbService に実装されている URL パーサや、HTTP メソッドインターセプターを取得しています。
InMemoryDbService の実装クラスで parseRequestUrl というメソッドをオーバーライドし、リクエスト URL の解釈を変更したり、
getpost というメソッドを実装することで、該当する HTTP メソッドの動きをカスタマイズしたりできます。

おわりに

Angular in-memory-web-api の使い方だけみても簡単に使えそうだけどイマイチ仕組みが分からないので、カスタマイズできるポイントがどこなのかがよく分からなかったのですが、ソースコードを読んでいくことで仕組みや、どういったカスタマイズができるか理解できました。

最後に、Angular の DI や、HttpClient の仕組みの理解の助けになる文献を載せておきます。

Comments are closed, but you can leave a trackback: Trackback URL.