0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Angular v18にmswを導入する手順

Posted at

Mock Service Worker(以下MSW)はHTTP通信をモックする開発用ツールです。フロントエンドでエラー表示やページネーションなどを作り込む時に、欲しいレスポンスがかんたんにシミュレートできて重宝します。

この記事では、AngularでMSWをセットアップする手順と、モックのカスタマイズをいくつか紹介します。

MSWには、サーバーサイドのNode.jsと、ブラウザのService Workerの2通りのセットアップ手順があり、この記事ではブラウザを対象としています。

  • @angular/cli v18
  • msw v2
  • Node.js v22

インストール

既存プロジェクトにパッケージをインストールします。

npm i msw -D 

initコマンド実行

CLIでコマンドを実行して mockServiceWorker.js を追加します。
このファイルはブラウザ上のService Workerとして動作します。

npx msw init ./src --save

./src/
├── app
│   ├── app.component.html
│   ├── app.config.ts
│   └── ...
├── index.html
├── main.ts
├── mockServiceWorker.js # <=== これ
├── mocks
│   └── handlers.ts

ブラウザから参照できるよう angular.json に登録します。

angular.json
"architect": {
	"build": {
    "configurations": {
      "development": {
        "assets": [
+          "src/mockServiceWorker.js"
        ]
      }
    },

ハンドラの追加

HTTP通信をモックする「ハンドラ」用のファイルを追加します。

mkdir src/mocks
touch src/mocks/handlers.ts 

ハンドラの中身は後述します。ここでは空で宣言しておきます。

src/mocks/handlers.ts
export const handlers = [];

MSWを起動

ハンドラを呼び出すスクリプトを追加します。

touch src/mocks/browser.ts
src/mocks/browser.ts

import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);

main.ts を以下のように書き換えます。

src/main.ts

import { isDevMode } from "@angular/core";

async function prepareApp() {
	// ローカル開発環境に限定
  if (isDevMode()) {
    const { worker } = await import("./mocks/browser");
    return worker.start();
  }

  return Promise.resolve();
}

// Angularアプリケーションを起動する直前にService Workerを立ち上げる
prepareApp().then(() => {
  bootstrapApplication(AppComponent, appConfig).catch((err) =>
    console.error(err)
  );
});

ここまでの手順で、Angularプロジェクトのディレクトリ構造は以下のようになります。
●印が付いているファイルはMSWのセットアップによって改変されたファイルです。

├── README.md
├── ● angular.json
├── node_modules/
├── ● package-lock.json
├── ● package.json
├── public/
├── src/
│   ├── app/
│   ├── index.html
│   ├── ● main.ts
│   ├── ● mockServiceWorker.js
│   ├── mocks/
│   │   ├── ● browser.ts
│   │   └── ● handlers.ts
│   └── styles.scss
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json

MSWのGitHubでAngularアプリケーションのサンプルコードが提供されています。
公式ドキュメントで見当たらないものはこちらを参考にすると良さそうです。

https://github.com/mswjs/examples/with-angular

HTTP通信してみる

任意のコンポーネントにHTTP通信の処理を書いて、MSWのモックを試してみましょう。

src/app/app.config.ts

import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
+    provideHttpClient(),
  ]
};
src/app/app.component.ts

import { HttpClient } from '@angular/common/http';
import { Component, inject, OnInit, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    @for (user of users(); track $index) {
      <p>{{ user.name }}</p>
    }
  `,
})
export class AppComponent implements OnInit {
  private readonly api = inject(HttpClient);

  users = signal<User[]>([]);

  ngOnInit() {
    this.api.get<User[]>("/api/users").subscribe(users => {
	    this.users.set(users);
	});
  }
}

type User = {
  id: number;
  name: string;
};

さきほど空にしておいたハンドラを記述します。

src/mocks/handlers.ts

import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("/api/users", () => {
    return HttpResponse.json([
      { id: 1, name: "Bob" },
      { id: 2, name: "Alice" },
    ]);
  }),
];

ハンドラ宣言は以下のようなシンタックスです。
エンドポイントひとつ分を表します。

http.`{メソッド}`(`{URL}`, () => {
	return HttpResponse.json(`{レスポンス}`);
});

エンドポイントを追加するには、配列にハンドラを追加します。

export const handlers = [
  http.get("/api/users", () => { ... }),
  http.get("/api/users/:id", () => { ... }),
  http.post("/api/users", () => { ... }),
  http.put("/api/users/:id", () => { ... }),
  http.delete("/api/users/:id", () => { ... }),
];

Angularアプリケーションを起動してブラウザを確認してみましょう。

npm start

GET /api/users に対して、モックのレスポンスが返ります。

モックのカスタマイズ

MSWの魅力は、モックのカスタマイズの容易さです。
イレギュラーを含むさまざまなケースで画面表示を作り込む時に活躍します。

  • 巨大なリストが返るケース
  • レスポンスが遅延するケース
  • クエリ文字列でページネーション指定するケース
  • パスパラメータに応じたデータを返すケース
  • HTTP 500が発生するケース
  • ポーリングするケース

通信用にバックエンドのアプリケーションのコードを改変したり、データベースにダミーデータを登録する必要はありません。TypeScriptで自由自在にモックが作れる便利さは偉大です。

またPOSTやDELETEをリクエストしてもデータが永続化されないので、ブラウザをリロードするだけで「最初からやり直し」ができます。

巨大なリストが返るケース

サイズの大きいリストを生成してレスポンスで返却します。
フィクスチャを宣言しても良いですし、TypeScriptで動的生成することもできます。

export const handlers = [
  http.get("/api/users", () => {
    const users = Array.from({ length: 10000 }).map((_, index) => {
      return {
        id: index + 1,
        name: `User ${index + 1}`,
      };
    });

    return HttpResponse.json(users);
  }),
];

// GET /api/users
// [
//   { id: 1, name: "User 1" },
//   { id: 2, name: "User 2" },
//   { id: 3, name: "User 3" },
//   ...(10000件)...
//  ]

レスポンスが遅延するケース

ユーティリティ関数 delay を使います。

https://mswjs.io/docs/api/delay/

import { delay } from "msw";

export const handlers = [
  http.get("/api/users", async () => {
    await delay(10000);

    return HttpResponse.json([{ id:1, name: "Bob" }]);
  }),
];

// GET /api/users
// ...(10秒)...
// [{ id:1, name: "Bob" }]

クエリ文字列でページネーション指定するケース

コールバック関数の引数で request オブジェクトを受け取り、クエリ文字列を取り出します。

https://mswjs.io/docs/recipes/query-parameters/

export const handlers = [
  http.get("/api/users", ({ request }) => {
    const users = [{}, {}, {}, ...];

    const page = Number(new URL(request.url).searchParams.get("page"));
    const perPage = 10;
    const first = perPage * (page - 1);
    const last = first + perPage;

    return HttpResponse.json(users.slice(first, last));
  }),
];

// GET /api/users?page=1
// [{}, {}, {}, ...]

パスパラメータに応じたデータを返すケース

コールバック関数の引数で params オブジェクトを受け取り、パラメータを取り出します。

https://mswjs.io/docs/network-behavior/rest/#reading-path-parameters

export const handlers = [
  http.get("/api/users/:id", ({ params }) => {
    const { id } = params;

    switch (Number(id)) {
      case 1: return HttpResponse.json({ id: 1, name: "Bob" });
      case 2: return HttpResponse.json({ id: 2, name: "Alice" });
      default: ...
    }
  }),
];

// GET /api/users/1
// { id: 1, name: "Bob" }

// GET /api/users/2
// { id: 2, name: "Alice" }

HTTP 500が発生するケース

HttpResponse.json のショートハンドを使わずに new HttpResponse() すると、ステータスコードやレスポンスボディを指定できます。

https://mswjs.io/docs/api/http-response

export const handlers = [
  http.get("/api/users", () => {
    return new HttpResponse('Critical Error!', {
      status: 500,
    });
  }),
];

// GET /api/users
// HTTP 500 Critical Error!

ポーリングするケース

ジェネレーター関数を使ってN回分のリクエストをシミュレートします。
ポーリング中にHTTP 500が発生するケースも試せますね。

https://mswjs.io/docs/recipes/polling

export const handlers = [
  http.get("/api/users", function* () {
    const list = [
      { id: 1, name: "Bob" },
      { id: 2, name: "Alice" },
      { id: 3, name: "John" },
    ];

    let count = 0;

    while (count < 3) {
      yield HttpResponse.json(list.slice(0, count + 1));
      count++;
    }

    return HttpResponse.json(list);
  }),
];

// GET /api/users
// [{ id: 1, name: "Bob" }]

// GET /api/users
// [{ id: 1, name: "Bob" }, { id: 2, name: "Alice" }]

// GET /api/users
// [{ id: 1, name: "Bob" }, { id: 2, name: "Alice" }, { id: 3, name: "John" }]

fetch or XHR

https://mswjs.io/docs/limitations/

公式ドキュメントにはfetchをトリガしないHTTP通信について注意が記載されていますが、Angularの withFetch を指定しても(fetch API)、指定しなくても(XMLHttpRequest)、特に変わりなくMSWが動作しました。

src/app/app.config.ts

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [ 
    provideHttpClient(withFetch()), // <===
  ]
};

Chrome 129.0.6668.101

Firefox 131.0.3

MSWの正常な稼働を確認するには、ブラウザのコンソールタブで [MSW] HH:mm:ss {URL} のログが出力された事を確認します。

ログはHTTP通信のたびに吐き出されるため、初回確認の後はオフにしておくとコンソールが静かになります。

src/main.ts

return worker.start({ quiet: true });

以上です。
かんたん便利にHTTP通信をモックできるMSW、気になった方はぜひ使ってみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?