🦄

【Nuxt3】入門

2023/10/19に公開

Nuxt.js 3

スクリーンショット 2023-10-02 13.30.08.png

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

想定読者

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

環境構築

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

公式ドキュメントに記載の通り、
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

Hello World

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

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

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

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

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

<script setup>の基本的な使い方

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

/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ファイルを作成します。

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

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

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/ディレクトリにあるトップレベルまたはネストされたページを表示するために必要です。

/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

遷移方法

Nuxt3では、<NuxtLink>コンポーネントを使用してリンクします。

https://nuxt.com/docs/api/components/nuxt-link#nuxtlink

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

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

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

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

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>
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

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

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

例)
pages
└ users
  └ [id].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

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

Nuxt3におけるlayoutsディレクトリは、各ページに共通のレイアウトやスタイルを適用するために使用されます🛠
layoutsディレクトリ内のdefault.vueという名前のファイルは、特定のレイアウトが指定されていないすべてのページに適用されるデフォルトのレイアウトとして機能します。
layouts

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

例)

layouts
└ default.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コンポーネントを使用します。

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

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

エラーページの作成

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

スクリーンショット 2023-10-15 17.37.15.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

head情報の設定

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

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の指定をします。

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

ローカルサーバーを立ち上げると、下記のように
設定したtitleタグと、手書き風のfont-familyが適応されています👾
スクリーンショット 2023-10-15 18.05.28.png

非同期通信

useFetch()を使用して非同期データを取得します。
useFetchは、Nuxt 3のComposition APIの一部として提供される非同期データ取得のためのcomposableです。

https://nuxt.com/docs/api/composables/use-fetch#usefetch

非同期通信を検証するにあたり、ダミーデータを無料で提供しているJSONPlaceholderというサイトで、jsonデータを取得します。

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

例)

const { data: users } = await useFetch("https://jsonplaceholder.typicode.com/users");
console.log(users.value);

スクリーンショット 2023-10-17 18.30.29.png

主な返り値

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

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

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

例)

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

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

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

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://nuxt.com/docs/getting-started/assets

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

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

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

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

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

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

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

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

<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(サーバーサイドレンダリング)に対応した共有状態を作成することができます。
公式にある通り、キーと初期値を用いた下記のような構文です。

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

例)

<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

これだけでは、コンポーネント内で状態管理をしているだけですが
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

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

まとめ

Nuxt3の環境構築からグローバル状態管理まで、浅く広くまとめました。
composablesディレクトリでのグローバル状態管理は比較的シンプルですし、
小中規模くらいのアプリでは十分に実用的そうですね!
大規模ならpiniaっていうライブラリが一般的なんですかね..

https://pinia.vuejs.org/

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

スクリーンショット 2023-10-19 16.54.41.png

Discussion