Nuxt3+Vuetify3+mswでモックアップアプリを作る

ちょっとしたWebアプリのモックアップを作る案件があったため、せっかくなので色々と最新のバージョンを盛り込んで構築したメモ。

* [Nuxt 3](https://v3.nuxtjs.org/) (執筆時点で3.0.0-rc.12)
  * Vue 3やviteを使ってみたかった
* [Vuetify 3](https://next.vuetifyjs.com/en/) (執筆時点で3.0.0-beta.14)
  * とりあえず新しそうだったため
* [Mock Service Worker (msw)](https://mswjs.io/)
  * 通信部分は本物っぽく作り、モックアップではダミーサーバー、後々本物のAPIサーバーに置き換えたい。少し調べた所、mswが良さそうだったので採用。

## プロジェクト生成

最近のフレームワークは一発で空プロジェクトを作れるので楽。Nuxt3はnuxiコマンドを使う。

```shell-session
$ npx nuxi init my-app-name
```

生成したmy-app-nameディレクトリをVSCodeで開いて開発を行う。  
開いたら、ひとまずyarn installを実行する。

```shell-session
$ yarn install
```

なお、VSCodeでフロントエンド(Vue)開発をする際にはこのあたりの拡張機能をインストールしておけば良いはず。

* Volar
* ESLint
* Prettier
* EditorConfig

## テスト起動

以下で開発サーバーを起動。

```shell-session
$ yarn dev
```

ブラウザーで http://localhost:3000/ を開く。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/54a3116f-0393-a943-09c7-ff2623333174.png)

## 警告の修正

アプリのエントリポイントである`app.vue`を開くと、Volarの警告が表示された。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/b91e2ff5-d265-28ce-96c3-5e432006a203.png)

将来のバージョンでは表示されないかもしれないが、ここではひとまず言われた通りに`@types/node`のバージョンを固定しておく。

```shell-session
$ yarn add -D @types/node@18.8.0
```

上記を実行後、VSCodeを再起動すると警告は表示されなくなった。

## Vuetify 3の追加

NuxtにVuetifyを追加する場合、yarnでパッケージを追加した後、NuxtにVuetifyを初期化させるフックとして`plugins/`にファイルを追加する。  
Nuxt3でもこの部分は変わっていないようだが、`plugins/`以下のファイルが自動認識されるため`nuxt.config.ts`への追記が不要になっている。

参考にさせて頂いた記事:  
https://zenn.dev/one_dock/articles/ab6d178741956d

パッケージの追加  
```shell-session
$ yarn add vuetify@next mdi
$ yarn add -D sass
```

plugins/vuetify.ts
```ts
import { createVuetify } from "vuetify"
import * as components from "vuetify/components"
import * as directives from "vuetify/directives"

export default defineNuxtPlugin((nuxtApp) => {
  const vuetify = createVuetify({
    components,
    directives,
  })

  nuxtApp.vueApp.use(vuetify)
})
```

なお、このタイミングで、セミコロンなしのコーディングスタイルが好みだったのでPrettierの設定ファイルを作成した。

.prettierrc
```json
{
  "semi": false
}
```

また、組み込みのCSSとトランスパイル処理を認識させるために以下の設定を追加する。  

nuxt.config.ts
```ts
export default defineNuxtConfig({
    css: ["vuetify/lib/styles/main.sass", "mdi/css/materialdesignicons.min.css"],
    build: {
      transpile: ["vuetify"],
    },
})
```

以上でVuetifyの設定は完了なので、試しに`app.vue`を書き換えてボタンでも置いてみる。

app.vue
```vue
<template>
  <div>Hello, Vuetify</div>
  <v-btn color="blue-grey" prepend-icon="mdi-magnify">SEARCH</v-btn>
</template>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/07792219-88aa-cb09-60b4-dc00e32fa506.png)

## mswの設定

Nuxt3で通信を行う際は[useFetch](https://v3.nuxtjs.org/api/composables/use-fetch)を使うのが一番簡単そうに見えた。

ボタンをクリックした際に通信を行う処理を書いてみる。

app.vue
```vue
<template>
  <div>Hello, Vuetify</div>
  <div>data: {{ jsonData }}</div>
  <v-btn color="blue-grey" prepend-icon="mdi-magnify" @click="onClick"
    >SEARCH</v-btn
  >
</template>

<script setup lang="ts">
const jsonData = useState("jsonData", () => ({}))
const onClick = async () => {
  const { data } = await useFetch("/api/search")
  jsonData.value = data.value
}
</script>
```

この状態でボタンをクリックすると、`/api/search`はSPAのパスとしてルーティングされてしまうため、jsonDataにHTML文字列が格納されて表示される。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/030a998d-ed01-a8b0-de34-1a8671cc97ff.png)

mswの設定を行い、`/api/search`が適当なJSONを返すようにする。  
msw関連のファイルは`mocks/`に配置することとする(任意)。

参考にさせて頂いた記事:  
https://tech.smartshopping.co.jp/nuxt_with_mock_service_worker

ライブラリのインストール
```shell-session
$ yarn add -D msw
```

mocks/api/search.ts  
APIが返す(ダミーの)レスポンスをコードとして書く。
```ts
import { ResponseResolver, MockedRequest, restContext } from "msw"

const get: ResponseResolver<MockedRequest, typeof restContext> = (
  _req,
  res,
  ctx
) => {
  return res(
    ctx.status(200),
    ctx.json([
      // ここがダミーのJSONデータ
      {
        message: "Hello",
      },
    ])
  )
}

export default { get }
```

mocks/handlers.ts  
`mocks/api/`以下のファイルをURLと関連付ける。
```ts
import { rest } from "msw"
import search from "@/mocks/api/search"

export const handlers = [rest.get("/api/search", search.get)]
```

mocks/browser.ts  
ブラウザーで起動するService Workerを定義する。  
(SSRを使う場合はNode.js側のService Workerも定義するらしいが、割愛。)
```ts
import { setupWorker } from "msw"
import { handlers } from "./handlers"

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

plugins/msw.ts  
mswの初期化フック。前述の通り、このファイルを作成するだけでNuxtから認識される。
```ts
import { worker } from "@/mocks/browser"

export default () => {
  worker.start()
}
```

public/mockServiceWorker.js  
mswのサービスワーカーが`GET /mockServiceWorker.js`して使うライブラリ。  
以下のコマンドで作成する。
```shell-session
$ mkdir public
$ npx msw init public/ --save=false
```

nuxt.config.ts  
今回はモックアップとしてビルド成果物をHTTPサーバーに配置する想定なので、SSRを無効化する。
```ts
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
  css: ["vuetify/lib/styles/main.sass", "mdi/css/materialdesignicons.min.css"],
  build: {
    transpile: ["vuetify"],
  },
  ssr: false,  // 追加
})
```

以上でmswが有効化され、画面のボタンクリック時に実際のHTTPリクエストの代わりにソースコードで定義されたダミーデータが取得されたような動作となる。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/52972517-3c05-e9cb-5013-a15e4bfc36b1.png)

また、この状態で`yarn generate`で生成したstaticなビルド成果物をnginx等のWebサーバーに設置し、単独で動作させることが可能である。  
将来的には`msw.ts`の`worker.start()`を`if (process.env.NODE_ENV === 'development') {}`で囲む等で本物のAPIサーバーと併用することもできるようだ。