クラウドワークス エンジニアブログ

日本最大級のクラウドソーシング「クラウドワークス」の開発の裏側をお届けするエンジニアブログ

OpenAPIとMSWを使ってユーザーのフォロー機能を実装した話

サムネイル

はじめに

こんにちは。

crowdworks.jpでユーザー向けの施策・機能開発を行うチームでエンジニアをしている駒井です。

2022年10月にクラウドワークスに入社してから早4ヶ月が経ちました。 crowdworks.jpでは負債解消の一環で、一部のRuby on Railsのerbで作られた画面をVue.jsに置き換える取り組みを行っています。(社内ではVue.js化と呼ばれています。) この記事では、施策チームで現在注力しているcrowdworks.jpのクライアントプロフィール画面のVue.js化の中で、ユーザーのフォロー機能を実装した際に扱った技術などについて紹介します。

対象について

本記事ではVue.js化を行っているクライアントプロフィール画面にある、プロフィール画像やフォローボタンが配置されている サイドバーの フォローボタン を対象として取り上げます。

クライアントプロフィールサイドバーのフォローボタン

Vue.js化における前提

クライアントプロフィール画面では、Railsのerbで実装されている箇所と既にVue.jsで実装されている箇所が混在していました。 その為、erbで実装されている箇所はVue.jsで書き直し、既にVue.jsで実装されているコンポーネントと組み合わせながら新たに実装する必要がありました。 自分は主にサイドバーのVue.js化を担当したのですが、サイドバーの中でも フォローボタンのVue.js化 ではシンプルな見た目に反して考慮すべきことが意外と多く、なかなか大変でした。 実装するにあたり、フロントエンドのデザインパターンで参考になったのは以下の記事です。

engineer.crowdworks.jp

crowdworks.jpのコンポーネント設計においては、こちらの記事にある通りContainer/Presentationalパターンの考え方を取り入れています。 この設計パターンに基づき、先述したフォローに関する実装はComposableとして切り出し、Containerコンポーネントで実行されるような形とすることで、API通信などのアプリケーションの動作に関する責務を負うContainerコンポーネントと、サイドバーのコンポーネント群を内包しUIのみに責務を負うPresentationalコンポーネントを実装します。

発注者のフォロー機能

先述の通り、crowdworks.jpには仕事の発注者をフォローする機能があります。 フォローするボタンを押して発注者をフォローすることで、その発注者の仕事依頼情報を受け取ることができるようになります。 こちらを実装する上で、フォローボタンはいくつかの状態を持つことになります。 例えばAPIに対してリクエストを送っている最中のloading状態です。 フォローするボタンの押下時にはAPIに対してPOSTのHTTPリクエストが送られます。そのレスポンスが返るまではボタンをdisabledにしておき、ボタンの連打による複数回のリクエストを防ぐ必要があります。レスポンスが返却されたらdisabledを解除し、フォロー中のボタンを表示します。 フォロー解除ではDELETEのHTTPリクエストが送られますが振る舞いとしては同様ですね。それぞれのUIの変化としては以下のようなイメージになります。

  • フォロー時

フォロー実行時のフロー

  • フォロー解除時 フォロー解除時のフロー

また、何らかの理由でリクエストが失敗するケースもあるかもしれません。失敗時にはフォローが失敗したことをUIで表現する必要もありますね。 フォロー失敗時のUI

これらを実装するには、実際にAPIにリクエストを送って動作確認しながら作業することも可能ですが、なるべくバックエンドの実装に依存したフロントエンド開発にならないようにすると効率的です。現在、クラウドワークスのフロントエンド開発ではStorybook駆動でのコンポーネント実装を進めているため、Storybookでの利用を前提としたモックを実装できるとフロントエンドのみで振る舞いの確認もできるということで Mock Service Worker(以下MSW) を利用し、バックエンドからの返却値の定義に関しては OpenAPI を用いて実装を進めました。

OpenAPIとは

最初にOpenAPIによるschemaの定義から進めました。 まずOpenAPIとはなんぞや、ということで公式から引用です。

swagger.io

The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to RESTful APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. When properly defined, a consumer can understand and interact with the remote service with a minimal amount of implementation logic.

An OpenAPI definition can then be used by documentation generation tools to display the API, code generation tools to generate servers and clients in various programming languages, testing tools, and many other use cases.

上記の引用文をGoogle翻訳で日本語訳したものが以下になります。

OpenAPI 仕様 (OAS) は、言語に依存しない標準の RESTful API へのインターフェイスを定義します。これにより、人間とコンピューターの両方が、ソース コードやドキュメントにアクセスしたり、ネットワーク トラフィックの検査を行ったりすることなく、サービスの機能を発見して理解できるようになります。適切に定義されていれば、コンシューマは最小限の実装ロジックでリモートサービスを理解し、対話することができます。

その後、OpenAPI 定義は、API を表示するためのドキュメント生成ツール、さまざまなプログラミング言語でサーバーとクライアントを生成するためのコード生成ツール、テスト ツール、およびその他の多くのユースケースで使用できます。

こちらの文言からまとめると、 言語に依存しないRESTful APIインターフェイスを定義するためのフォーマット で、 人間とコンピュータの両方が読むことができ 、さらに 定義を元に様々なツールで使える 便利な仕様といったところでしょうか。 下記はymlで定義したフォロー実行時のschemaの例です。

FollowRelationshipsPostResponse:
  description: 自分以外のユーザをフォローしていることを表現するオブジェクト
  type: object
  required:
    - id
    - follower_id
    - followed_id
  properties:
    id:
      description: フォローしていることを表すオブジェクトのID
      type: number
      example: 1
    follower_id:
      description: フォローしているユーザのID
      type: number
      example: 1
    followed_id:
      description: フォローされているユーザのID
      type: number
      example: 2

このように、フロントエンドに返却するJSONデータをyml形式で記述したドキュメントのようなイメージですね。これを見ることでフォロー実行をするとどんなレスポンスが返却されるかが一目瞭然です。 また、OpenAPI generatorというソフトウェアを利用することでOpenAPI schemaから各種言語のフロントエンド・バックエンドのコードの自動生成もしてくれるようです。(凄すぎる) この辺りの開発環境は弊社の負債解消チームであるジャンヌチームらが、より便利・効率的に開発ができるように整備してくれています。(圧倒的感謝...!)

Mock Service Worker(MSW)とは

OpenAPIでの定義が済んだら、MSWを使用しフロントエンドの実装を進めていきました。 例の如く公式から引用です。

mswjs.io

Mock by intercepting requests on the network level. Seamlessly reuse the same mock definition for testing, development, and debugging. ネットワークレベルでリクエストをインターセプトすることでモックを作成します。テスト、開発、デバッグのために同じモック定義をシームレスに再利用します。

Service Workerのレイヤーでリクエストを受け取りモックを作成することができるようです。 では、例としてフォローをする時のサンプルコードを書いてみましょう。

モックの定義(followPostMock.ts)

import { DefaultBodyType, MockedRequest, rest, RestHandler } from 'msw';

export const followPostMock = (): RestHandler<MockedRequest<DefaultBodyType>>[] => [
  rest.post('/api/follow/post', (req, res, ctx) => {
    const followRelationships = {
      id: 1,
      follower_id: 1,
      followed_id: 2,
    };
    return res(ctx.status(200), ctx.json(followRelationships));
  }),
];

軽く解説すると、MSWからimportしたrestには、REST APIリクエストのモックを作成するのに便利なリクエストハンドラのセットが含まれます。こちらのpostメソッドを使い、フォロー実行時には/api/follow/postへのPOSTリクエストが送信され、レスポンスとしてid、follower_id、followed_idといったプロパティを含んだJSONが返却されるようなhandlerを定義します。こちらをテストやStorybookで使用していきます。

またここでは記載していませんが、networkError()というメソッドを使うことでネットワークエラーとしてレスポンスを返すこともできるようです。ユーザー環境などによりネットワークエラーになった時のエラーハンドリングを実装する際にも使えそうということでとても便利ですね。

テストの実装(followPost.test.ts)

import { setupServer } from 'msw/node';
import { followPostMock } from './followPostMock';
import { follow } from './follow'; // フォローを実行(リクエストを送信)し、stateを変更する関数とする

const server = setupServer(...followPostMock());

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('/api/follow/post', () => {
  it('フォローが成功すること', async () => {
    const responseData = await follow();

    expect(responseData).toMatchObject({
      id: 1,
      follower_id: 1,
      followed_id: 2,
    });
  });
});

※実際にAPIにリクエストを送り、UIのstateを変更するfollow関数の実装に関してはここでは省略します。 テストでは、まずsetupServerを使ってリクエストをインターセプトするサーバーを定義します。引数には作成したhandlerを渡して使用します。 次にbeforeAll() を使って全てのテストの開始前にモックサーバーをスタートします。 また、複数のテスト・スイート間で干渉し合わないようにafterEach()を使い、各テストが終了するごとにモックサーバーの状態をリセットします。 終了時にはafterAll() で全てのテストが完了したあとにモックサーバーをクローズするようにします。 あとは実際にテストケースを書く形になりますが、上記のようにモックサーバーを別途作成してしまえば、テスト自体の記述がスッキリしますね。

Storybookの実装(FollowButton.stories.ts)

import FollowButton from './FollowButton.vue';
import { followPostMock } from './followPostMock';

export default {
  component: FollowButton,
  parameters: {
    msw: {
      handlers: [...followPostMock()],
    },
  },
};
.
.
.

Storybookでは、mswパラメータのhandlersプロパティにモックを展開して使います。(詳しくはこちらを参考に) このようにMSWを使用してモックサーバーを定義しておくことで、Storybookでも横断してモックを再利用できるようになります。Jestのmock()spyOn()などのメソッドはテスト以外では使えないことを考えると、汎用性が高いですね。あとはStoryを書きます。 そして残りのフォロー解除の機能もサンプルコードを参考に実装し、無事Storybook上でもフォロー・フォロー解除の際の振る舞いを表現することができました(やったね!)

Storybook上でのフォローボタンの振る舞い

終わりに

以上、クライアントプロフィール画面のVue.js化を進める中で、OpenAPIとMSWを使ってユーザーのフォロー機能を実装した話でした。 実装を進めるにあたり、チームメンバーとモブプロをしたのですが、コーディング以外の面でも学びになることが多く、改めてメンバーに感謝すると共に、チームでプロダクトを作るっていいなあと感じました。

余談ですが、自分が所属している施策チームは社内では「 デリバード 」と呼ばれていて、小さい頃から多少なりともゲームをやってきた自分はすぐにポケモンだと分かったのですが、そのチーム名になった理由が気になってメンバーに聞いたことがあります。

ちなみにデリバードは「はこびやポケモン」に分類され、ペンギンのような体型にサンタのような風貌をしたポケモンなのですが、その時に聞いた話によると、デリバードが覚える技で「プレゼント」という技があり、 施策を通してユーザーに価値を提供する(プレゼントする) というミッションを持った施策チームにピッタリでは?という話があり、そこからこの名前をとりデリバードになったそうです。 とてもいいチーム名だなと思い、個人的には気に入っています。 今回のVue.js化もそうですが、引き続きプロダクト開発を通してユーザーにより良い体験・価値を届けられるように精進していきたいですね。

We're hiring!

ここまで読んでいただきありがとうございました。 クラウドワークスでは、働き方の変革に挑戦するエンジニアを募集しています!

www.wantedly.com

© 2016 CrowdWorks, Inc., All rights reserved.