HOME
2023.03.24

Connect-WebをMSWでモックする方法|Webフロントエンド

CODEAPIフロントエンドgRPCConnectMSW
gRPCのロゴ

目次

Connect-WebはgRPCを使いたい人にとっては導入も簡単で使い勝手の良いブラウザ用APIライブラリです。
一方で新しい技術であることからも日本語ドキュメントが少ないという弱点もある。

今回は、Connect-Webのモックについて述べた記事があまりなかったので、これについて調べた内容を書き残します。

Connect-Web

Connect-Webの概要

Connect-Webはブラウザで利用できるgRPC互換のHTTP通信用のライブラリです。
gRPC互換であることからもわかる通りProtocol Buffersを使ってインターフェースの定義を行います。
また、クライアントなどの作成も全てProtocol Buffersに基づいて自動生成することができます。

gRPC-webとの違い

送受信の方法など細かい違いがあれど、一番大きな違いはconnect-webにはproxyが必要ないということです。
このおかげでproxyの煩わしい設定なしにいきなりgRPCを使うことができるため、導入が非常に簡単です。

MSW

MSWの概要

MSW(Mock Service Worker)はサービスワーカーを用いてブラウザとサーバーのAPI通信をモックするためのライブラリです。

モックとは?

そもそもモックとはなにかというと、外見だけそれっぽく作ったとりあえずの試作品のことです。
この場合ではAPIの試作品のことを指しています。
つまり、サーバー側の処理やAPIがまだできていないときに、リクエストに対するレスポンスを適当に作っておくということです。
これによってバックエンドが開発できていなくてもフロントエンドの開発を滞りなく進めることができます。

MSWのメリット

MSWのメリットはいくつかありますが主には次のようなものが挙げられます。

  • 実際のリクエストをそのままインターセプトしてモックできる。
  • GraphQLのモックができる
  • storybook内でも利用できる

特に一番最後のstorybook内でも使えるというのはフロントエンドのコンポーネント開発を行う上では非常にありがたいです。

実装

実際にConnect-Webのチュートリアルをモックしてみる。

Connect-Webのチュートリアル

まず、適当なディレクトリでcreate react-appを行います。
今回は適当にconnectという名前のアプリにしておきます。

npm create vite@latest -- connect-msw-example --template react-ts

次にsrc/protosにeliza.protoを作成します。

cd src
mkdir protos
cd protos
touch eliza.proto

これがAPIのインターフェース定義になっており、これに基づいてAPIのクライアントを作ったりします。

eliza.protoの内容は次のようになります。

syntax = "proto3";

package buf.connect.demo.eliza.v1;

service ElizaService {
  rpc Say(SayRequest) returns (SayResponse) {}
  rpc Converse(stream ConverseRequest) returns (stream ConverseResponse) {}
  rpc Introduce(IntroduceRequest) returns (stream IntroduceResponse) {}
}

message SayRequest {
  string sentence = 1;
}

message SayResponse {
  string sentence = 1;
}

message ConverseRequest {
  string sentence = 1;
}

message ConverseResponse {
  string sentence = 1;
}

message IntroduceRequest {
  string name = 1;
}

message IntroduceResponse {
  string sentence = 1;
}

protoファイルを含めProtocol Buffersについては別記事で詳しく書こうと思います。

次に、eliza.protoから必要なコードを生成します。
そのための準備としてprotoファイルのコンパイラーをインストールします。
今回はConnectのチュートリアルでも紹介されており、Connectと連携が強いbufというコンパイラーを採用します。

npm install --save-dev @bufbuild/protoc-gen-connect-web @bufbuild/protoc-gen-es
npm install @bufbuild/connect-web @bufbuild/protobuf

続いてコンパイラーの設定ファイルを用意します。

package.jsonと同じディレクトリにbuf.gen.yamlを作成し、次の内容を書きます。

version: v1
plugins:
  - name: es
    out: src/codegen
    opt: target=ts
  - name: connect-web
    out: src/codegen
    opt: target=ts

さらに、protoファイルをコンパイルするスクリプトをpackage.jsonに登録しておきます。

"scripts": {
  ...
  "buf:generate": "buf generate ./protos"
},

これでターミナルに次のように入力すればsrc/codegen下にtypescriptファイルが生成できます。

npm run buf:generate

App.tsxの内容を次のように書き換えるとチュートリアルのアプリを動かせるようになります。

import { useState } from 'react'
import './App.css'

import {
  createConnectTransport,
  createPromiseClient,
} from "@bufbuild/connect-web";
import { ElizaService } from './codegen/eliza_connectweb';

// Import service definition that you want to connect to.

// The transport defines what type of endpoint we're hitting.
// In our example we'll be communicating with a Connect endpoint.
const transport = createConnectTransport({
  baseUrl: "https://demo.connect.build",
});

// Here we make the client itself, combining the service
// definition with the transport.
const client = createPromiseClient(ElizaService, transport);

function App() {
  const [inputValue, setInputValue] = useState("");
  const [messages, setMessages] = useState<
      {
        fromMe: boolean;
        message: string;
      }[]
      >([]);
  return <>
    <ol>
      {messages.map((msg, index) => (
          <li key={index}>
            {`${msg.fromMe ? "ME:" : "ELIZA:"} ${msg.message}`}
          </li>
      ))}
    </ol>
    <form onSubmit={async (e) => {
      e.preventDefault();
      // Clear inputValue since the user has submitted.
      setInputValue("");
      // Store the inputValue in the chain of messages and
      // mark this message as coming from "me"
      setMessages((prev) => [
        ...prev,
        {
          fromMe: true,
          message: inputValue,
        },
      ]);
      const response = await client.say({
        sentence: inputValue,
      });
      setMessages((prev) => [
        ...prev,
        {
          fromMe: false,
          message: response.sentence,
        },
      ]);
    }}>
      <input value={inputValue} onChange={e => setInputValue(e.target.value)} />
      <button type="submit">Send</button>
    </form>
  </>;
}

export default App

試し起動してみる。

npm run dev

すると次のようなアプリが起動し、connectが無事に動いていることが確認できる。
connect-msw.gif
ここまでがconnectのチュートリアルの内容です。
次は、現在バックエンドと通信している部分をモックに置き換える作業を行っていきます。

MSWの導入

まず、以下のコマンドでMSWを導入します。

npm install msw --save-dev

次にいくつかの設定を行っていきます。
まず、MSWの初期化とサービスワーカーの配置を行うために次のコマンドを打ちます。

npx msw init public/ --save

これでブラウザからのリクエストをサービスワーカーがインターセプトすることができるようになります。

続いて、src下にmockディレクトリを作成し、その中にhandlers.tsとbrowser.tsを作成します。

cd src
mkdir mock
cd mock
touch handlers.ts browser.ts

handlers.tsにはモックの挙動、すなわちどんなリクエストに対してどんなレスポンスを返すかということを書いていきます。
今回は先程作ったelizaアプリの入力をインターセプトしたいので次のように書きます。

import { rest } from 'msw'
import { SayResponse } from '../codegen/eliza_pb'
import { ElizaService } from '../codegen/eliza_connectweb'

const payload = new SayResponse({sentence: "This is mock response"})

export const handlers = [
  rest.post(`https://demo.connect.build/${ElizaService.typeName}/${ElizaService.methods.say.name}`, (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json(payload)
      )
    }),
]

connectで行われていることはほとんどrestと同じなので、rest用のhandlerでインターセプトが行なえます。
ただし、ステータスコードなどに細かな違いがあるのでその点だけ注意する必要があります。
詳しくはconnectのドキュメントのconnect protocolの項が参考になります。

また、browser.tsではワーカーインスタンスをセットアップしておきます。

import { setupWorker } from 'msw'

import { handlers } from './handlers'

export const worker = setupWorker(...handlers)

これで準備が整ったので

実際にモックを行ってみます。

試しにApp.tsxの最上段から数行を次のように書き換えてみます。

import { useState } from 'react'
import './App.css'

import {
  createConnectTransport,
  createPromiseClient,
} from "@bufbuild/connect-web";
import { ElizaService } from './codegen/eliza_connectweb';
import { worker } from './mock/browser'
if (process.env.NODE_ENV === 'development') {
  await worker.start()
}

これでアプリを再起動すると次のようになります。
connect-msw-mock.gif
しっかりとモックができていることが確認できます。

終わりに

本記事ではconnectをMSWでモックする際の手順について説明しました。
connectは新しい技術ですが、protocol buffersの有用性なども考えると今後も使われる技術になっていくのではないかと思います。
今後もgRPC、protocol buffers界隈の話題は注目していきたいですね。

ホントはMSWのstorybook addonを使うところまで説明したかったのですが、今日はつかれたのでまた別記事を作ろうと思います。

CODE

© Asunaro 2022