Nuxt 3 + Vuetify 3 + TypeScript の事始め（appbarを出すところまで）

# 概要

Nuxt 3 が 2022年4月にＲＣ(Release Candidate)になったことで、今後、Nuxt 3での開発が中心になることも予想される。
また、それにあわせて、Vuetify3(執筆時点ではbeta版）、TypeScriptを組み合わせて、開発の環境設定＋躓きそうなポイントをメモをしておく。


# ドキュメント

- [Vuetify 3 beta](https://next.vuetifyjs.com/en/)
- [Nuxt 3 RC](https://v3.nuxtjs.org/)

# 環境

- macOS Monterey 12.3.1 / Apple M1
- Visual Studio Code / Volar-plugin

- Node.JS

```shell
$ node -v
v16.13.1
```

- npm

```shell
$ npm --v
8.1.2
```

- Vueコマンド

```shell
$ vue --version
@vue/cli 5.0.4
```


# 手順

## プロジェクト作成

まずはプロジェクトを作成してみましょう。`nuxt3-test-app`はプロジェクト名称ですので、お好きなものをつけてください。

```shell
$ npx nuxi init nuxt3-test-app
```

コマンドを実行してみましょう。`nuxi`がインストールされていない場合には、インストールするか聞かれますので、指示に従ってインストールしてください。実行結果が次のように出るはずです。`/path/to/your/working/directory/`はあなたが実行したディレクトリ配下のものが表示されます。

```shell
Nuxt CLI v3.0.0-rc.3                                                                                                                                  
ℹ cloned nuxt/starter#v3 to /path/to/your/working/directory/nuxt3-test-app                                                                                                                                                                                                                          
 ✨ Your stellar Nuxt project is just created! Next steps:
 📁  cd nuxt3-test-app                                                                                                                                
 💿  Install dependencies with npm install or yarn install or pnpm install --shamefully-hoist                                                         
 🚀  Start development server with npm run dev or yarn dev or pnpm run dev                                                                            ```
```

絵文字などと一緒に出ているのはとてもかわいいですよね。
さて、`nuxt 3`で作成されたデフォルトのセットをみてみますしょう。

```shell
$ cd nuxt3-test-app 
$ code .
```

Visual Studio Code が入っているのにもかかわらず、macOS で `code .` でVisual Studio Code が立ち上がらない場合には、環境変数が通っていないことが考えられます。

参考：[M1 MacでVS Codeをターミナルから起動できるようにするまで](https://qiita.com/Teppei_s/items/e5befee55b229695c979)

Visual Stuido Code を起動すると エクスプローラーに以下のように表示されているはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63328/5de1cbcc-71a5-cddc-178f-ea588a2c47ef.png)

どうです？最低限ですよね。

## Nuxtの起動

ターミナルに戻って`npm install`をやってみましょう。


```shell
$ npm install  # working directoryで実行すること
npm WARN deprecated node-pre-gyp@0.13.0: Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future

added 647 packages, and audited 648 packages in 26s

77 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
```

続いてテスト実行してみましょう。

```shell
$ npm run dev
  > Local:    http://localhost:3000/ 
  > Network:  http://192.168.x.x:3000/

ℹ Vite server warmed up in 165ms                                                                                                                      
ℹ Vite client warmed up in 579ms                                                                                                                     
✔ Vite server built in 396ms                                                                                                                          
✔ Nitro built in 185 ms     
```

これが出たら成功です。ちなみに、`Nitro`は、Nuxt 3から搭載された実行エンジンです。詳しくは説明しませんが、[公式サイト](https://v3.nuxtjs.org/guide/concepts/server-engine/)に以下のようなメリットがあると書かれています。

- Cross-platform support for Node.js, Browsers, service-workers and more.
- Serverless support out-of-the-box.
- API routes support.
- Automatic code-splitting and async-loaded chunks.
- Hybrid mode for static + serverless sites.
- Development server with hot module reloading.

さて、ブラウザで確認してみましょう。

```shell
http://localhost:3000/
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63328/15969a33-6284-9724-adf3-7759154862ef.png)

こちらの画像が出れば、実行ができたということが確認できます。

ターミナルでcommand+cで実行を停止してみましょう。


## Vuetify3のインストール

Vuetify3は執筆段階では、beta版です。なので、製品などへの組み込みは注意が必要です。


```shell
$ yarn add vuetify@next sass  
```

パッケージのインストール後、Visual Studio Codeに戻り、以下のpluginを作成してください。Nuxt 3は現段階ではVuetifyのプラグインがないため、自作する必要があるそうです。

```typescript:plugins/vuetify.ts
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import { createVuetify } from 'vuetify'

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

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

```typescript:nuxt.config.ts
import { defineNuxtConfig } from 'nuxt'

// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
    css: ['vuetify/lib/styles/main.sass'],
    build: {
        transpile: ['vuetify'],
    },
})

```


## ページを作ってみる

app.vueをいじってみましょう。以前のNuxt2ではApp.vueでしたが、小文字になったようです。
app.vueはレイアウトファイルの元のような振る舞いをしているようです。
みていきましょう。
デフォルトだとシンプルなものになっています。真ん中のdivタグの中にある`<NuxtWelcome />`を`<NuxtPage />`に変更してみてください。こうすることで、app.vueを使いつつ、具体的な中身はpagesディレクトリの配下を参照することができます。

```vuejs:app.vue(変更後)
<template>
  <div>
    <NuxtPage />
  </div>
</template>
```

pagesディレクトリ配下に2つファイルを作ってみましょう。


```vuejs:pages/aboutus.vue(新規作成)
<template>
    <h1>About us page</h1>
</template>
```


```vuejs:pages/welcome.vue(新規作成)
<template>
    <h1>Welcome to Our Nuxt 3 first page</h1>
</template>
```

これらを配置した上で、ターミナルから`npm run dev`で実行をしてみると、

http://localhost:3000/welcome
http://localhost:3000/aboutus

にアクセスできるようになります。

## 少し複雑なページにしてみる

コンポーネントを作ってみます。
`v-app-bar`をラップした`Header`のコンポーネントを作ってみました。`pages`の以下のページは変更なしでよく、`http://localhost:3000/aboutus`にアクセスできることを確認してみましょう。

```vuejs:components/header/Header.vue(新規作成)
<template>
    <v-app-bar color="primary" id="app-bar">
        <template v-slot:prepend>
            <v-app-bar-nav-icon></v-app-bar-nav-icon>
        </template>

        <v-app-bar-title>Photos</v-app-bar-title>

        <v-menu open-on-hover>
            <template v-slot:activator="{ props }">
                <v-btn v-bind="props" icon="mdi-dots-vertical">
                </v-btn>
            </template>
            <v-list dark dense>
                <v-list-item v-for="menu in items" :key="menu.no" link>
                    <v-list-item-title>{{ menu.title }}</v-list-item-title>
                </v-list-item>
            </v-list>
        </v-menu>
    </v-app-bar>
</template>

<script>
export default {
    data: () => ({
        items: [
            { no: 1, title: 'About us' },
            { no: 2, title: 'プライバシーポリシー' },
            { no: 3, title: '利用規約' },
            { no: 4, title: 'ログアウト' },
        ],
        closeOnContentClick: true,
    }),
}
</script>
```

```vuejs:layouts/custom.vue(新規作成)
<template>
    <v-app>
        <div>
            <header>
                <Header />
            </header>
            <v-main>
                <slot />
            </v-main>
        </div>
    </v-app>
</template>

<script setup>
import Header from "@/componets/header/Header.vue";
</script>
```

```vuejs:app.vue(変更)
<template>
    <link href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css" rel="stylesheet">

    <NuxtLayout name="custom">
        <NuxtPage />
    </NuxtLayout>
</template>
```

```vuejs:pages/aboutus.vue(変更)
<template>
    <h1>About us page</h1>
    This is aboutus page 1<br />
    This is aboutus page 2<br />
    This is aboutus page 3<br />
    This is aboutus page 4<br />
    This is aboutus page 5<br />
    This is aboutus page 6<br />
</template>
```

ちゃんとできるかと思ったのですが、私の環境・Vuetifyのバージョン（beta 2）では、次のようになってしまいました。
ロード後にappbarが画面幅よりも小さいのが出て、しばらくしてから画面幅最大にappbarが引き延ばされるという仕様でした。

![appbar 2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63328/0f876311-fe79-87cb-7ef1-899daed33540.gif)

## 違う方法でappbarっぽいのを実装してみる

`v-toolbar` / `v-btn` を利用して、`v-app-bar`っぽいものを実装してみました。`Vuetify 3.0`が正式版になればなおるであろうことではありますが、今少し困っているので無理矢理対応してみました。
しかし、こちらのソースコードでは、ナビバーの右側のボタンが反応しませんでした。
（左側のハンバーガーメニューはそもそも実装していませんので、反応しません。）

```vuejs:components/header/OriginalHeader.vue(新規作成)
<template>
    <v-toolbar color="primary">
        <v-btn color="white" icon="mdi-menu">
        </v-btn>
        <v-toolbar-title>xxxシステム</v-toolbar-title>

        <v-spacer></v-spacer>

        <v-menu open-on-hover>
            <template v-slot:activator="{ on }">
                <v-btn v-on="on">
                    <v-icon icon="mdi-dots-vertical"></v-icon>
                </v-btn>
            </template>

            <v-list dark dense>
                <v-list-item v-for="item in items" :key="item.no" link>
                    <v-list-item-title>{{ item.title }}</v-list-item-title>
                </v-list-item>
            </v-list>
        </v-menu>
    </v-toolbar>
</template>

<script>
export default {
    data: () => ({
        items: [
            { no: 1, title: 'About us' },
            { no: 2, title: 'プライバシーポリシー' },
            { no: 3, title: '利用規約' },
            { no: 4, title: 'ログアウト' },
        ],
        closeOnContentClick: true,
    }),
}
</script>
```



```vuejs:layouts/custom.vue(変更)
<template>
    <v-app>
        <OriginalHeader />
        <v-main>
            <slot />
        </v-main>
    </v-app>
</template>

<script setup>
import OriginalHeader from "@/components/header/OriginalHeader.vue";
</script>
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63328/4fb82f5e-93d3-f9b0-7521-365ac5d4ee84.png)


## 成功例appbar

v-menuのところの書き方をv-btnを親タグとして書き、それを参照する形でv-menuを内包する書き方だと、hoverがうまく動きました。ただ、メニューが少し切れてしまっております。left offset-xなどを試してみましたが、うまく反応しておりません。

```vuejs:components/header/OriginalHeader.vue(変更)
<template>
    <v-toolbar color="primary">
        <v-btn color="white" icon="mdi-menu">
        </v-btn>
        <v-toolbar-title>xxxシステム</v-toolbar-title>

        <v-spacer></v-spacer>

        <v-btn color="white" dark>
            <v-icon icon="mdi-dots-vertical"></v-icon>
            <v-menu activator="parent" :open-on-hover="true" left offset-x nudge-left="100">
                <v-list>
                    <v-list-item v-for="item in items" :key="item.no" link>
                        <v-list-item-title>{{ item.title }}</v-list-item-title>
                    </v-list-item>
                </v-list>
            </v-menu>
        </v-btn>
    </v-toolbar>
</template>

<script>
export default {
    data: () => ({
        sticky: false,
        items: [
            { no: 1, title: 'About us' },
            { no: 2, title: 'プライバシーポリシー' },
            { no: 3, title: '利用規約' },
            { no: 4, title: 'ログアウト' },
        ],
        closeOnContentClick: true,
    }),
}
</script>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63328/5743fec8-8d5d-7e7c-2b32-b51ebfe95dbb.png)

# おわりに

Vuetify 3.0 beta2で試してみましたが、まだまだ情報が少ないのが現状です。そのうち修正される内容だらけだと思いますが、何かの参考になれば幸いです。（間違い、認識間違いがあったら申し訳ございません。）