ITと筋トレの二刀流

未だゼロ刀流

Angularのモックサーバーを簡単に実現する

f:id:tatsuyashi:20190127002850p:plain:w200

Angularで開発する際に、バックエンドのAPIがまだ実装されていない状態でAngularの実装を行うことは多いと思います。

その場合にAPIを一旦モックにして進めると思いますが、選択肢としては、

  • モック用のAPIサーバを用意する
  • in-memory-web-apiを使用する
  • serviceメソッドの戻りを固定値にする

などが考えられると思います。

今回の記事では私が実際に行っている方法で、1〜3とは異なるモックの仕組みを紹介したいと思います。

ソースコードは以下で公開してますので、記事を飛ばしたい方はこちらから確認してみてください。

GitHub - tatsuyashi/angular-mock-sample

キモとなるコミットはこちら↓
use mock on dev-server · tatsuyashi/angular-mock-sample@1889bd6 · GitHub

やりたかった事

私がモックの仕組みを作成するにあたり、以下の条件を満たすものを考えました。

  • 他のライブラリに依存しない
  • 実際にHTTP通信を発生させる
  • モック→実APIへの切り替えは設定ファイル以外触らない
  • production環境には一切乗らない(bundleさせない)

モックの仕組み

今回のモックの仕組みを簡単に図にすると以下のようになります。

f:id:tatsuyashi:20190127012006p:plain:w500

ポイントをまとめると、

  • 開発時に起動するdev server上でモックとなるJSONを公開する
  • JSONのパスは /mock/{HTTPメソッド}/{URL}.json とし、APIのメソッドとURLに対応したファイルパスで作成する
  • production時は実APIをそのまま呼び出す
  • 上記の仕組みをHttpInterceptorを使用して実現する

となります。

dev serverをそのままモック置き場として代用することで、別でAPIサーバを立てる必要もないですし、アプリケーションから実際にHTTP通信が行われるようになります。

それでは、次章から実際のコードを基にポイントを説明していきます。

ソースコードはこちら↓
GitHub - tatsuyashi/angular-mock-sample

サンプルアプリケーションの説明

今回のサンプルですが、GET・POSTのボタンからそれぞれGET・POSTのAPIを呼び出し、結果をJSON形式で画面に表示するシンプルなものになります。

f:id:tatsuyashi:20190127013958p:plain:w500

dev serverにモックを公開する

まずはdev server上にモックを公開する手順を説明します。

①モックファイルを作成する

最初にモックとなるファイルを作成して配置します。

APIのメソッドとURLに対応したパスにJSONファイルを作成します。

メソッド URL JSONファイルパス
GET api/guitars src/mock/GET/api/guitars.json
POST api/guitars src/mock/POST/api/guitars.json
src/mock/GET/api/guitars.json

[
  {"maker": "Gibson", "name": "Les Paul"},
  {"maker": "Fender", "name": "Stratocaster"},
  {"maker": "Fender", "name": "Telecaster"}
]
src/mock/POST/api/guitars.json

{"maker": "Gretsch", "name": "Silver Falcon"}

②angular.jsonで公開設定

angular.jsonに下記を追加します。

angular.json

      "architect": {
        "build": {
            (略)
            "assets": [
              "src/favicon.ico",
              "src/assets",
              "src/mock"  ←追加
            ]

これでdev serverで/mockも公開されました。

次にproductionのときに/mockを公開しないように設定します。

angular.json

          "configurations": {
            "production": {
              "assets": [  ←追加
                "src/favicon.ico",  ←追加
                "src/assets" ←追加 (src/mockは書かない)
              ],  ←追加
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              (略)

productionの設定内でassetsを上書き、その際にsrc/mockを含めないようにします。

以上でモックファイルの公開とproductionのときに含めない設定が完了しました。

HTTPリクエストの向き先をモックに変える

モックファイルの公開はできたので、次はAPIのHTTPリクエストをモックに向けるような仕組みを入れていきます。

①環境設定ファイルに項目追加

environment.tsにモックを使用するかどうかの値を保持します。

src/environment/environment.ts (environment.prod.tsも同じ)

export const environment = {
  production: false,
  mock: true  ← environment.prod.tsはfalse
};

②モックに向けるHttpInterceptorの実装

モック使用環境において、APIのリクエストをモックに変える部分はAngularのHttpInterceptorの仕組みを使用します。

Angular 日本語ドキュメンテーション

今回は以下のようにモック用のHttpInterceptorを用意します。

src/app/http-interceptors/mock-http-interceptor.ts

@Injectable()
export class MockHttpInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest<any>, next: HttpHandler):
    Observable<HttpEvent<any>> {
    
    if (environment.production || !environment.mock) {
      return next.handle(req);
    }

    const mockRequest = this.makeMockRequest(req.method, req.url);
    const newReq = req.clone({ ...mockRequest });
    return next.handle(newReq);
  }

  /**
   * make mock method and url.
   * @param method http method
   * @param url request url
   */
  private makeMockRequest(method: string, url: string): { method: string, url: string} {
    // method
    const mockMethod = 'GET';
    // change url
    const mockUrl = `mock/${method}/${url}.json`;

    return {method: mockMethod, url: mockUrl};
  } 


}

解説

makeMockRequestメソッドでモック用のHTTPメソッドとURLのセットを作成しています。

private makeMockRequest(method: string, url: string): { method: string, url: string} {
    // method
    const mockMethod = 'GET';
    // change url
    const mockUrl = `mock/${method}/${url}.json`;

    return {method: mockMethod, url: mockUrl};
  } 

※HTTPメソッドがGET固定になっていますが、これは現在の課題となっていて、GET以外の場合はURLが合っていても404(Not Found)になってしまうため、どんなHTTPメソッドであってもGETに変換するようにしています。

作成したモックのリクエストを返すことで向き先がモックに変わります。

  intercept(req: HttpRequest<any>, next: HttpHandler):
   (略)

    const mockRequest = this.makeMockRequest(req.method, req.url);
    const newReq = req.clone({ ...mockRequest });
    return next.handle(newReq);
  }

production環境の場合や、モックを使用しない設定になっている場合はそのまま返します。

  intercept(req: HttpRequest<any>, next: HttpHandler):
    Observable<HttpEvent<any>> {
    
    if (environment.production || !environment.mock) {
      return next.handle(req);
    }
    (略)

③モック用のHttpInterceptorを有効にする-1

作成したHttpInterceptorを有効にするために、まずHttpInterceptorProvidersを作成します。

src/app/http-interceptors/index.ts

const devHttpInterceptorProviders = environment.production ? [] :  [
  { provide: HTTP_INTERCEPTORS, useClass: MockHttpInterceptor, multi: true },
];

export const httpInterceptorProviders = [
  ...devHttpInterceptorProviders,
  { provide: HTTP_INTERCEPTORS, useClass: XxxInterceptor, multi: true },
];

解説

devHttpInterceptorProvidersという開発環境のみ発動するHttpInterceptorProviderをまとめます。
productionモード時は空配列が返ります。
これを行うことでng build --prodでビルドした際にMockHttpInterceptorがbundleされなくなります。

const devHttpInterceptorProviders = environment.production ? [] :  [
  { provide: HTTP_INTERCEPTORS, useClass: MockHttpInterceptor, multi: true },
];

devHttpInterceptorProvidersとどの環境でも発動するHttpInterceptorProviderをまとめてexportします。
(XxxInterceptorは特に中身はありませんが、何かしらのInterceptorを定義した場合を想定しています)

export const httpInterceptorProviders = [
  ...devHttpInterceptorProviders,
  { provide: HTTP_INTERCEPTORS, useClass: XxxInterceptor, multi: true },
];

③モック用のHttpInterceptorを有効にする-2

exportしたHttpInterceptorProvidersをModuleでproviderとして設定します。

src/app/app.module.ts

  providers: [
    httpInterceptorProviders,
  ],

以上でdev serverを使ったモックの完成となります。

まとめと課題

この仕組みで冒頭に述べたやりたかった事が全て実現できるようになりました。

  • 他のライブラリに依存しない
    →追加ライブラリなし
  • 実際にHTTP通信を発生させる
    →dev server(port 4200)にアクセスする
  • モック→実APIへの切り替えは設定ファイル以外触らない
    →environment.tsの変更のみ。serviceクラスも変更不要
  • production環境には一切乗らない(bundleさせない)
    →モックファイルおよびモックのためのHttpInterceptorクラスはproductionビルド時にはbundleされない

ただ、いくつか課題は残っています。

  • POST、PUTなどのGET以外のAPIはHTTPメソッドを無理矢理GETに変えている
    →POST、PUT、DELETEはそのままリクエストすると404になるので苦肉の策でGETに変えてリクエストしています。
    dev serverの設定を変えればできるのかもしれませんが、方法がわからないため課題となっています。

  • JSON以外のファイル形式に対応していない
    →9割以上のAPIJSONを返すと思うためJSONしか対応していませんでしたが、その他のファイル形式には対応していません。

特に1つ目の課題はトライしましたが全くうまくいかなかったのでわかる方がいれば・・・

最後に、
Angularの開発を行う際にそのプロジェクトに応じた共通基盤を作成すると思いますが、その1つしてモックは出てくると思いますので、 今回紹介した方法がお役に立てれば幸いです。