【Nuxt3】入門

# Nuxt.js 3

![スクリーンショット 2023-10-02 13.30.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/e4923b3d-5240-7a7b-e833-e70f90065a31.png)


Vue.jsベースのフレームワークです🚀
Vueアプリケーションの開発を簡素化し、効率化するための多くの機能と最適化を提供します。
前のバージョンであるNuxt2からいくつかの新機能と改善を導入していますが
今回の記事では、その比較については取り扱いません。
vue系の経験があるけど、Nuxt3も入門してみたい！という方に向けて、
環境構築からルーティング、デフォルトレイアウトなど基本的な内容をまとめました。

## 想定読者

1. VueとTypeScriptちょっとわかる
1. Nuxtの環境構築から概要、基本的な内容だけ手っ取り早く学びたい


## 環境構築

node、npmがインストールされていることが前提です。
インストールしていない場合は、まずこれらをインストールしてください。

[公式ドキュメント](https://nuxt.com/docs/getting-started/installation)に記載の通り、
`npx nuxi init <project-name>`を実行することで、
`<project-name>`部分で指定された名前の新しいディレクトリが作成され、その中に Nuxt.js プロジェクトの基本的なファイルとディレクトリ構造がセットアップされます。

ドキュメントに従って、コマンドを実行してローカルサーバーを立ち上げます。

`npx nuxi init nuxt3-app`
`cd nuxt3-app`
`npm install`
`npm run dev`

ローカルサーバーが立ち上がりました。

![スクリーンショット 2023-10-02 14.32.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/a96f5cf6-8eb6-0360-2dce-705063fc463a.png)

## Hello World


> Get started
Remove this welcome page by replacing `<NuxtWelcome />` in app.vue with your own code.

ローカルサーバーで立ち上げて表示されるWelcomeページの上記記載に則って、
app.vueにある`<NuxtWelcome />`を書き換えてみます。
生成されたプロジェクトの雛形内に`NuxtWelcome`コンポーネントファイルが見当たらなかったのですが、
[ドキュメント](https://nuxt.com/docs/api/components/nuxt-welcome)によると `NuxtWelcome `コンポーネントは@nuxt/ui に含まれているらしいです。

```vue:/nuxt3-app/app.vue
<template>
  <div>
    <h1>Hello World!</h1>
  </div>
</template>
```

## マスタッシュ構文`{{}}`と、setupの使用

setupを使用してデータとして、表示させてみます。

[`<script setup>`の基本的な使い方](https://zenn.dev/coedo/articles/86bc31acb4ea47)

> Composition API ではあらたに setup() が利用できるようになりました。
Component 作成時において props 等の解決が行われた時点で呼ばれます。
その setup() の簡易な書き方（糖衣構文）が`<script setup>`です。
Vue.js 3.2 以降では `<script setup>` による記述が推奨されています。

```vue:/nuxt3-app/app.vue
<script setup lang="ts">
const message = ref<string>("Hello world!");
</script>
<template>
  <div>
    <h1>{{ message }}</h1>
  </div>
</template>
```

## 複数ページの作成

Nuxt3で複数ページを実装したい場合、ルートディレクトリに`pages`というディレクトリを作成し、
さらにその配下で、vueファイルを作成することで、それらが各ページの役割を果たします。
このpagesディレクトリの構造でルーティングが決まります。
vueでは自分でルーティング設定しますが、Nuxtでは自動で行われます🎉

早速pagesディレクトリを作成し、その配下に、index.vue、price.vueファイルを作成します。

```vue:nuxt3-app/pages/index.vue
<script setup lang="ts"></script>
<template>
  <div>
    <h1>トップページ</h1>
    <hr />
  </div>
</template>
```

```vue:nuxt3-app/pages/price.vue
<script setup lang="ts"></script>
<template>
  <div>
    <h1>料金ページ</h1>
    <hr />
  </div>
</template>
```

app.vueでは、`<NuxtPage />`を記述し、pages配下に作成した、ファイルを呼び出します。
`<NuxtPage />`はNuxt3で新しく導入されたコンポーネントで、ページの内容をレンダリングするためのコンポーネントです。

[NuxtPage](https://nuxt.com/docs/api/components/nuxt-page)

> `<NuxtPage>` is a built-in component that comes with Nuxt. NuxtPage is required to display top-level or nested pages located in the pages/ directory.
> `<NuxtPage>` は Nuxt に付属する組み込みコンポーネントです。NuxtPageは、pages/ディレクトリにあるトップレベルまたはネストされたページを表示するために必要です。

```vue:/nuxt3-app/app.vue
<script setup lang="ts"></script>
<template>
  <div>
    <NuxtPage />
  </div>
</template>
```

この状態で、ローカルサーバーを立ち上げて、`http://localhost:3000/`にアクセスすると
トップページが表示され、`http://localhost:3000/price`では料金ページが表示されます。
![スクリーンショット 2023-10-14 19.00.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/a3a11a7d-4788-5df0-29bc-10a7a9f5ae5b.png)

## 遷移方法

Nuxt3では、`<NuxtLink>`コンポーネントを使用してリンクします。
[NuxtLink](https://nuxt.com/docs/api/components/nuxt-link#nuxtlink)

公式ドキュメントにある通り、下記のような使用例です。
外部リンクの場合は、デフォルトで、`rel="noopener noreferrer"`が設定されているようですね✏️

```vue
<NuxtLink to="https://nuxtjs.org">
Nuxt website
</NuxtLink>

<!-- 内部ルーティング -->
<NuxtLink to="/about">
About page
</NuxtLink>
```

index.vue、price.vueに`NuxtLink`コンポーネントを追加するとリンクが生成されます。
トップページでは外部リンクも追加してみましたが、デベロッパーツールで見ると
rel属性がついていることがわかりました。

```vue:nuxt3-app/pages/index.vue
<script setup lang="ts"></script>
<template>
  <div>
    <h1>トップページ</h1>
    <hr />
    <NuxtLink to="/price">料金ページへ</NuxtLink>
    <br />
    <NuxtLink to="https://www.google.co.jp/">Google</NuxtLink>
  </div>
</template>
```

```vue:nuxt3-app/pages/price.vue
<script setup lang="ts"></script>
<template>
  <div>
    <h1>料金ページ</h1>
    <hr />
    <NuxtLink to="/">トップページへ</NuxtLink>
  </div>
</template>
```

![スクリーンショット 2023-10-15 12.28.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/2e39921c-0e12-fba8-3e96-8916137f56e6.png)

## 動的ルーティングの生成方法

Nuxt3の動的ルーディングは`[id].vue`というファイルを作成することで実装できます。

例)
pages
└ users
　   └ [id].vue

```vue:nuxt3-app/pages/users/[id].vue
<script setup lang="ts">
import { useRoute } from "vue-router";
const route = useRoute();
</script>
<template>
  <div>
    <h1>users/{{ route.params.id }}</h1>
  </div>
</template>
```

`http://localhost:3000/users/1`などのurlにアクセスすると、この[id].vueファイルが表示され、
動的ルーティングが実装できていることを確認できます。
vue-routerライブラリから、`useRoute()`をインポートし、返却値をroute変数に格納、
`route.params.id`で、動的セグメントを参照しています。

![スクリーンショット 2023-10-15 13.03.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/4e6579b1-cda8-0c0f-c01f-7cb9bba7815b.png)

## 共通レイアウトを適応する`layouts`ディレクトリ

Nuxt3におけるlayoutsディレクトリは、各ページに共通のレイアウトやスタイルを適用するために使用されます。
layoutsディレクトリ内の`default.vue`という名前のファイルは、特定のレイアウトが指定されていないすべてのページに適用されるデフォルトのレイアウトとして機能します。
[layouts](https://nuxt.com/docs/guide/directory-structure/layouts)

例として、ルートにlayoutsディレクトリを作成します。

例)

layouts
└ default.vue

```vue:nuxt3-app/layouts/default.vue
<script setup lang="ts"></script>
<template>
  <div>
    <header>Nuxt App</header>
 　　　　　　<!-- 下記slotは、NuxtLayoutコンポーネントの子要素に差し代わる -->
    <slot />
    <footer>&copy; Sato</footer>
  </div>
</template>
<style>
header {
  background: #000;
  color: #fff;
}
footer {
  background: orange;
  text-align: center;
}
</style>
```

このレイアウトを呼び出して適応させるには、`NuxtLayout`コンポーネントを使用します。

```vue:nuxt3-app/app.vue
<script setup lang="ts"></script>
<template>
  <div>
    <NuxtLayout>
      <NuxtPage />
    </NuxtLayout>
  </div>
</template>
```

`NuxtLayout`コンポーネントで、`default.vue`レイアウトを呼び出して、
`<slot />`には`NuxtLayout`コンポーネントの子要素、今回では`<NuxtPage />`が差し込まれます。

ローカルサーバーを立ち上げて、`http://localhost:3000/`にアクセスすると、
`default.vue`レイアウトが適応されていることがわかります。
トップページなので`NuxtPage`コンポーネントは`pages/index.vue`の中身をレンダリングしています。

![スクリーンショット 2023-10-15 17.30.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/b6d34521-45e7-7462-2485-66c2920d1651.png)

料金ページへ遷移しても、このdefaultレイアウトが適応されています。

## エラーページの作成

現状、存在しないurlにアクセスすると下記、404ページが表示されるかと思います。

![スクリーンショット 2023-10-15 17.37.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/c7550654-077a-9105-4fe5-57f0ae1ea319.png)

ルートに`error.vue`ファイルを設置することで、この404ページを表示を`error.vue`ファイルにできます。

```error.vue
<template>
  <div>
    <h1>エラー！</h1>
  </div>
</template>
```

ステータスコードによる条件分岐や、エラーメッセージの表示例としては
下記のようなコードになると思います。

```error.vue
<script setup>
const error = useError();
</script>
<template>
  <div>
    <h1 v-if="error.statusCode === 404">404エラー！</h1>
    <h1 v-else>エラー</h1>
    <p>{{ error.message }}</p>
  </div>
</template>
```

存在しないurlにアクセスすると下記のような表示が確認できます。

![スクリーンショット 2023-10-15 17.49.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/1786a82d-55fc-38af-2bf5-a343320b5239.png)


## head情報の設定

Nuxt3では`useHead()`を使用することで、ページやコンポーネントのhead情報を設定できます。
`app.vue`ファイルを下記のように修正し、titleとGoogle Fontsの設定をしてみます。

```vue:nuxt3-app/app.vue
<script setup lang="ts">
useHead({
  title: "Nuxt App", // titleタグ設定
  link: [
    // GoogleFonts設定
    {
      rel: "stylesheet",
      href: "https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap",
    },
  ],
});
</script>
<template>
  <div>
    <NuxtLayout>
      <NuxtPage />
    </NuxtLayout>
  </div>
</template>
```

忘れずにfont-familyの指定をします。

```vue:/layouts/default.vue
...
<style>
html {
font-family: "Permanent Marker", cursive;
}
</style>
```

ローカルサーバーを立ち上げると、下記のように
設定したtitleタグと、手書き風のfont-familyが適応されています👾
![スクリーンショット 2023-10-15 18.05.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/9a63ce01-5bcf-c438-b37b-391dc43db36a.png)

## 非同期通信

`useFetch()`を使用して非同期データを取得します。
useFetchは、Nuxt 3のComposition APIの一部として提供される非同期データ取得のためのcomposableです。
[useFetch](https://nuxt.com/docs/api/composables/use-fetch#usefetch)

非同期通信を検証するにあたり、ダミーデータを無料で提供している[JSONPlaceholder](https://jsonplaceholder.typicode.com/)というサイトで、jsonデータを取得します。

下記のような構文で、データが取得しconsoleできました。

例)

```js
const { data: users } = await useFetch("https://jsonplaceholder.typicode.com/users");
console.log(users.value);
```
![スクリーンショット 2023-10-17 18.30.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/f7e71d58-661b-0d98-6aa3-8c8d1f471fae.png)

### 主な返り値

例文では、dataプロパティを分割代入し`users`という変数名で保存していますが、他にも下記のようなプロパティが返却されます。

| プロパティ名 | 説明 |
|:-----------|:-----------|
| data       | 非同期関数で取得できたデータ        |
| pending     | データがまだ取得中かどうかを示す真偽値      |
| refresh/execute       | ハンドラ関数によって返されるデータを更新するための関数        |
| error         | データ取得が失敗した場合のエラーオブジェクト          |
| status       | データリクエストの状態を示す文字列       |

公式ドキュメント記載の通り、様々なオプションもとれて、`useFetch()`の動作を細かくカスタマイズできます。

例)

```js
const route = useRoute()
const { data, pending, error, refresh } = await useFetch(`https://api.nuxtjs.dev/mountains/${route.params.slug}`, {
  pick: ['title']
})
```

`userFetch()`を使用して、データの取得と表示をしてみます。
下記のように、`app.vue`を修正します。
本質ではない返却値の型注釈で長くなっています..
`useFetch<any[]>`としても良かったのですが、せっかくなので、例として型注釈しました。

```app.vue
<script setup lang="ts">
// レスポンスの型注釈
type Geo = {
  lat: string;
  lng: string;
};

type Address = {
  street: string;
  suite: string;
  city: string;
  zipcode: string;
  geo: Geo;
};

type Company = {
  name: string;
  catchPhrase: string;
  bs: string;
};

type User = {
  id: number;
  name: string;
  username: string;
  email: string;
  address: Address;
  phone: string;
  website: string;
  company: Company;
};

const { data: users } = await useFetch<User[]>("https://jsonplaceholder.typicode.com/users");
useHead({
  title: "Nuxt App", // titleタグ設定
  link: [
    // GoogleFonts設定
    {
      rel: "stylesheet",
      href: "https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap",
    },
  ],
});
</script>
<template>
  <div>
    <NuxtLayout>
      <NuxtPage />
      <ul>
        <li v-for="user in users" :key="user.id">{{ user.id }} : {{ user.name }}</li>
      </ul>
    </NuxtLayout>
  </div>
</template>
```

下記のようにレンダリングされました🎉

![スクリーンショット 2023-10-17 18.55.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/94edd2af-618e-90ff-e086-194429e0922f.png)

### エラーハンドリングやローディング

`data`以外のプロパティも返却されると先述しましたが、`error`や`pending`も取得して、
エラーハンドリングやローディングを実装してみます。


```app.vue
<script setup lang="ts">
...

// error,pendingも取得
const { data: users, error, pending } = await useFetch<User[]>("https://jsonplaceholder.typicode.com/usersss"); // 存在しないurl

...
</script>
<template>
  <div>
    <NuxtLayout>
      <NuxtPage />
      <p v-if="pending">データ読み込み中...</p>
      <p v-else-if="error">エラーが発生しました: {{ error?.message }}</p>
      <ul>
        <li v-for="user in users" :key="user.id">{{ user.id }} : {{ user.name }}</li>
      </ul>
    </NuxtLayout>
  </div>
</template>
```

上記のようなコードで、存在しないurlをfetchした場合、404エラーとなることがわかります。

![スクリーンショット 2023-10-17 19.19.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/32d85432-194a-84bc-44f0-802a98480c42.png)

## 画像表示


https://nuxt.com/docs/getting-started/assets


> Nuxt は 2 つのディレクトリを使用して、スタイルシート、フォント、画像などのアセットを処理します。

**public**
ディレクトリの内容はサーバーのルートでそのまま提供されます

**assets**
このディレクトリには、慣例により、ビルド ツール (Vite または webpack) で処理するすべてのアセットが含まれています。

publicディレクトリ配下のものは、画像最適化や拡張子変換などのビルドプロセス処理をされずそのまま公開され、
assetsディレクトリ配下のものはそれらのビルド処理をされるようです。

ルートに`public`か`assets`フォルダを作成しその配下に画像を格納します。
作成したフォルダによってパスの記述が異なります。

例) publicディレクトリ配下の画像表示

```vue
<template>
  <img src="/img/nuxt.png" alt="Discover Nuxt 3" />
</template>
```

例) assetsディレクトリ配下の画像表示

```vue
<template>
  <img src="~/assets/img/nuxt.png" alt="Discover Nuxt 3" />
</template>
```

## 状態管理


https://nuxt.com/docs/api/composables/use-state

Nuxt2では、グローバル状態管理ではVuexを使うのが一般的でしたが、
Nuxt3では、`useState`というComposableが提供されるようになりました。
これを使用することで、リアクティブでSSR（サーバーサイドレンダリング）に対応した共有状態を作成することができます。
公式にある通り、キーと初期値を用いた下記のような構文です。

```js
const 変数名 = useState("キー", () => 初期値);
```

例)

```vue
<script setup lang="ts">
const counter = useState("counter", () => 0);
</script>
<template>
  <div>
    Counter: {{ counter }}
    <button @click="counter++">+</button>
    <button @click="counter--">-</button>
  </div>
</template>
```

![スクリーンショット 2023-10-19 15.50.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/54ac811f-ca34-c833-cc6f-436b6721ad3d.png)

これだけでは、コンポーネント内で状態管理をしているだけですが
`composables`という名前のディレクトリを作成し、その配下のファイルで
`useState`を使用することで、グローバルな状態管理ができます。

例として、クリックするごとに引数で取った数値が加算されていくような処理を作成します。

例)ルートにcomposablesディレクトリと、tsファイル作成

composables
└ index.ts

```composables/index.ts
export const useCounter = () => {
  // state定義
  const counter: Ref<number> = useState("counter", () => 0);

  // state更新関数
  const updateCounter = (counter: Ref<number>) => (value: number) => {
    counter.value += value;
  };

  return {
    counter: readonly(counter), // readonlyでラップして、外部からの直接変更を防ぐ
    updateCounter: updateCounter(counter),
  };
};

```

app.vueで下記のように作成した`useCounter()`を参照することでグローバル状態管理ができました。

```app.vue
<script setup lang="ts">
// composablesで定義したuseCounter()からstateと、state更新用関数を分割代入で取得
const { counter, updateCounter } = useCounter();
</script>
<template>
  <div>
    Counter: {{ counter }}
    <button @click="updateCounter(5)">+5</button>
  </div>
</template>
```

こちらで、正常に動作することが確認できました！

![スクリーンショット 2023-10-19 16.22.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/684f8ab5-ec77-543e-7e7d-0b077f412b54.png)

Reactでいう、`useContext`フックみたいなことですかね..

https://qiita.com/__knm__/items/69a4c6bf916a2d1ca9dd

## まとめ

Nuxt3の環境構築からグローバル状態管理まで、浅く広くまとめましたがいかがだったでしょう？
細かい深いところは実務で都度学んでいければいいかなと考えています..。
composablesディレクトリでのグローバル状態管理は比較的シンプルですし、
小中規模くらいのアプリでは十分に実用的です！
大規模ならpiniaっていうライブラリが一般的なんですかね..

https://pinia.vuejs.org/

サイトのパイナップルが可愛かった

![スクリーンショット 2023-10-19 16.54.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/864de6e5-0d96-02cc-baf8-7b1ce8ecfc6b.png)