Nuxt.js 3
Vue.jsベースのフレームワークです🚀
Vueアプリケーションの開発を簡素化し、効率化するための多くの機能と最適化を提供します。
前のバージョンであるNuxt2からいくつかの新機能と改善を導入していますが
今回の記事では、その比較については取り扱いません。
vue系の経験があるけど、Nuxt3も入門してみたい!という方に向けて、
環境構築からルーティング、デフォルトレイアウトなど基本的な内容をまとめました。
想定読者
- VueとTypeScriptちょっとわかる
- 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
ローカルサーバーが立ち上がりました。
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 に含まれているらしいです。
<template>
<div>
<h1>Hello World!</h1>
</div>
</template>
マスタッシュ構文{{}}
と、setupの使用
setupを使用してデータとして、表示させてみます。
Composition API ではあらたに setup() が利用できるようになりました。
Component 作成時において props 等の解決が行われた時点で呼ばれます。
その setup() の簡易な書き方(糖衣構文)が<script setup>
です。
Vue.js 3.2 以降では<script setup>
による記述が推奨されています。
<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ファイルを作成します。
<script setup lang="ts"></script>
<template>
<div>
<h1>トップページ</h1>
<hr />
</div>
</template>
<script setup lang="ts"></script>
<template>
<div>
<h1>料金ページ</h1>
<hr />
</div>
</template>
app.vueでは、<NuxtPage />
を記述し、pages配下に作成した、ファイルを呼び出します。
<NuxtPage />
はNuxt3で新しく導入されたコンポーネントで、ページの内容をレンダリングするためのコンポーネントです。
<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/ディレクトリにあるトップレベルまたはネストされたページを表示するために必要です。
<script setup lang="ts"></script>
<template>
<div>
<NuxtPage />
</div>
</template>
この状態で、ローカルサーバーを立ち上げて、http://localhost:3000/
にアクセスすると
トップページが表示され、http://localhost:3000/price
では料金ページが表示されます。
遷移方法
Nuxt3では、<NuxtLink>
コンポーネントを使用してリンクします。
NuxtLink
公式ドキュメントにある通り、下記のような使用例です。
外部リンクの場合は、デフォルトで、rel="noopener noreferrer"
が設定されているようですね✏️
<NuxtLink to="https://nuxtjs.org">
Nuxt website
</NuxtLink>
<!-- 内部ルーティング -->
<NuxtLink to="/about">
About page
</NuxtLink>
index.vue、price.vueにNuxtLink
コンポーネントを追加するとリンクが生成されます。
トップページでは外部リンクも追加してみましたが、デベロッパーツールで見ると
rel属性がついていることがわかりました。
<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>
<script setup lang="ts"></script>
<template>
<div>
<h1>料金ページ</h1>
<hr />
<NuxtLink to="/">トップページへ</NuxtLink>
</div>
</template>
動的ルーティングの生成方法
Nuxt3の動的ルーディングは[id].vue
というファイルを作成することで実装できます。
例)
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
で、動的セグメントを参照しています。
共通レイアウトを適応するlayouts
ディレクトリ
Nuxt3におけるlayoutsディレクトリは、各ページに共通のレイアウトやスタイルを適用するために使用されます。
layoutsディレクトリ内のdefault.vue
という名前のファイルは、特定のレイアウトが指定されていないすべてのページに適用されるデフォルトのレイアウトとして機能します。
layouts
例として、ルートにlayoutsディレクトリを作成します。
例)
layouts
└ default.vue
<script setup lang="ts"></script>
<template>
<div>
<header>Nuxt App</header>
<!-- 下記slotは、NuxtLayoutコンポーネントの子要素に差し代わる -->
<slot />
<footer>© Sato</footer>
</div>
</template>
<style>
header {
background: #000;
color: #fff;
}
footer {
background: orange;
text-align: center;
}
</style>
このレイアウトを呼び出して適応させるには、NuxtLayout
コンポーネントを使用します。
<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
の中身をレンダリングしています。
料金ページへ遷移しても、このdefaultレイアウトが適応されています。
エラーページの作成
現状、存在しないurlにアクセスすると下記、404ページが表示されるかと思います。
ルートにerror.vue
ファイルを設置することで、この404ページを表示をerror.vue
ファイルにできます。
<template>
<div>
<h1>エラー!</h1>
</div>
</template>
ステータスコードによる条件分岐や、エラーメッセージの表示例としては
下記のようなコードになると思います。
<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にアクセスすると下記のような表示が確認できます。
head情報の設定
Nuxt3ではuseHead()
を使用することで、ページやコンポーネントのhead情報を設定できます。
app.vue
ファイルを下記のように修正し、titleとGoogle Fontsの設定をしてみます。
<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の指定をします。
...
<style>
html {
font-family: "Permanent Marker", cursive;
}
</style>
ローカルサーバーを立ち上げると、下記のように
設定したtitleタグと、手書き風のfont-familyが適応されています👾
非同期通信
useFetch()
を使用して非同期データを取得します。
useFetchは、Nuxt 3のComposition APIの一部として提供される非同期データ取得のためのcomposableです。
useFetch
非同期通信を検証するにあたり、ダミーデータを無料で提供しているJSONPlaceholderというサイトで、jsonデータを取得します。
下記のような構文で、データが取得しconsoleできました。
例)
const { data: users } = await useFetch("https://jsonplaceholder.typicode.com/users");
console.log(users.value);
主な返り値
例文では、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[]>
としても良かったのですが、せっかくなので、例として型注釈しました。
<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>
下記のようにレンダリングされました🎉
エラーハンドリングやローディング
data
以外のプロパティも返却されると先述しましたが、error
やpending
も取得して、
エラーハンドリングやローディングを実装してみます。
<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エラーとなることがわかります。
画像表示
Nuxt は 2 つのディレクトリを使用して、スタイルシート、フォント、画像などのアセットを処理します。
public
ディレクトリの内容はサーバーのルートでそのまま提供されます
assets
このディレクトリには、慣例により、ビルド ツール (Vite または webpack) で処理するすべてのアセットが含まれています。
publicディレクトリ配下のものは、画像最適化や拡張子変換などのビルドプロセス処理をされずそのまま公開され、
assetsディレクトリ配下のものはそれらのビルド処理をされるようです。
ルートにpublic
かassets
フォルダを作成しその配下に画像を格納します。
作成したフォルダによってパスの記述が異なります。
例) 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>
状態管理
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>
これだけでは、コンポーネント内で状態管理をしているだけですが
composables
という名前のディレクトリを作成し、その配下のファイルで
useState
を使用することで、グローバルな状態管理ができます。
例として、クリックするごとに引数で取った数値が加算されていくような処理を作成します。
例)ルートにcomposablesディレクトリと、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()
を参照することでグローバル状態管理ができました。
<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>
こちらで、正常に動作することが確認できました!
Reactでいう、useContext
フックみたいなことですかね..
まとめ
Nuxt3の環境構築からグローバル状態管理まで、浅く広くまとめましたがいかがだったでしょう?
細かい深いところは実務で都度学んでいければいいかなと考えています..。
composablesディレクトリでのグローバル状態管理は比較的シンプルですし、
小中規模くらいのアプリでは十分に実用的です!
大規模ならpiniaっていうライブラリが一般的なんですかね..
サイトのパイナップルが可愛かった