Vue3 + TypeScript + MSWの試しメモ(2)
1. はじめに
前回はVueのプロジェクトを立ち上げて、そこにMSWを導入するところまで行いました。
今回はサンプルアプリを元に、MSWでモックAPIを実装していきたいと思います。
2. サンプルアプリの内容
MSWを用いるサンプルアプリとしてTodoアプリを用います。アプリの仕様は以下の通りです。
- Todoの一覧を取得し表示(GET)
- Todoの追加(POST)
- API接続中に「Loading」と表示
3. モックAPIの作成
最初にモックAPIを実装していきます。
3.1. モックデータの用意
Todoのモックデータを用意します。モックデータのファイルもmockフォルダの配下にまとめておきます。
src/mock/data/todoMockData.json
[ { "id": 1, "title": "Todo-1" }, { "id": 2, "title": "Todo-2" }, { "id": 3, "title": "Todo-3" } ]
idとタイトルだけのシンプルなjsonです。
3.2. handlerの実装
次にMSWのリクエストハンドラーを実装していきます。Todoの一覧を返すGETと、Todoに追加するPOSTのハンドラーを追加します。
3.2.1. ハンドラーの定義
今回はREST APIで作成します。
src/mock/handler.ts
import { rest } from "msw"; export const handlers = [ // GETハンドラー rest.get('/todos', null), // POSTハンドラー rest.post('/todos', null), ]
3.2.2. レスポンスリゾルバの実装
APIのリクエストを処理するために、HTTPメソッドとパスの定義が完了しました。次にget、postのnullになっている第二引数にモック化したレスポンスを返す関数を定義します。
src/mock/handler.ts
import { rest } from "msw"; import todosMockData from "./data/todosMockData.json" // モックデータ export const handlers = [ // GETハンドラー rest.get('/todos', (req, res, ctx) => { // return res( ctx.delay(2000), // レスポンスを2秒遅らせる ctx.status(200), // 200のステータスコードを設定 ctx.json(todosMockData) // モックデータをレスポンスボディに設定 ) }), // POSTハンドラー rest.post('/todos', async(req, res, ctx) => { const { todo } = await req.json(); // 入力したTodoの文字列取得 return res( ctx.status(201), 201のステータスコードを設定 ctx.json({ id: Math.random(), title: todo, }) ) }) ]
レスポンスを返す関数(レスポンスリゾルバ)ではreq, res, ctxを引数にとります。
この中でctxはモックのレスポンスを作成するためのResponse transformerが含まれています。これを使用することで、レスポンスのステータスコードやヘッダ、ボディなどを設定することができたり、レスポンスを遅延させるdelay
なども設定することができます。今回はAPI接続時のローディングも確認したいので、delayを加えました。
3.2.3. TypeScriptの適用
最後に各ハンドラーにTypeScriptを適用します。
src/mock/handler.ts
import { rest } from "msw"; import todosMockData from "./data/todosMockData.json" // Todoの型をApp.vueで定義しているのでimport import type { Todo as postResponseBody } from "../App.vue" interface Todo { todo: string } export const handlers = [ // GETハンドラー rest.get<never, never, postResponseBody[]>('/todos', (req, res, ctx) => { return res( ctx.delay(2000), ctx.status(200), ctx.json(todosMockData) ) }), // POSTハンドラー rest.post<Todo, never, postResponseBody>('/todos', async(req, res, ctx) => { const { todo } = await req.json<Todo>(); return res( ctx.status(201), ctx.json({ id: Math.random(), title: todo, }) ) }) ]
MSWのハンドラーにはRequest Body、Request Params, Response Bodyの型を定義します。
以下の記事を参考に記述を行いましたが、最近のバージョンではRequest ParamsとResponse Bodyが逆になっているので注意が必要です。(Discussionsにその旨の記載があります。)
以上でモックAPIの作成は完了です。
4. Vueコンポーネントの作成
Vueコンポーネントは以下の通りです。普通のWeb APIに接続するときと同様の記述になっていますので、説明は省略します。
Todoの型定義はsrc/mock/handler.ts
でも使用するのでexportしています。
src/App.vue
<template> <!-- API通信中はLoadingを表示 --> <template v-if="apiLoading"> <div>Loading...</div> </template> <template v-else> <ul v-if="todos.length > 0"> <template v-for="todo in todos"> <li>{{ todo.title }}</li> </template> </ul> </template> <form @submit.prevent="handleSubmit"> <input type="text" v-model="inputValue" /> <button type="submit">追加</button> </form> </template> <script setup lang="ts"> import { ref, onMounted } from "vue"; export type Todo = { id: number; title: string; }; const apiLoading = ref<Boolean>(false); const todos = ref<Todo[]>([]); const inputValue = ref<string>(""); onMounted(() => { apiLoading.value = true; fetch("/todos") .then((res) => { if (!res.ok) { throw new Error(res.statusText); } else { return res.json().then((data: Todo[]) => { todos.value = data; }); } }) .catch((error) => { console.log(error); }) .finally(() => { apiLoading.value = false; }); }); const handleSubmit = () => { apiLoading.value = true; fetch("/todos", { method: "POST", body: JSON.stringify({ todo: inputValue.value, }), }) .then((res) => { if (!res.ok) { throw new Error(res.statusText); } else { return res.json().then((data: Todo) => { todos.value = [...todos.value, data]; }); } }) .catch((error) => { console.log(error); }) .finally(() => { apiLoading.value = false; }); }; </script>
5. 終わりに
MSWを使ってモックAPIを作成してみましたが、スムーズに実装することができました。
実際の開発で使用する際には、以下の内容の画面表示を確認できるようにモックAPIを作成するルールにすれば、画面実装時の考慮漏れが防げると思いました。
- 正常時のレスポンス
- 異常時のレスポンス
- レスポンスとして受け取った配列が空の場合
- ネットワークエラーの場合(参考:networkError)
"レスポンスとして受け取った配列が空の場合"の画面表示は、そもそものデザインの段階で考慮されてないといったこともあるので、モックAPIで確認できるようにしておけば、早い段階で気づくことができます。
またネットワークエラーは再現がしづらいので、簡単に確認ができるのは便利だと思いました。
6. 参考
Cypressで特定の要素にドラッグ&ドロップする方法
はじめに
Cyressである要素を別の要素領域内にドラッグ&ドロップする操作を行いたいと思い、そのやり方を調べました。
今回のサンプルアプリ
今回は下記のサイトを元に、Cypressで動かすVueアプリを実装しました。
下図は実装後の画面です。
各Category内の「ProductA」「ProductB」などがドラッグの対象となっており、ドラッグした要素を別のCategory領域にドロップすると、移動させることができます。
今回はCategoryA内の「ProductA」をCategoryBにドラッグ&ドロップで移動させる操作を、Cypressで行うようにします。
実装方法
結論から先に書くと、以下のコードで実現することができます。ProductA、CategoryBのdiv要素にそれぞれdata-cy属性を追加し名称を設定しています。
describe('ドラッグ&ドロップ操作の検証', () => { beforeEach(() => { cy.visit('http://localhost:XXXX/') }) it("ProductAをCategoryAからCategoryBに移動させる", () => { const dataTransfer = new DataTransfer(); // ProductAをドラッグ cy.get("[data-cy='ProductA']") .trigger("dragstart", { dataTransfer }); // CategoryBにドロップ。 cy.get("[data-cy='Category_B']") .trigger("drop", { dataTransfer }); }); });
引っかかったところ
最初に"dataTransfer"をtriggerのオプションに指定せず、実行していました。
// ProductAをドラッグ cy.get("[data-cy='ProductA']") .trigger("dragstart");
するとCypress上で下図のエラーで落ちました。
"effectAllowed"を設定するように指示されています。effetctedAllowはDataTransferのプロパティです。
Cypress内部でDataTransferオブジェクトを使用するようなのでdragstart、dropの各イベントのtriggerのオプションにDataTransferオブジェクトを渡すように設定すると、エラーが起きずドラッグ&ドロップの操作が通りました。
余談
Cypressのドキュメントのrecipesドラッグ&ドロップのE2Eテストのサンプルが記載されています。
このサンプルでは、mousemoveイベントでx,yの移動距離をセットしてドラッグ&ドロップを行う仕様でした。
function movePiece (number, x, y) { cy.get(`.piece-${number}`) .trigger('mousedown', { which: 1 }) .trigger('mousemove', { clientX: x, clientY: y }) .trigger('mouseup', { force: true }) }
今回の用途は特定の要素領域にドロップしたかったので参考にはしなかったですが、スライダー形式のUIであればこれを使えば良さそうです。
参考
Vue3 + TypeScript + MSWの試しメモ(1)
はじめに
最近MSW(MockServiceWorker)の話題をよく見かけるので、自分が関わっているプロダクトで利用できそうか試しに使ってみました。 今回の記事ではMSWを実際に利用するまでの環境構築について記載しています。
MSWとは
名前の通りServiceWorkerを使用しており、クライアントからのAPIリクエストをインターセプトして返してくれるモックAPIです。この図を見ると動作イメージがしやすいかと思います。
ServiceWorker上で返してくれるので、別途モックサーバーを立てることなく、すぐに用意できるところがメリットの1つとして挙げられるようです。実際に簡単にモックAPIを試すことができました。
試したバージョン情報
- Vue:3.2.45
- MSW: 1.0.0
環境構築
Vueの環境構築
Vue3のドキュメントに掲載されている方法で環境構築していきます。
npm init vue@latest
今回はTypeScriptを利用するのでTypeScriptだけ"Yes"にしました。
✔ Project name: … vue-msw ✔ Add TypeScript? … No / Yes ✔ Add JSX Support? … No / Yes ✔ Add Vue Router for Single Page Application development? … No / Yes ✔ Add Pinia for state management? … No / Yes ✔ Add Vitest for Unit Testing? … No / Yes ✔ Add an End-to-End Testing Solution? › No ✔ Add ESLint for code quality? … No / Yes
Vueプロジェクトが生成されたら、起動して画面を確認します。
cd vue-msw npm install npm run dev
MSWの導入
作成したVueプロジェクトに、MSWを導入していきます。
こちらもMSWのインストール手順を元に進めていきます。
npm install msw --save-dev
次にブラウザでのセットアップ手順を元に、MSWを起動できるところまで作業します。
mockServiceWorker.jsの用意
MSWで使用するServiceWorkerの起動スクリプトファイルを用意します。これは以下のコマンドを実行することで自動生成されます。Vueの場合はpublicフォルダ配下に置く必要があるため、下記のコマンドで実行します。
参考:Browser Setup
npx msw init public/ --save
実行が完了すると、public配下にmockServiceWorker.js
が生成されます。またpackage.jsonに下記の内容が追記されます。
"msw": { "workerDirectory": "public" }
この記述があることで、mswのインストールを行うたびに指定されたディレクトリ内のmockServiceWorker.jsの内容を生成、更新してくれます。なのでライブラリのバージョンアップ時もこのjsファイルに何か手を加えたりなどする必要はないようです。 mockServiceWorker.jsの内容が古い場合は、コンソールでエラーを吐いてくれるみたいです。
参考:Why do I see the "Detected outdated Service Worker" error in my console?
browser.tsの用意
src/mock/browser.ts
ファイル内で、ServiceWrokerのインスタンス生成を行う記述を書いておきます。インスタンスの生成にはmswが提供しているsetupWorker
を使用します。今回はVueからモックAPIを利用するのでsetupWorker
を使用しましたが、JestなどNodeから利用する場合はsetupServer
を使うようです。
参考:Configure server
setupWorkerの引数にモックAPIのハンドラー設定を渡します。
import { setupWorker } from 'msw' export const worker = setupWorker();
MSWの起動設定
アプリケーションのコード内から、src/mock/browser.ts
のworkerを呼び出し、mswを起動実行の記述を追加します。
Vue.jsの場合はsrc/main.ts
に書きます。
import { createApp } from 'vue' import App from './App.vue' +if (process.env.NODE_ENV === 'development') { + import('./mocks/browser') + .then(({worker}) => { + worker.start() + }); +} createApp(App).mount('#app')
モックAPIなので本番環境では利用しないため(公式ドキュメントにも非推奨と記載)、開発モードのみMSWが起動するようにします。
起動確認
以上の設定を行い、npm run dev
でVueアプリケーションを立ち上げると、コンソール画面にMSWの起動完了のメッセージが表示されます。
mockServiceWorker.jsの置き場所について
ドキュメント通りだとmockServiceWorker.jsをpublicフォルダに置く必要がありましたが、今の開発環境の都合上別のフォルダに置きたいと思い、対応方法を調べてみました。
以下のようにworker.start()
に参照先のURL設定を追加すると、public配下じゃなくてもmockServiceWorker.jsを起動できるようです。
worker.start({ serviceWorker: { url: '/example/mockServiceWorker.js' } })
URL パスは、サーバーのルート ディレクトリからの相対パスです。
参考:serviceWorker
またドキュメントでは、mockServiceWorker.jsはgitにpushしておくことを推奨しています。
ただnpx msw init
やnpm ci
を実行すれば自動生成されるので、それでも良いとも書かれています。個人的にはライブラリ側で自動生成したコードファイルをgitで管理するのは違和感があるため、後者の運用が良いかと思っています。
参考:Should I commit the worker script to Git?
終わりに
MSWを実際に起動するところまで確認ができました。
次の記事では実際にAPIのハンドラーを書いていき、VueアプリケーションからモックAPIに接続する部分を書いていきたいと思います。
参考
warning: DevTools failed to load SourceMap の対処
React + TypeSctipt + Webpackの環境構築を試しており、webpack-dev-serverを導入した際に、
DevTools failed to load SourceMap
のWarningが出てきたので、動作的には問題ないですけど気になるので、その対処方法をメモしておきます。
解決のきっかけはここの issue のコメントにありました。「webpack.config.js
のdevtoolにinline-source-map
を入れればいけるよ」と。
確かにソースマップが見つからないと言われているので、devtoolに何かしらの値をセットし、ソースマップファイルを生成すればよかったですね。
今回はissueのコメント通りに、devtoolにinline-source-map
を指定しました。これでwarningが消えました。
module.exports = { // 省略 devServer: { contentBase: `${__dirname}/dist`, port: 8080, open: true }, devtool: 'inline-source-map', };
devtoolにfalse
やnone
を設定すると、ソースマップファイルは生成されないので、再びwarningが出ます。
ここに devtoolに設定できる値の一覧が掲載されています。環境に応じて、ソースマップの生成方法を選べるみたいです。
またこちらの記事ではもう少し詳しくdevtoolの設定内容について説明してくれています。
useRefについて
現在、以下のDeep Dive into Redux Toolkit with React - Complete Guideを視聴し、React/Redux環境でのTypeScriptの記述方式やReduxToolkitについて学習しています。
その中で、ReactHooksの一種であるuseRefがプロジェクト内で使用されており、 よく知らないなと思い調べたのでメモを残しておきます。
先にuseRefが何なのか述べると、useRefというのはDOMへアクセスしたいときか、状態を保持したいときに利用するものらしいです。
後者はuseState管理でもできますが、異なる点は状態が更新されても再レンダリングされないところです。
なので、そういったシチュエーションの場合はuseRefを使用した方が良いことがわかりました。
動画内では下記のようにuseRefを使用しています。これはDOMへアクセスしたいときの使い方です。
const editInput = useRef<HTMLInputElement>(null); // ① ...// 省略 useEffect(() => { if (isEditMode) { editInput.current.focus(); // ③ } }, [isEditMode]); ...// 省略 <form onSubmit={handleUpdate}> <label htmlFor="edit-todo">Edit:</label> <input ref={editInput} // ② onChange={handleEditInputChange} value={editTodoInput} /> <button type="submit">Update</button> <button onClick={handleCancelUpdate}>Cancel</button> </form>
①
初期値をnullとしてuseRefを用いた変数を定義します。このとき、この変数自体はinputタグの中に仕込むので、型はHTMLInputElementを設定しておきます。
②
inputタグに①で定義したeditInputを組み込みます。これでeditInputからDOM操作を行えます。
③
isEditMode(編集可能な状態)がtrueであれば、editInput.current.focus()でinputタグにアクセスしフォーカスを当てることができます。
公式ドキュメントの例では、インスタンス変数として状態を保持したいときの使用方法の例も掲載されています。 ここではタイマーIDの値を保存するために使用しています。
React Hook Form IE11での構文エラー対処法
React Hook Formを使用していると、IE11を起動すると構文エラーが発生します。
公式ドキュメントでIE11を使用する場合の対応方法が記載されています。
React Hook Formをimportする際に、下記のように変更するとエラー回避されます。
// 変更前 import { useForm } from "react-hook-form"; // 変更後 import { useForm } from 'react-hook-form/dist/react-hook-form.ie11'
webpackを使用している場合は、aliasにimportの設定を追加しておくことで、
変更前の記述のままで使用することができます。
参考: IE11 support ?
resolve: { alias: { "react-hook-form": "react-hook-form/dist/react-hook-form.ie11" }, },