本文書ではVue.jsのフルスタックフレームワークであるNuxt 3を使ってアプリケーションの開発を効率的に行いたいという人を対象に Nuxt 3についての解説をできるかぎり簡単なコードを利用しながら行っています。Vue.jsのComposition APIのコードが理解できることを前提に記述しているのでVue.jsを使って記述したコードの詳細について解説は行なっていませんが、本文書を読み終えるとNuxt 3の全体像と基礎的な理解を深めることができます。

Nuxtを利用したファイルのアップロードについては別の記事で公開しています。

目次

Nuxt 3とは

NuxtはVue.jsをベースに開発されたフルスタックのWEBフレームワークです。
Vue.jsはUI(User Interface)を担当しますがNuxtではVue.jsが持つUIに関わる部分だけではなくSSR(サーバサイドレンダリング)を含めたレンダリング機能やMetaタグ、ルーティング、エラーハンドリングなど本格的なアプリケーションの構築に必須となるさまざまな機能を事前に組み込むことでアプリケーション開発を効率的に行うことができます。例えばルーティングであればVue-Routerが事前に組み込まれているだけではなく決められてたディレクトリにファイルを作成するだけで自動でルーティングを設定してくれます(ファイルベースルーティング)。またコンポーネントを利用する際にimport文を利用しなくても自動でimportを行うAuto-Import機能も持っています。TypeScriptもサポートしてデフォルトから利用することができます。これらの機能は一例ですがこの他にもさまざまな機能が含まているので本文書で確認を行っていきます。

Nuxt 3プロジェクトの作成

Nuxt 3 プロジェクトの作成を npx nuxi@latest initコマンドを利用して行います。nuxt3-app はプロジェクト名で任意の名前をつけることができるので好きな名前を設定してください。


 % npx nuxi@latest init nuxt3-app

√ Which package manager would you like to use?
npm
o Installing dependencies...                                                                                  10:12:18

> postinstall
> nuxt prepare

√ Types generated in .nuxt                                                                                    10:13:13

added 739 packages, and audited 741 packages in 55s

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

found 0 vulnerabilities
√ Installation completed.                                                                                     10:13:14

√ Initialize git repository?
Yes
i Initializing git repository...                                                                              10:13:17

Initialized empty Git repository in C:/Users/Reffect/Desktop/nuxt3-app/.git/
                                                                                                              10:13:17
✨ Nuxt project has been created with the v3 template. Next steps:
 › cd nuxt3-app                                                                                               10:13:17
 › Start development server with npm run dev

コマンドを完了するとnuxt3-appフォルダが作成されるのでnuxt3-appに移動します。


 % cd nuxt3-app
 % npm install

package.json ファイルを確認していデフォルトでインストールされるライブラリとスクリプトの確認を行います。nuxt のバージョンが 3.8.2 であることが確認できます。また3.8からはNuxt DevToolsがインストールされます。


{
  "name": "nuxt-app",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "devDependencies": {
    "@nuxt/devtools": "latest",
    "nuxt": "^3.8.2",
    "vue": "^3.3.12",
    "vue-router": "^4.2.5"
  }
}

開発サーバを起動するためにnpm run devコマンドを実行します。


 % npm run dev
> nuxt dev

Nuxt 3.8.2 with Nitro 2.8.1                                                              10:16:09
                                                                                         10:16:10
  ➜ Local:    http://localhost:3000/
  ➜ Network:  use --host to expose

  ➜ DevTools: press Shift + Alt + D in the browser (v1.0.6)                              10:16:15

ℹ Vite server warmed up in 3638ms                                                       10:16:23
ℹ Vite client warmed up in 4517ms                                                       10:16:23
✔ Nitro built in 2160 ms  

ブラウザからhttp://localhost:3000/にアクセスするとNuxt 3のデフォルトページが表示されます。

Nuxt 3の初期画面
Nuxt 3の初期画面

開発サーバの起動と同時に自動でブラウザを起動し初期画面を表示したい場合にはオプション– -oを利用することができます。


 % npm run dev -- -o

開発サーバが起動して初期画面が表示されるまでローディング画面で”Nuxt のログがブラウザの中央に表示され、その後初期画面が表示されます。

デフォルトディレクトリ構成

インストール直後のディレクトリ構成を確認してみましょう。エディターは VSCode を利用して確認しています。左側のディレクトリのリストを確認すると node_modules 以外にpublic, serverディレクトリが存在しますが非常にシンプルな構成であることがわかります。ブラウザ上に表示される内容に関わるファイルは app.vue ファイルのみだということがわかります。

デフォルトのディレクトリ構成
デフォルトのディレクトリ構成

app.vueファイル

app.vueファイルの中身を確認するとNuxtWelcomeタグが設定されていることがわかりますがimport文もないためどのファイルがNuxtWelecomeコンポーネントに対応するのかはわかりません。


<template>
  <div>
    <NuxtWelcome />
  </div>
</template>

NuxtWelcomeコンポーネントについての説明はドキュメントに記載されているので確認します。ドキュメントによるとNuxtWelcomeコンポーネントは@nuxt/assetsに含まれていることがわかります。

NuxtWelcomeコンポーネントについて
NuxtWelcomeコンポーネントについて

@nuxt/assetsライブラリの中身を見るとhttps://github.com/nuxt/assets/blob/main/packages/templates/templates/welcome/index.htmlのindex.htmlファイルで初期画面に表示されていることが確認できるので中身が気になる人はぜひ確認してみてください。

初期画面に表示されている内容を変更するためには初期画面に表示されていたメッセージ”Remove this welcome page by replacing in app.vue with your own code.”を参考にします。

初期画面のメッセージ
初期画面のメッセージ

app.vueファイルからNuxtWelcomeコンポーネントを削除して”Hello World”を記述します。


<template>
  <h1>Hello World</h1>
</template>

ブラウザで確認すると”Hello World”が確認できます。

Hello World
Hello World

app.vueファイルはNuxt 3のメインコンポーネントです。複数のページを作成する場合は pages ディレクトリを利用しますが pages ディレクトリはオプションなので必ず利用する必要はありません。ランディングページのみ、ルーティングが必要ないアプリケーションの場合は、app.vue ファイルを利用するだけでアプリケーションを構築することができます。app.vue ファイルのみ利用した場合にはルーティングの vue-router をバンドルする必要がないためビルド後のバンドルファイルのサイズを減らすことができます。バンドルサイズが小さくなることでクライアントが受け取るデータ量も減るためページの表示速度の高速化につながります。

app.vue以外のファイル

app.vue ファイル以外に nuxt.config.ts, tsconfig.json, README.md, package.json ファイルなどがあります。nuxt.config.ts は Nuxt に関する設定を行うことができるファイルで本文書でも nuxt.config.ts ファイルを利用して設定を行います。拡張子が ts となっている通りデフォルトから TypeScript に対応しています。tsconfig.json は TypeScript の一般的な設定ファイルです。README.md にインストールコマンドなどの説明が記述されています。

ディレクトリ構成

Nuxt 3ではプロジェクト作成時に node_modules, public, server 以外のディレクトリは存在しませんがそれぞれ異なる役割を持っているディレクトリを追加していくことでアプリケーションを構築していきます。ディレクトリ名はNuxt 3の中で事前に決められておりコンポーネントを利用する場合は componentsディレクトリを作成します。componentsディレクトリの下にコンポーネントファイルを保存することで Auto Imports 機能により import 文を利用しなくても自動でコンポーネントの import が行われます。そのためディレクトリ名や作成したディレクトリがどのような役割を持っているのかをディレクトリ毎に理解しておく必要があります。

Auto Imports機能を利用せず、import文を利用してコンポーネントをインポートすることは可能です。

pagesディレクトリ

app.vue ファイルだけでアプリケーションを構築することができるということは説明しましたがNuxtを利用する開発者は通常複数ページで構成されたアプリケーションを構築します。ページを追加したい場合にはpagesディレクトリを利用します。Nuxtではファイルベースルーティングを採用しているので pages ディレクトリの中にファイルを作成するだけで自動でルーティングが設定されます。

pagesディレクトリはデフォルトでは存在しないのでpagesディレクトリを作成してその下にindex.vueファイルを作成して下記のコードを記述してください。


<template>
  <h1>Main Page</h1>
</template>
pagesディレクトリとページファイルの作成はnpx nuxi add page indexコマンドを利用しても作成することができます。実行するとpagesフォルダとその中にindex.vueファイルが作成されます。

pages/index.vueファイルを作成しただけでブラウザ上の表示が変わるわけではなりません。

pages/index.vueファイルの内容を表示させるためにはapp.vueファイルにNuxtPageコンポーネントを追加する必要があります。


<template>
  <div>
    <NuxtPage />
  </div>
</template>

NuxtPage タグを追加すると NextPage タグを追加した箇所に pages/index.vue ファイルのコンテンツが表示されます。説明通りに設定したにも関わらずコンテンツが表示されず画面が真っ白の場合は npm run dev コマンドを再実行してください。

pages/index.vueファイルの中身が表示
pages/index.vueファイルの中身が表示

さらにpagesディレクトリにabout.vueファイルを作成して下記の内容を記述します。


<template>
  <h1>About Page</h1>
</template>

about.vueファイルを作成後にブラウザのURLにhttp://localhost:3000/aboutを直接入力すると”About Page”が表示されます。自動でルーティングが設定されていることがわかります。もしNuxtに自動ルーティング機能が備わっていない場合はルーティングファイルを利用して/aboutにアクセスがあった場合に表示させるコンポーネントを指定する必要があります。

Aboutページの表示
Aboutページの表示

pagesディレクトリに存在しないURLにアクセスした場合は404ページが表示されます。

Not Foundページ
Not Foundページ

pages ディレクトリにファイルを作成するとルーティングが自動で設定されること、コンテンツを表示するためにはメインファイルである app.vue ファイルにNuxtPageタグが必要であることがわかりました。

手動でのルーティングを行なった経験がない人は下記の記事が参考になります。自動でルーティングが設定されない場合にはどのようにルーティングを設定するかを理解することができます。

npx nuxi add page indexコマンド

npx nuxi add page indexコマンドを実行した場合の動作も確認しておきます。


 %  npx nuxi add page index
ℹ Creating directory C:/Users/Reffect/Desktop/nuxt3-app/pages               10:57:21   
ℹ This enables vue-router functionality!                                 10:57:21   
[10:57:21] ℹ 🪄 Generated a new page in C:/Users/Reffect/Desktop/nuxt3-app/pages/index.vue

pagesディレクトリが存在しない場合は自動で作成が行われて作成されるindex.vueファイルには以下のテンプレートコードが記述されています。


<script lang="ts" setup></script>

<template>
  <div>
    Page: foo
  </div>
</template>

<style scoped></style>

layoutsディレクトリ

index.vue ファイル、about.vue ファイルのページ間を移動できるナビゲーションを追加したい場合に両方のファイルに同じ内容を追加することは非効率です。レイアウトファイルを作成することでページで共通する内容を1つのファイルにまとめることができ、レイアウトファイルを共有するすべてのページに適用することができます。レイアウトファイルにはヘッダーやフッター、サイドバーなど複数のページで共有できる内容を記述します。

レイアウトを利用したい場合には layouts ディレクトリを作成して default.vue ファイルを作成してください。<slot />は必須です。


<template>
  <div>
    <nav>ここにナビゲーションバーを入れる</nav>
    <slot />
  </div>
</template>

layoutsフォルダにdefault.vueファイルを作成してだけでは何も変化はありません。app.vueファイルにNuxtLayoutタグを追加しNuxtPageタグをラップします。


<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

app.vueファイルにNuxtLayoutタグを追加することで自動でlayoutsディレクトリに存在するdefault.vueファイルの内容が適用されます。

default.vueの内容が表示
default.vueの内容が表示

http://localhost:3000/だけではなくhttp://localhost:3000/aboutページにアクセスしてもdefault.vueファイルの内容が適用されていることが確認できます。

aboutページの確認
aboutページの確認

もしレイアウトをある特定のページで適用したくない場合にはdefinePageMeta関数で制御することができます。pages/about.vueファイルにレイアウトを適用したくない場合はdefinePageMega関数の引数にlayoutプロパティを持つオブジェクトを追加しlayoutの値をfalseに設定します。設定するとabout.vueファイルのみレイアウトの適用が行われなくなります。index.vueファイルについてはdefault.vueのレイアウトが適用されたままになります。


<template>
  <h1>About Page</h1>
</template>
<script setup>
definePageMeta({
  layout: false,
});
</script>

カスタムレイアウトファイル

layoutsフォルダにdefault.vueファイルを作成することで自動でレイアウトとして適用されましたがページ毎に異なるレイアウトを適用したい場合の方法を確認します。

layoutsフォルダにcustom.vueファイルを作成します。defaultとは異なり任意の名前をつけることができます。


<template>
  <div>
    <nav>カスタムレイアウト</nav>
    <slot />
  </div>
</template>

追加したcustome.vueファイルのレイアウトを利用したい場合もdefinePageMeta関数を利用します。引数のオブジェクトのlayoutプロパティの値にlayoutsディレクトリにあるファイル名を指定します。ここでは作成したcustomを指定します。


<template>
  <h1>About Page</h1>
</template>
<script setup>
definePageMeta({
  layout: 'custom',
});
</script>

aboutページにはcustom.vueファイルのレイアウトが適用されます。

カスタムレイアウトを適用
カスタムレイアウトを適用

全体にデフォルトで設定するレイアウトファイルをdefault.vueからcustom.vueファイルに変更したい場合にはNuxtLayoutタグのname propsを利用することができます。


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

“/”にアクセスするとlayouts/default.vueファイルのレイアウトではなくlayouts/customのレイアウトが適用されます。

v-bind を name Props を利用することで動的にレイアウトを変更することもできます。admin が true の場合は custom.vue ファイル、admin が false の場合は default.vue ファイルが適用されます。


<template>
  <NuxtLayout :name="layout">
    <NuxtPage />
  </NuxtLayout>
</template>
<script setup>
const admin = true;
const layout = admin ? 'custom' : 'default';
</script>

NuxtLayoutコンポーネント

レイアウトファイルの設定についてはabout.vueファイルに直接NuxtLayoutコンポーネントを利用することもできます。name propsにレイアウトファイル名を設定することができますがここではname propsを設定していないのでデフォルトのレイアウトが適用されます。


<template>
  <div>
    <NuxtLayout>
      <h1>About Page</h1>
    </NuxtLayout>
  </div>
</template>
pageファイルでNuxtLayoutを利用する場合はNuxtLayoutタグがルートエレメントにならないように設定することが推奨されています。pages/about.vueファイルのルートエレメントはdivにしています。

app.vueファイルにもNuxtLayoutタグが利用されていることを確認しておきます。


<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

ブラウザで/aboutに確認すると2回デフォルトのレイアウトが適用されることになります。

ページファイルへのレイアウトファイルの適用
ページファイルへのレイアウトファイルの適用

about.vueのNuxtLayoutにname propsを追加しcustomレイアウトの設定のみ設定後、customレイアウトのみ反映させたい場合はdefinePageMeta関数でlayoutをfalseに設定します。app.vueファイルで適用されているdefaultレイアウトは適用されず、customレイアウトのみ適用されます。


<template>
  <div>
    <NuxtLayout name="custom">
      <h1>About Page</h1>
    </NuxtLayout>
  </div>
</template>
<script setup>
definePageMeta({
  layout: false,
});
</script>

app.vueファイルの削除

app.vueファイルはNuxt 3のメインコンポーネントだと説明しましたがapp.vueは必須ではないので削除することができます。削除を行った時の動作を確認します。

pagesディレクトリのindex.vueファイルの内容はそのままの状態です。


<template>
  <h1>Main Page</h1>
</template>

layouts ディレクトリに default.vue ファイルが存在した状態でブラウザから”/“にアクセスすると app.vue は存在していませんが default.vue のレイアウトが適用されます。

レイアウトを適用したくない場合には definePageMeta 関数の layout を false に設定することで適用されません。


<template>
  <h1>Main Page</h1>
</template>
<script setup>
definePageMeta({
  layout: false,
});
</script>

カスタムレイアウトcustom.vueファイルのレイアウトを設定したい場合はlayoutに”custom”を設定することが適用されます。


<template>
  <h1>Main Page</h1>
</template>
<script setup>
definePageMeta({
  layout: 'custom',
});
</script>

NuxtLayoutを利用してレイアウトを設定することができますがその場合はdefinePageMeta関数でデフォルトのレイアウトが適用されないように設定しておく必要があります。


<template>
  <div>
    <NuxtLayout name="custom">
      <h1>Main Page</h1>
    </NuxtLayout>
  </div>
</template>
<script setup>
definePageMeta({
  layout: false,
});
</script>

app.vue ファイルを必須ではないので削除できること、app.vue ファイルが存在しない場合には自動でデフォルトのレイアウトが適用されることがわかりました。

動的にLayoutの変更

ページに適用するレイアウトを動的に変更したい場合に setPageLayout 関数を利用することもできます。

最初は definePagaMeta 関数で layout を false に設定しているのでデフォルトのレイアウトが設定されていない状態です。画面上に表示される”Update layout”ボタンをクリックすると custom レイアウトが適用されます。レイアウトの適用に setPageLayout 関数を利用しています。


<template>
  <div>
    <button @click="enableCustomLayout">Update layout</button>
  </div>
</template>
<script setup>
function enableCustomLayout() {
  setPageLayout('custom');
}
definePageMeta({
  layout: false,
});
</script>

useRoute関数を利用してroute.meta.layoutの値を設定することでも動的に変更することは可能です。


<template>
  <div>
    <button @click="enableCustomLayout">Update layout</button>
  </div>
</template>
<script setup>
const route = useRoute();
const enableCustomLayout = () => {
  route.meta.layout = 'custom';
};
definePageMeta({
  layout: false,
});
</script>

definePageMeta関数を利用しない場合はデフォルトのlayoutが適用されておりボタンをクリックするとcustomのレイアウトが適用されます。

名前付きslotの設定

app.vue ファイルを削除し layouts ディレクトリに default.vue ファイルが存在し index.vue ファイルにデフォルトのレイアウトが適用された状態で slot の設定の動作確認を行います。


名前付き slot を利用するために app.vue ファイルを削除しないといけないわけではありません。

<template>
  <div>
    <nav>こにナビゲーションバーを入れる</nav>
    <slot />
  </div>
</template>

<template>
  <h1>Main Page</h1>
</template>

default.vueファイルで名前付きslotを追加します。slotタグにname propsを追加し任意の名前をつけています。


<template>
  <div>
    <slot name="header" />
    <nav>こにナビゲーションバーを入れる</nav>
    <slot />
  </div>
</template>

index.vueファイルではdefinePageMeta関数でlayoutプロパティをfalseに設定しNuxtLayoutコンポーネントを追加します。nameにはdefaultを設定します。templateタグに#headerを設定しタグの中に名前付きslotを設定した場所に表示させたい内容を記述します。


<template>
  <div>
    <NuxtLayout name="default">
      <template #header>ヘッダー</template>
      <h1>Main Page</h1>
    </NuxtLayout>
  </div>
</template>
<script setup>
definePageMeta({
  layout: false,
});
</script>

templateタグで設定した”ヘッダー”が表示されることがわかります。

名前付きSlotの設定方法
名前付きSlotの設定方法
NuxtLayoutに明示的にname propsでdefaultを設定しない場合はdefinePageMeta関数によりdefaultのレイアウトが適用されないのでnameの設定が必須になります。またNuxtLayoutタグがない場合は、エラーになります。

componentsディレクトリ

app.vue ファイルは削除して layouts ディレクトリに default.vue ファイルがある状態で動作確認を行っています。


<template>
  <h1>Main Page</h1>
</template>;

再利用可能なコンポーネントファイルを保存するためにcomponentsディレクトリを作成します。

作成したcomponentsディレクトリにNavbar.vueファイルを作成します。Navbar.vueファイルはdefault.vueファイルでナビテーションバーを設定するために利用します。


<template>
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
  </nav>
</template>

作成した Navbar コンポーネントを layouts の default.vue ファイルに追加します。Vue.js では別ファイルに記述したコンポーネントを利用する場合には import を行う必要があります。Nuxt 3 では components ディレクトリに保存したコンポーネントファイルは自動で import されるため import 処理を行う必要がありません。


<template>
  <div>
    <Navbar />
    <slot />
  </div>
</template>

設定後は上部にリンクが表示されます。Home、Aboutそれぞれにaタグでリンクが貼られているのでクリックするとページの移動を行うことができます。

NavBarコンポーネントの表示
NavBarコンポーネントの表示
aタグは後程NuxtLinkタグに変更します。

Component Name

components ディレクトリに新たに nav ディレクトリを作成し先ほど作成した Navbar.vue ファイルと同じファイルを nav ディレクトリに作成します。import を利用した場合にはコンポーネントファイルのパスを設定するため同じファイル名でも問題ありません。Nuxt 3 では自動で import を行ってくれる場合にはどのようにファイルを区別するのでしょう。nuxt.config.ts ファイルで設定を行う pathPrefix の値によって動作が異なります。

pathPrefix を false に設定した場合の動作確認を行います。


export default defineNuxtConfig({
  devtools: { enabled: true },
  components: [
    {
      path: '~/components',
      pathPrefix: false,
    },
  ],
});

nav ディレクトリの下に作成した Navbar.vue ファイルには以下のコードを記述します。


<template>
  <nav>
    <a href="/">Home2</a>
    <a href="/about">About2</a>
  </nav>
</template>

layouts/default.vueファイルの中でNavbarタグを利用します。


<template>
  <div>
    <Navbar />
    <slot />
  </div>
</template>

ブラウザで確認するとブラウザ上には components ディレクトリ直下にある Navbar コンポーネントのみ表示されターミナルには WARNING が表示されます。2 つのファイルのコンポーネント名が重複していることを教えてくれます。


WARN  Two component files resolving to the same name Navbar:

 - /Users/mac/Desktop/nuxt3-app/components/nav/Navbar.vue
 - /Users/mac/Desktop/nuxt3-app/components/Navbar.vue

components ディレクトリ直下の Navbar.vue ファイルの名前を一時的に Navbar2.vue に変更します。

変更後、ブラウザで確認するとnav/Navbarコンポーネントファイルの内容が表示されます。

ディレクトリ名を利用してAuto Import
Auto Imports

pathPrefix の値が false の場合は Navbar ファイルが一つであれば nav ディレクトリに保存されていても Navbar タグで Auto imports されることがわかります。

nuxt.config.ts ファイルから pathPrefix の値を削除すると nav ディレクトリに保存した Navbar.vue ファイルを利用することができなくなり下記の WARNING が表示されます。


[Vue warn]: Failed to resolve component: Navbar
If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.

ディレクトリがネストされているコンポーネントファイルはディレクトリの名前とファイルの名前を利用することで Auto Imports することができます。nav ディレクトリにある Navbar.vue ファイルを利用したい場合には NavNavbar タグを利用します。ディレクトリ名を先頭に追加しています。


<template>
  <div>
    <NavNavbar />
    <slot />
  </div>
</template>

ブラウザで確認すると nav ディレクトリにある Navbar.vue ファイルの内容が表示されます。同じファイル名が components ディレクトリに存在する場合はディレクトリ名を利用することで識別できることがわかりました。

ディレクトリ名を利用してAuto Import
ディレクトリ名を利用してAuto Import

ファイル名が Navbar.vue ではなく NavBar.vue ファイルとした場合は NavNavBar としても nav ディレクトリ NavBar を Auto Imports してくれません。NavNavBar では、nav/nav/Bar.vue ファイルを import していることになります。

Auto Imports を利用しない場合は通常の import を利用することができます。


<template>
  <div>
    <Navbar />
    <slot />
  </div>
</template>
<script setup>
import Navbar from '@/components/nav/Navbar';
</script>

ComponentsのLazy Loading(遅延読み込み)

Nuxt のコンポーネントは Lazy Loading という機能を持っているため必要な時だけ Lazy Loading を設定したコンポーネントに対応する JavaScript ファイルをダウンロードすることができます。Lazy Loading の機能を利用することで ページアクセス時にダウンロードするJavaScript のサイズを小さくすることができます。Lazy LoadingはDynamic Importsとも呼ばれます。

コンポーネントの Lazy Loading を利用した時としない時の動作をネットワークタブを見ながら確認します。

components フォルダに Coupon.vue ファイルを作成します。コードの中身を下記の通りです。


<template>
  <div>Couponコード:1234</div>
</template>

index.vueファイルで作成したCouponコンポーネントを利用します。ref 関数を利用して定義した show 変数と v-if ディレクトリを利用してページが表示された時は Coupon コンポーネントは非表示にし、ボタンをクリックした時に表示できるように設定します。


<script setup>
const show = ref(false);

const handleClick = () => {
  show.value = true;
};
</script>
<template>
  <div>
    <h1>Main Page</h1>
    <button @click="handleClick">Coupon Get</button>
    <Coupon v-if="show" />
  </div>
</template>

デベロッパーツールのネットワークタブを開いてページを表示します。一連のダウンロードファイルの中に Coupon.vue ファイルが含まれていることが確認できます。

Couponコンポーネントのダウンロード
Couponコンポーネントのダウンロード

ボタンをクリックするとCoupon.vueファイルの内容が表示されます。

次にLazy Loadingを確認するためにCouponの前にLazyを追加したタグに変更します。


<script setup>
const show = ref(false);

const handleClick = () => {
  show.value = true;
};
</script>
<template>
  <div>
    <h1>Main Page</h1>
    <button @click="handleClick">Coupon Get</button>
    <LazyCoupon v-if="show" />
  </div>
</template>

ページを開いてネットワークタブを見ても Copon.vue ファイルは見つかりません。ページ上の”Coupon Get”ボタンをクリックしてください。ボタンをクリックした後にネットワークタブを見ると最後に Coupon.vue ファイルが追加されることがわかります。このように Lazy Loading を利用することでコンポーネントが必要な時のみLazy Loadingを設定したコンポーネントに関連するJavaScriptファイルをダウンロードさせることができます。

ボタンクリック後のCouponコンポーネントのダウンロード
ボタンクリック後のCouponコンポーネントのダウンロード

Lazy Loadingの動作確認が終わったらindex.vueファイルを元のコードに戻しておきます。


<template>
  <div>
    <h1>Main Page</h1>
  </div>
</template>

ナビゲーション設定

Navbar.vueファイルの中ではページを移動するためにaタグを利用していました。


<template>
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
  </nav>
</template>

a タグを利用してもページを移動することはできますが a タグではページを移動する度にページの再読み込みが行われます。クリックする度にページ全体を読み込みをするのではなくページの中で更新が必要な箇所だけ更新が行われるように a タグから NuxtLink コンポーネントを利用するために NuxtLink タグに変更します。


<template>
  <nav>
    <NuxtLink to="/">Home</NuxtLink>
    <NuxtLink to="/about">About</NuxtLink>
  </nav>
</template>

a タグから NuxtLink コンポーネントに変更するとページ全体が再読み込みされなくなるためページの移動がスムーズに行われるようになります。デベロッパーツールで確認すると NuxtLink タグを利用した場所には a タグが設定されていることが確認できますが JavaScript によって本来の a タグとは異なる動作になるように制御されています。NuxtLink で to props に設定できる値は内部リンクだけではなく外部のサイトを指定することもできます。外部のサイトの場合は通常の a タグのように動作し、内部リンクのようにスムーズにページが移動するということはありませんが内部リンクと外部サイトのリンクでタグを使い分ける必要はありません。また a タグのように target props(target 属性)を設定することもできます。


<template>
  <nav>
    <NuxtLink to="/">Home</NuxtLink>
    <NuxtLink to="/about">About</NuxtLink>
    <NuxtLink to="https://google.com" target="_blank">Google</NuxtLink>
  </nav>
</template>

URLの設定はto propsで行なっていますがtoのaliasとしてhref propsも利用可能です。ここまでの動作確認で分かる通りNuxt 3でリンクを設定する場合にはaタグではなくNuxtLinkを利用して設定していきます。

Nuxt 3のルーティングにはVue Routerライブラリが利用されています。

assetsディレクトリの設定

assetsディレクトリにはstylesheetsやfontsなどの情報を保存します。assetsディレクトリにcssや画像を保存しassetsディレクトリを利用した場合の動作を確認します。

CSSファイルの設定

assetsディレクトリににcssディレクトリを作成してstyle.cssファイルを作成します。

style.cssファイルに以下を記述します。


h1 {
  color: gray;
}

cssファイルはNuxtの設定ファイルであるnuxt.config.tsファイルで指定することができます。


// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },
  css: ['/assets/css/style.css'],
});

ブラウザで確認するとスタイルが適用されていることがわかります。

styleタグの確認
styleタグの確認

index.vue ファイルから直接 index.css ファイルを import することもできます。


<script setup>
import '@/assets/css/style.css';
</script>
<template>
  <div>
    <h1>Main Page</h1>
  </div>
</template>

script タグではなく style タグでも import することができます。その場合は import の前に@をつけて行います。scoped をつけると適用範囲を限定することができます。scoped は必須ではありません。


<template>
  <div>
    <h1>Main Page</h1>
  </div>
</template>
<style scoped>
@import '@/assets/css/style.css';
</style>

画像のファイルの保存

画像ファイルを assets ディレクトリに保存します。ここでは icon.png ファイルを assets フォルダの直下に」保存しています。Nuxtに関する画像はhttps://nuxt.com/design-kitからダウンロードすることができます。

index.vue ファイルから img タグを利用して assets ディレクトリの icon.png ファイルを指定しますがパスの先頭には”~”が必要となります。


<template>
  <div>
    <h1>Main Page</h1>
    <div>
      <img src="~/assets/icon.png" alt="Nuxt3 Icon" />
    </div>
  </div>
</template>

デベロッパーツールの要素を確認するとsrc属性の値は/assets/icon.pngではなく先頭に/_nuxt/が追加されていることがわかります。

assetsディレクトリに保存した画像の表示
assetsディレクトリに保存した画像の表示

このようにassetsディレクトリを利用したcssファイルや画像を保存して利用することができますがパスの設定等に注意が必要です。

publicディレクトリの設定

プロジェクト作成時から存在するpublicディレクトリには画像ファイルのような静的なファイルを保存することができます。publicディレクトリにはfavicon.iconファイルが保存されています。

assetsディレクトリでも画像を保存しブラウザ上から表示することができましたがassetsディレクトリと設定方法が異なります。

publicディレクトリにicon.pngファイルを保存します。index.vueファイルのimgタグでicon.pngを指定しますがpublicは必要ではありません。


<template>
  <div>
    <h1>Main Page</h1>
    <div>
      <img src="/icon.png" alt="Nuxt3 Icon" />
    </div>
  </div>
</template>

ブラウザで確認すると指定した画像が表示されます。パスはimgタグのsrcで設定した/icon.pngと同じ値になっていることがわかります。

publicディレクトリに保存したファイルの表示
publicディレクトリに保存したファイルの表示

faviconの設定

publicディレクトリへの画像ファイルの設定方法がわかったのでfaviconの設定方法について確認しておきます。

Nuxt 3ではmetaタグなどのデフォルトの設定をNuxtの設定ファイルであるnux.config.tsファイルで行うことができます。metaタグについては後ほど説明を行いますがtitleとdescriptionのみ設定を行いたい場合は下記のように行うことができます。設定はすべてのページで反映されます。


// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  css: ['/assets/css/style.css'],
  app: {
    head: {
      title: 'Nuxt 3 basic',
      meta: [{ name: 'description', content: 'Nuxt 3 for beginners' }],
    },
  },
});

設定を行うとブラウザのタブに設定したtitleの値が表示されます。

ブラウザのタブに表示されたtitleの確認
ブラウザのタブに表示されたtitleの確認

headタグの中のmetaタグのdescriptionにも設定値が反映されるので確認してください。

次はpublicディレクトリに保存したfaviconの設定を行います。


// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  css: ['/assets/css/style.css'],
  app: {
    head: {
      title: 'Nuxt 3 basic',
      meta: [{ name: 'description', content: 'Nuxt 3 for beginners' }],
      link: [{ rel: 'icon', href: '/icon.png' }],
    },
  },
});

ブラウザを確認すると設定したfaviconがブラウザのタブに表示されることが確認できます。

表示されたfaviconの確認
表示されたfaviconの確認

ブラウザ上では下記のように設定されます。Nuxt 3のドキュメントサイトと同じ設定になっています。


<link rel="icon" href="/icon.png">

画像の設定を通してpublicディレクトリの利用方法を理解することができました。

composablesディレクトリ

composables ディレクトリの中に composable な関数を含むファイルを保存するとコンポーネントから import することなく(Auto imports)利用することができます。composables な関数は reactive な変数とロジックをコンポーネントから切り離して再利用できるようにした関数です。

composables な関数がどのようなものかはっきりしていない人もいる思いますので useCounter という名前の composables な関数を作成して動作確認を行います。composables な関数は Nuxt 特有の関数ではなく Vue.js でも頻繁に利用します。他のフレームワークでもどのようの機能が利用されており、 React ではカスタムフックと呼ばれます。

composables ディレクトリを作成して useCounter.ts ファイルを作成します。useCounter 関数の引数から initialValue を受け取り ref 関数の初期値として count 変数を定義しています。count 変数を更新するための inc、dec 関数を定義して return で count, inc, dec を戻すといった内容です。composables な関数には ref 関数で定義された reactive な変数とロジックのみが含まれています。


export function useCounter(initialValue: number) {
  const count = ref(initialValue);
  const inc = () => (count.value = count.value + 1);
  const dec = () => (count.value = count.value - 1);
  return {
    count,
    inc,
    dec,
  };
}

pages/index.vue ファイルで useCounter を利用したい場合には下記のように import なし(Auto imports)で利用することができます。


<script setup>
const { count, inc, dec } = useCounter(100);
</script>
<template>
  <div>
    <h1>Main Page</h1>
    <div>Count:{{ count }}</div>
    <div>
      <button @click="() => inc()">increase</button>
      <button @click="() => dec()">decrease</button>
    </div>
  </div>
</template>

ブラウザ確認するとuseCounterの引数に設定した100が表示されincreaseボタンまたはdecreaseボタンを押すと数字が増減します。

increate, decreaseボタンで数字が増減
increate, decreaseボタンで数字が増減

pluginsディレクトリ

plugins ディレクトリを作成してプラグインファイルを作成するとアプケーションの起動時に Nuxt が作成したプラグインを自動で登録を行ってくれるため登録作業を行う必要がありません。

plugins ディレクトリを作成し、もっともシンプルなプラグイン hello.ts を利用して設定方法を確認します。

引数 msg を受け取り、Hello と受け取った引数を表示するだけのプラグインです。


export default defineNuxtPlugin(() => {
  return {
    provide: {
      hello(msg: string) {
        return `Hello ${msg}!`;
      },
    },
  };
});

pluginsディレクトリにプラグインを作成するだけでコンポーネントから利用することができます。index.vueでcompsablesのuseNuxtApp関数を実行してプラグイン$helloを取り出します。


<script setup>
const { $hello } = useNuxtApp();
</script>
<template>
  <div>
    <h1>Main Page</h1>
    <h2>{{ $hello('World') }}</h2>
  </div>
</template>

ブラウザにはプラグインhelloを利用したHello World!が表示されます。

プラグインを利用してHello World!
プラグインを利用してHello World!

ここまでの説明を通してpages, layouts, components, assets, public, composables, pluginsのディレクトリがどのような役割を持っているのかとどのような設定を行えば動作をするのかを理解することができました。

ルーティングの設定

pagesディレクトリのindex.vue, about.vueファイルを作成して自動でルーティングが設定されることを確認していますが階層ページなどルーティングの設定についてさらに詳しく確認しておきます。

階層ページの作成

About ページは pages ディレクトリの直下に about.vue ファイルを作成してブラウザから URL の/about でアクセスすることで About ページの内容を表示することができました。次は URL が/users/list というように階層になっている場合のルーティング方法について確認します。

pages ディレクトリの中に users ディレクトリを作成し、その下に list.vue ファイルを作成して以下のコードを記述します。


<template>
  <h1>User Listページ</h1>
</template>

Nuxt 3が自動でルーティングを行なってくれるので、ファイル作成後ブラウザで/users/listにアクセスするとlist.vueのtemplateタグに記述した内容が表示されます。このようにNuxtを利用すると階層的なURLのルーティングも自動で行ってくれます。

User Listページの確認
User Listページの確認

追加したListページのルーティングをNavbar.vueファイルに追加します。NuxtLinkのto propsにはオブジェクトを利用してnameプロパティでルーティングの名前を利用することでも設定できます。


<template>
  <nav>
    <NuxtLink to="/">Home</NuxtLink>
    <NuxtLink to="/about">About</NuxtLink>
    <NuxtLink :to="{ name: 'users-list' }">User List</NuxtLink>
  </nav>
</template>

users-list という名前が突然出てきましたが、ルーティングの name プロパティに設定する値については NuxtDevToolsを利用してpagesの情報から確認することができます。

NuxtDevTool
NuxtDevTool
自動でルーティングが設定されるのでファイル名を更新するとnameの値も変わります。Vue Routerを手動で設定する場合にはルーティングに対して任意の名前のname設定することでURLを変更してもnameでリンクを設定する場合は各ファイルでのURLの変更の必要ががなくなります。

リンクの追加を行ったのでHome ,Aboutページへの移動のようにページ上部のリンクからUser Listページに移動することができます。

/users/listでアクセスを行うことができるようになりましたが/users/にアクセスした場合にページを表示したい場合はusersディレクトリの下にindex.vueファイルを作成します。index.vueファイルない状態でアクセスすると404ページが表示されます。


<template>
  <h1>User Indexページ</h1>
</template>

/usersにアクセスするとindex.vueで記述した内容が表示されます。

/usersへのアクセス
/usersへのアクセス

ページのネスト化

pagesディレクトリの下にusersディレクトリと同じ名前のusers.vueファイルを作成します。


<template>
  <h1>Usersページ</h1>
</template>

users.vueファイルを作成すると/users, /users/listどちらにアクセスしてもusers.vueファイルに記述したUsersページのみ表示されるようになります。

users/listコンポーネントの中身を表示させるには親コンポーネントにあたるusers.vueファイルでNuxePageタグを追加する必要があります。


<template>
  <div>
    <h1>Usersページ</h1>
    <NuxtPage />
  </div>
</template>

NuxtPageタグを追加後に/users/listにアクセスすると下記のようにusers.vueファイルの内容とusers/list.vueファイルの内容が表示されます。list.vueファイルの内容はNuxtPageタグを追加した場所に表示されます。

ネスト化したルーティング
ネスト化したルーティング

/users/にアクセスするとusers.vueファイルの内容と/users/index.vueファイルの内容が表示されます。このようにページのネスト化を行うことができました。

Dynamicルーティング

/users/list にアクセスすると User List ページが表示されますが、/users/1、/users/2 のように/users/以下が動的に変わる場合に/users/以下の値をパラメータとして受け取りパラメータ毎に異なるのページ内容を表示したい場合の設定方法を確認していきます。パラメータは動的に変わるので users ディレクトリに[id].vue ファイルを作成します。[]で囲んだ値を任意の名前をつけることができます。


<template>
  <p>ユーザID</p>
</template>

/users/1, /users/2, /users/100にアクセスしても同じ内容が表示されます。

idを変更しても同じ内容が表示
idを変更しても同じ内容が表示

idに入った値は$routeオブジェクトを利用して取得することができます。$routeオプジェクトのparamsの中にidの値が入っています。


<template>
  <p>ユーザID: {{ $route.params.id }}</p>
</template>

/users/4にアクセスするとidの4がブラウザ上に表示されます。パラメータの値によってページ内容を変えることができるようになりました。

idをブラウザ上に表示
idをブラウザ上に表示

scriptタグの中でidにアクセスするために$routeではなくuseRoute関数を利用することができます。


<script setup>
const router = useRoute();

console.log(router.params.id);
</script>
<template>
  <p>ユーザID: {{ $route.params.id }}</p>
</template>

ブラウザのデベロッパーツールのコンソールにはidの値が表示されます。

Dynamicルーティングを利用する際に[id].vueというファイル名だけではなくuser-[id].vueファイルという名前をつけることもできます。idの部分が動的に変わるのでブラウザからアクセスする場合は/users/user-4という形でアクセスすることになります。”user-“は固定値となります。

Catch-all Route

pagesディレクトの下にdashboardディレクトリを作成してさらに[…slug].vueファイルを作成します。dashboard以下のすべてのURLでこのファイルの内容が表示されることになります。


<template>
  <h1>Dashboard</h1>
  <p>{{ $route.params.slug }}</p>
</template>

/dashboard以下の100/foo/barにアクセスした場合でも[…slug].vueファイルの内容が表示されていることが確認できます。

catch-all Routeの設定した場合
catch-all Routeの設定した場合

middlewareディレクトリ

middleware ディレクトリの下にファイルを作成することで Route Middleware の設定を行うことができます。Route Middleware を利用することでページ間の移動、サイトへのアクセス後のページが表示される前に事前に設定していた処理を行うことができます。例えばある特定のページには管理者権限を持っているユーザしかアクセスさせたくないといった場合、ページを表示する前に認証チェックを行うといったことが可能になります。認証のチェックに失敗した場合にはアクセスしたページを表示させず別のページにリダイレクトさせるといったことが行えます。

Route Middleware の動作確認を行うために middleware ディレクトリを作成しその下に auth.ts ファイルを作成します。

auth.ts ファイルでは defineNuxtRouteMiddleware 関数を利用します。defineNuxtRouteMiddleware 関数の中では to, from のオブジェクトが渡されるのでどのような値が入っているのか確認します。


export default defineNuxtRouteMiddleware((to, from) => {
  console.log('from', from);
  console.log('to', to);
});

設定したmiddlewareはabout.vueファイルのdefinePageMeta関数で設定することができます。middlewareプロパティの値に作成したファイル名authを設定します。


<template>
  <div>
    <h1>About Page</h1>
  </div>
</template>
<script setup>
definePageMeta({
  middleware: 'auth',
});
</script>

aboutページ以外のページからaboutページにアクセスするとコンソールログにfromとtoのオブジェクトが表示されます。ここでは/(ルート)ページから/aboutページに移動しています。

to, fromオブジェクトの確認
to, fromオブジェクトの確認

from はどのページから移動してきているのか to はどのページに移動しようとしているのかがわかります。from、to オブジェクトにはそれぞれのページのパスやパラメータなどが含まれていることがわかります。

middleware と from オブジェクトを利用することで/(ルート)ページからアクセスがあった場合のみ再度/(ルート)ページにリダイレクトさせるといったこともできます。

下記のコードではfrom の fullPath の値が”/“の場合には router の helper 関数である navigateTo 関数で”/“にリダイレクトさせるように設定しています。”/“から/about にページ移動すると about ページに移動することはできません。”/“以外のその他のページからは about ページにアクセスすることは可能です。


export default defineNuxtRouteMiddleware((to, from) => {
  if (from.fullPath === '/') {
    return navigateTo('/');
  }
});

ページ移動を中止したい場合にはaboutNavigation関数を利用することができます。


export default defineNuxtRouteMiddleware((to, from) => {
  if (from.fullPath === '/') {
    return abortNavigation();
  }
});

アクセスするユーザが管理者かどうかをチェックする関数を作ることでmiddlewareを利用してページへのアクセス制限をかけることができます。管理者ではない場合は/loginにリダイレクトされ管理者の場合はそのままページが表示されます。


export default defineNuxtRouteMiddleware((to, from) =>
  //isAdmin関数はアクセスしているユーザが管理者かどうかチェックする関数です。isAdminは存在しないので動作しません。各自が要件にあった関数を作成する必要があります。
  if (isAdmin() === false) {
    return navigateTo('/login')
  }
})

about ページのみに auth.ts ファイルによる middleware を設定しましたがすべてのページに設定したい場合にはファイルに global をつけることで自動で設定されます。auth.ts の場合は auth.global.ts とします。

ファイル名にglobalを追加設定した後はどのページにアクセスしてもブラウザのデベロッパーツールのコンソールに from, to オブジェクトが表示されるようになります。


export default defineNuxtRouteMiddleware((to, from) => {
  console.log('from', from);
  console.log('to', to);
});
layoutsディレクトリのレイアウトファイルではdefinePageMeta関数によるmiddlewareの設定はできません。

midelewareディレクトリにファイルを作成するのではなくdefinePageMeta関数にinlineでmiddlewareを設定したい場合には以下のように記述することができます。/aboutページに移動した場合のみデベロッパーツールのコンソールにto, fromオブジェクトが表示されます。


<script setup>
definePageMeta({
  middleware: defineNuxtRouteMiddleware((to, from) => {
    console.log('to', to);
    console.log('from', from);
  }),
});
</script>
<template>
  <div>
    <h1>About Page</h1>
  </div>
</template>

Page Transitions

ページを移動する際にアニメーションを設定したい場合にPage Transitionsを利用することができます。

Nuxtの設定ファイルであるnuxt.config.tsファイルのappプロパティにpageTransitionを追加します。ページ移動のモードの設定も行います。


export default defineNuxtConfig({
  // css: ['/assets/css/style.css'],
  app: {
    pageTransition: { name: 'page', mode: 'out-in' },
    head: {
      title: 'Nuxt 3 basic',
      meta: [{ name: 'description', content: 'Nuxt 3 for beginners' }],
      link: [{ rel: 'icon', href: '/icon.png' }],
    },
  },
});

設定後にapp.vueファイルにstyleを設定しますがapp.vueファイルを利用していない場合にはレイアウトファイルに設定します。


<template>
  <div>
    <Navbar />
    <slot />
  </div>
</template>
<script setup></script>
<style>
.page-enter-active,
.page-leave-active {
  transition: all 0.4s;
}
.page-enter-from,
.page-leave-to {
  opacity: 0;
  filter: blur(1rem);
}
</style>

ページ上部のナビゲーションバーを利用してページを移動するとblurによる少し文字にぼかしが入り通常よりもゆっくりと文字が画面に表示されます。

Page Tranditionは設定するだけではなくページ単位またはアプリケーション全体でPage Transitionの設定を無効にすることができます。

ページ単位の場合はdefinePageMeta関数で設定を行います。


<script setup lang="ts">
definePageMeta({
  pageTransition: false
})
</script>

アプリケーション全体ではNuxtの設定ファイルであるnuxt.config.tsファイルで設定します。


defineNuxtConfig({
  app: {
    pageTransition: false,
  }
//略

Meta Tagsの設定

構築したアプリケーションのページを検索エンジンに正しく認識してもらうためには SEO(Search Engine Optimization)への対応が必要になります。HtmlのMeta タグを設定することはSEOにとっても非常に重要なのでNuxt 3でのMeta タグの設定方法を確認します。

アプリケーション全体のMetaタグ設定は Nuxt の設定ファイルである nuxt.config.ts ファイルの app.head で行うことができます。アプリケーション全体に設定するとすべてページが同じ Meta タグになってしまうため、Meta タグはページ毎にも設定する必要がありその場合には composables の useHead 関数を利用することができます。さらにuseSeoMetaではSEO用のタグを設定することができます。

プロジェクト作成時には nuxt.config.ts、useHead 関数による Meta タグの設定は行われていませんが head タグを確認するとデフォルトの状態では charset が”utf-8”, viewport が”width=device-width, initial-scale=1”に設定されています。

デフォルトの設定
デフォルトの設定

 nuxt.config.tsによる設定

faviconsの設定の時にtitleとmetaタグでdescriptionを設定しましたが再度確認しておきます。


export default defineNuxtConfig({
  app: {
    head: {
      title: 'Nuxt 3 basic',
      meta: [{ name: 'description', content: 'Nuxt 3 for beginners' }],
      link: [{ rel: 'icon', href: '/icon.png' }],
    },
  },
});

linkで設定されているfaviconの画像ファイルicon.pngはpublicフォルダ直下に保存しています。ブラウザで確認するとブラウザのタブにtitleの設定値とfaviconの画像が表示されます。

表示されたfaviconの確認
表示されたfaviconの確認

nuxt.config.tsで設定した場合にはすべてのページで同じ内容が設定されている状態です。

useHeadによる設定

scriptタグの中でComposableのuseMeta関数を利用をしてtitle, base, script, style, metaとlinkを設定することができます。ここではtitleとmetaのdescriptionの設定のみabout.vueファイルで行います。他のmetaタグの設定についても同様の方法で行うことができます。


<template>
  <div>
    <h1>About Page</h1>
  </div>
</template>
<script setup>
useHead({
  title: 'Aboutページ',
  meta: [
    {
      name: 'description',
      content: 'Aboutページ',
    },
  ],
});
</script>

ブラウザのタブを確認するとuseHeadで設定した値になっていることが確認できます。またデベロッパーツールの要素を見るとdescriptionも設定されていることがわかります。

useHeadの設定による設定値の確認
useHeadの設定による設定値の確認

useHeadの設定はref関数によって動的に設定を行うこともできます。ref関数でtitleとdescriptionを定義しています。


<template>
  <div>
    <h1>About Page</h1>
  </div>
</template>
<script setup>
const title = ref('Aboutページ');
const description = ref('Aboutページ');
useHead({
  title,
  meta: [
    {
      name: 'description',
      content: description,
    },
  ],
});
</script>

各ページでtitleにサイト名などを含めたい場合はtitleTemplateを利用することができます。

app.vueファイル、レイアウトファイルでもuseHead関数を利用することができるのでlayousディレクトリのdefault.vueファイルにtitleTemplateに関数を利用して以下のように設定します。


<template>
  <div>
    <Navbar />
    <slot />
  </div>
</template>
<script setup>
useHead({
  titleTemplate: (title) => {
    return title ? `${title} - Nuxt 3 basic` : 'Nuxt 3 basic';
  },
});
</script>

about.vueファイルは先ほどの状態でブラウザで確認するとtitleにはdefault.vueのuseHeadのtitleTemplateで設定した”Nuxt 3 basic”が含まれていることが確認できます。

titleTemplateの値の反映を確認
titleTemplateの値の反映を確認

Dynamicルーティングで確認したidを設定する場合は下記のように行うことができます。


<script setup>
const router = useRoute();
useMeta({
  title: 'Nuxt 3',
  meta: [
    {
      name: 'description',
      content: `User Id: ${router.params.id}`,
    },
  ],
});
</script>
<template>
  <p>ユーザID: {{ $route.params.id }}</p>
</template>

titleTemplateで関数を利用しない場合はtitleの箇所に%sを設定することでtitleが設定されます。


useHead({
  titleTemplate: '%s - Nuxt 3 basic',
});

Meta Componentsによる設定

Nuxt 3では<Title>, <Base>, <Script>, <Style>, <Meta> , <Link>, <Body>, <HTML>と<Head>タグを利用することができます。useHeadと同様にtitleとdescriptionを設定するのであれば下記のように行うことができます。


<template>
  <div>
    <Head>
      <Title>Aboutページ</Title>
      <Meta name="description" content="Aboutページ" />
    </Head>
    <h1>About Page</h1>
  </div>
</template>
<script setup></script>

useHeadと同様に設定した値が反映されていることが確認できます。

Htmlタグにlang属性などを設定すると反映されます。


<template>
  <div>
    <Html lang="ja">
      <Head>
        <Title>Aboutページ</Title>
        <Meta name="description" content="Aboutページ" />
      </Head>
    </Html>
    <h1>About Page</h1>
  </div>
</template>
<script setup></script>

htmlタグのlangをjaに設定したい場合にはレイアウトファイルに設定するとそのレイアウトファイルを利用するページすべてで設定が反映されます。


<template>
  <div>
    <Html lang="ja" />
    <Navbar />
    <slot />
  </div>
</template>
<script setup>
useHead({
  titleTemplate: '%s - Nuxt 3 basic',
});
</script>

layoutsディレクトリのdefault.vueファイルでHeadタグを設定した後にaboutページにアクセスするとhtmlタグにlang属性jaが設定されていることが確認できます。

Headタグによるlangの設定
Headタグによるlangの設定

Metaタグの基本的な設定方法を理解することができました。

Data Fetching

アプリケーションを構築する場合に Data Fetching によりデータを取得し取得したデータをブラウザ上に表示させる必要があります。Nuxt 3 ではデータを取得するための関数が事前に準備されているためそれらの関数を利用することができます。Nuxt 3 で利用できるデータ取得に関する関数は useFetch, useLazyFetch, useAsyncData, useLazyAsyncData の 4 つです。それぞれの利用方法を確認していきます。

useFetch

useFetch は Promise を返す関数で GET リクエストであれば引数に URL を指定するだけで指定した URL からデータを取得することができます。

posts ディレクトリを作成して index.vue ファイルを作成して useFetch 関数を利用して無料で利用できる JSONPlaceHolder からデータを取得します。利用する URL は’https://jsonplaceholder.typicode.com/posts/‘でアクセスすると100件のPostデータが取得できます。

useFetch を利用してどのようなデータが戻されるのか確認を行います。


<script setup>
const response = await useFetch('https://jsonplaceholder.typicode.com/posts/');
console.log(response);
</script>
<template>
  <div>
    <h1>Posts一覧</h1>
  </div>
</template>

/postsにブラウザからアクセスするとブラウザのデベロッパーツールのコンソールにはuseFetch関数の実行によって戻される情報が表示されます。

useFetchで戻されるデータの中身
useFetchで戻されるデータの中身

戻されたデータの中には data の他に error, pending, execute, refresh が含まれていることがわかります。

コンソールのメッセージの情報から data, error, pending は RefImp と表示されていることからリアクティブな状態で戻されていることがわかります。ref 関数で変数を定義した時と同じなので script タグ内では data.value で data に含まれる情報にアクセスすることができます。残りの execute, refresh は関数です。

data には取得したデータ、error にはエラーメッセージ(エラーがある場合)、pending にはデータの取得中かどうか Boolean 値が含まれています。execute, refresh は関数なので実行することができ、実行すると再度データ取得を行うことができます。

data には 100 件の Post データ含まれているので分割代入で data のみ取り出して data にはわかりやすいように posts という別名をつけて v-for ディレクティブで展開しブラウザ上にタイトルを表示させます。


<script setup>
const { data: posts } = await useFetch(
  'https://jsonplaceholder.typicode.com/posts/'
);
</script>
<template>
  <div>
    <h1>Posts一覧</h1>
    <ul>
      <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
  </div>
</template>

/postsにアクセスするとJSONPlaceHolderから取得した100件のPost情報が表示されます。

useFetch関数で取得したPostデータ一覧
useFetch関数で取得したPostデータ一覧

componentsディレクトリのNavbar.vueファイルのナビゲーションにPost Listも追加しておきます。


<template>
  <nav>
    <NuxtLink to="/">Home</NuxtLink>
    <NuxtLink to="/about">About</NuxtLink>
    <NuxtLink :to="{ name: 'users-list' }">User List</NuxtLink>
    <NuxtLink to="/posts">Post List</NuxtLink>
  </nav>
</template>

dataだけではなくerrorの内容も確認します。errorの中身はscriptタグ内ではerror.valueで確認することができます。


<script setup>
const { data: posts, error } = await useFetch(
  'https://jsonplaceholder.typicode.com/posts/'
);
console.log(('error', error.value));
</script>

useFetchの処理が正常に行われた場合にはerror.valueの値はnullになります。

エラーを意図的に発生させるためuseFetch関数の引数を存在しないURLに変更するとerror.valueにはエラーメッセージが保存されます。v-ifディレクティブを利用してerrorに値が含まれているか分岐を行いerrorが含まれている場合にはブラウザ上にエラーメッセージを表示させます。


<script setup>
const { data: posts, error } = await useFetch(
  'https://jsonplaceholder.typicode.com/post/'
);
</script>
<template>
  <div>
    <h1>Posts一覧</h1>
    <p v-if="error">{{ error }}</p>
    <ul>
      <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
  </div>
</template>

useFetchによるリクエストに失敗した場合はエラーメッセージが表示されます。

ブラウザ上にエラーメッセージを表示
ブラウザ上にエラーメッセージを表示

refresh関数を利用してデータの再取得を行うため再取得ボタンを追加します。ボタンをクリックするとrefresh関数が実行され再取得が行われます。JSONPLaceHolder上のデータに変更がないため再取得しても画面上には変化がありませんがデベロッパーツールのネットワークタブを見るとリクエストが送信されrefresh関数が動作していることを確認することができます。


<script setup>
const {
  data: posts,
  error,
  refresh,
} = await useFetch('https://jsonplaceholder.typicode.com/posts/');

</script>
<template>
  <div>
    <h1>Posts一覧</h1>
    <button @click="refresh()">再取得</button>
    <p v-if="error">{{ error }}</p>
    <ul>
      <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
  </div>
</template>

pendingについてはuseLazyFetch関数の箇所で確認します。

useAsyncData

useFetch関数はuseAsyncData関数と$fetch関数の利用を便利にしたラッパーです。そのためuseFetch関数と同じ処理をuseAsyncData関数と$fetch関数を使って行うことできます。

useFetch関数をuseAsyncData関数と$fetch関数で書き換えると以下のコードになります。


<script setup>
const { data: posts, error } = await useAsyncData('posts', () =>
  $fetch('https://jsonplaceholder.typicode.com/posts/')
);
</script>
<template>
  <div>
    <h1>Posts一覧</h1>
    <p v-if="error">{{ error }}</p>
    <ul>
      <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
  </div>
</template>

useFetch の中では$fetchが利用されますがuseAsyncDataでは$fetch を利用せず別の関数に変更することができます。useFetch とは異なり第二引数の関数の中で処理を追加することもできます。つまり useFetch よりも useAsyncData を利用することで複雑な処理が行えるようになります。


const { data: posts, error } = await useAsyncData('posts', () => {
  console.log('fetch'); //追加の処理
  return $fetch('https://jsonplaceholder.typicode.com/posts/');
});

useAsyncData の第一引数にはキャッシュに利用されるユニークキーを設定しています。キーは省略することが可能で省略した場合は自動でキーが設定されます。その場合のキーはファイル名と行番号を利用して作成されます。


<script setup>
const { data: posts, error } = await useAsyncData(() =>
  $fetch('https://jsonplaceholder.typicode.com/posts/')
);
</script>
<template>
  <div>
    <h1>Posts一覧</h1>
    <p v-if="error">{{ error }}</p>
    <ul>
      <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
  </div>
</template>

posts というキーを設定して取得した値は useNuxtApp().payload.data からアクセスすることができます。


<script setup>
const { data: posts, error } = await useAsyncData(() =>
  $fetch('https://jsonplaceholder.typicode.com/posts/')
);
console.log(useNuxtApp().payload.data);
</script>
<template>
  <div>
    <h1>Posts一覧</h1>
    <p v-if="error">{{ error }}</p>
    <ul>
      <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
  </div>
</template>

postsというキーに100件のデータが含まれていることがわかります。

設定したキーで保存した値の確認
設定したキーで保存した値の確認

キーを指定していない場合も動作確認すると自動でキーが設定されていることも確認できます。

キーを設定していない場合
キーを設定していない場合

$fetchについて

useAsyncDataの中で突然出てきた$fetch関数について確認しておきます。

$fetchはヘルパー関数で内部ではofetchライブラリを利用しています。ofetchライブラリはNode, ブラウザ、ワーカー上で利用できるfetch APIです。

fetch 関数であれば使い慣れている人もいると思いますので fetch 関数との代表的な違いを確認しておきます。

fetch 関数では取得したデータを JSON Parse する必要(response.json())がありますが ofetch では自動で行ってくれます。


//$fetch関数
const posts = ref([]);
const data = await $fetch('https://jsonplaceholder.typicode.com/posts/');
posts.value = data;

//fetch関数
const posts = ref([]);
const response = await fetch('https://jsonplaceholder.typicode.com/posts/');
const data = await response.json();
posts.value = data;

fetchでPOSTリクエストを実行する際にJSON.stringifyでJSON形式のデータに変換しますがofetchでは自動で行ってくれます。


//$fetch関数
const data = await $fetch('/api/user', {
  method: 'POST',
  body: { name: 'John Doe' },
});

//fetch関数
const response = await fetch('/api/user', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name: 'John Doe' }),
});

その他には存在しないURLにアクセスした場合にresponse.okがfalseの場合エラー処理を行ってくれたりinterceptorsやauto retryやbaseURLを設定することもできます。

useLazyFetch

useFetchとuseLazyFetchの違いを確認するためにuseFetchのコードをuseLazyFetchのコードに書き換えます。


<script setup>
const { data: posts, error } = await useLazyFetch(
  'https://jsonplaceholder.typicode.com/posts/'
);
</script>
<template>
  <div>
    <h1>Posts一覧</h1>
    <p v-if="error">{{ error }}</p>
    <ul>
      <li v-for="post in posts" :key="post.id">
        <NuxtLink :to="`/posts/${post.id}`">{{ post.title }}</NuxtLink>
      </li>
    </ul>
  </div>
</template>

最初に”/“にアクセスして上部のリンクから/posts に移動します。現在の設定では useFetch から useLazyFetch に変更しても違いはわかりません。

違うを明確にするため JSONPlaceHolder からのデータ取得の時間が遅延するようにブラウザのデベロッパーツールでネットワークのスピードを”Slow 3G”に変更します。

ネットワークのスピードを変更
ネットワークのスピードを変更

再度動作確認を行うと useFetch の場合は/posts にアクセスするとすぐに posts ページの画面が表示されるわけではなくしばらく時間が経過すると”Post 一覧”と取得したデータが同時に表示されます。useLazyFetch の場合は最初に”Post 一覧”が表示されてからその後取得したデータが表示されます。

ドキュメントを確認すると”By default, useFetch blocks navigation until its async handler is resolved.”と記載されており、useFetch が async handler が resolve されるまでナビゲーションをブロックしているためデータの取得が完了するまでページへの移動が行われません。useLazyFetch ではナビゲーションがブロックされないのでページに移動して”Post 一覧”がデータの取得が完了する前に表示されることになります。

useFetch のオプションの lazy を true に設定することで useLazyFetch と同じ動作になります。


const { data: posts, error } = await useFetch(
  'https://jsonplaceholder.typicode.com/posts/',
  {
    lazy: true,
  }
);

useLazyFetchから戻されるpendingを利用することでデータ取得中のみローディングを表示させることができます。


<script setup>
const {
  data: posts,
  error,
  pending,
} = await useLazyFetch('https://jsonplaceholder.typicode.com/posts/');
</script>
<template>
  <div>
    <h1>Posts一覧</h1>
    <p v-if="error">{{ error }}</p>
    <p v-if="pending">Loading...</p>
    <ul>
      <li v-for="post in posts" :key="post.id">
        <NuxtLink :to="`/posts/${post.id}`">{{ post.title }}</NuxtLink>
      </li>
    </ul>
  </div>
</template>

ブラウザ上で確認するとデータの取得が完了して表示させるまで”Loading…”のメッセージが表示されます。

pendingを利用してLoading...を表示
pendingを利用してLoading…を表示
useFetchでpendingを設定してもデータの取得が完了してからページが表示されるため値がいつもfalseなのでLoading…が表示させることはありません。

一度目の”/”から/postsへの移動時にはLoading…のみ画面がに表示されますが2回目からは取得済みのキャッシュのデータが利用されるため”loading…”と取得済みのデータが表示されます。

2回目のアクセス時の"Loading..."の表示
2回目のアクセス時の”Loading…”の表示

useLazyAsyncData

useLazyFetch関数はuseLazyAsyncData関数と$fetch関数を利用を便利にしたラッパーです。そのためuseLazyFetch関数と同じ処理をuseLazyAsyncData関数と$fetch関数で行うことができます。


<script setup>
const {
  data: posts,
  error,
  pending,
} = await useLazyAsyncData('posts', () =>
  $fetch('https://jsonplaceholder.typicode.com/posts/')
);
</script>
<template>
  <div>
    <h1>Posts一覧</h1>
    <p v-if="error">{{ error }}</p>
    <p v-if="pending">Loading...</p>
    <ul>
      <li v-for="post in posts" :key="post.id">
        <NuxtLink :to="`/posts/${post.id}`">{{ post.title }}</NuxtLink>
      </li>
    </ul>
  </div>
</template>

Modules

Nuxt3ではModulesを利用することでNuxtプロジェクトに簡単に機能の追加を行うことができます。Nuxt3で利用できるモジュールはhttps://nuxt.com/modulesで確認することができます。

Nuxt Tailwind

CSSのスタイルにTailwind CSSを利用したい場合にはNuxt Tailwindを利用することができます。

最初にモジュールのインストールを行います。


 % npm install --save-dev @nuxtjs/tailwindcss

モジュールのインストール後にnuxt.config.tsファイルに追加したモジュールを設定します。


// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  modules: ['@nuxtjs/tailwindcss'],
});

Tailwind CSSの設定ファイルを利用する場合はnpx tailwindcss initコマンドを実行します。


% npx tailwindcss init

Created Tailwind CSS config file: tailwind.config.js

以上で設定は完了です。

classにTailwind CSSが提供するclassを設定することで反映されます。さらにNuxt Tailwindを利用している場合は開発サーバを起動するとTailwind Viewerを利用することができます。


 %npm run dev

Nuxi 3.0.0                                                                                                        10:11:24
Nuxt 3.0.0 with Nitro 1.0.0                                                                                       10:11:24
                                                                                                                  10:11:26
  > Local:    http://localhost:3000/ 
  > Network:  http://192.168.2.132:3000/

ℹ Using default Tailwind CSS file from runtime/tailwind.css                                      nuxt:tailwindcss 10:11:26
ℹ Tailwind Viewer: http://localhost:3000/_tailwind/   

http://localhost:3000/_tailwind/にアクセスするとTailwindのクラスを確認することができます。クラス名にカーソルを合わせるとコピーすることもできます。

Tailwind Config Viewerの画面
Tailwind Config Viewerの画面

useState

コンポーネント間やページ間で状態管理(データ共有)したい場合に composables の useState を利用することができます。useState はシンプルな状態管理に利用することができより複雑な状態管理であればライブラリの Pinia を利用することになります。

状態管理とはどのようなことかを理解するために ref 関数を利用した場合と比較します。

about.vue ファイルを利用して ref 関数で counter を定義します。ボタンにより counter の数を増やせるように設定を行います。


<template>
  <div>
    <Head>
      <Title>Aboutページ</Title>
      <Meta name="description" content="Aboutページ" />
    </Head>
    <h1>About Page</h1>
    <h2>Counter</h2>
    <p>Count: {{ counter }}</p>
    <div><button @click="counter++">+</button></div>
  </div>
</template>
<script setup>
const counter = ref(0);
</script>

ボタンをクリックするとCountの数がクリック分だけ増えていきます。

ref関数でcount変数を定義
ref関数でcount変数を定義

Countを増やした後上部のリンクを利用して別のページに移動して再度aboutページに戻ってきてください。ブラウザ上のCountの値をクリアされて0と表示されます。このようにref関数ではページ間の移動を行うと値がクリアされ保持することができません。

ページを移動してもcountの値を保持したい場合にuseStateを利用することができます。


<script setup>
const counter = useState('counter', () => 0);
</script>
counterはref関数なのでscriptタグ内でcounterの値を確認したい場合はcounter.valueでアクセスすることができます。

useStateの第一引数にkeyのcounterを設定して初期値として関数で0を戻しています。keyを設定することで後ほど別のページから共有したcounterにアクセスします。

ボタンをクリックして別のページに移動し再度aboutページに戻ってきてください。先ほどのref関数とは異なりcountの値が保持されていることが確認できます。

1つのページ内ではページを移動してもuseStateにより状態が保持できることがわかりました。

次は別のページからもcounterの値にアクセスできるか確認します。users.vueファイルを使って先ほど設定したキーのcounterとuseStateを利用します。


<template>
  <div>
    <h1>Usersページ</h1>
    <p>Count:{{ counter }}</p>
    <NuxtPage />
  </div>
</template>
<script setup>
const counter = useState('counter');
</script>

aboutページでボタンをクリックしてcountを増やした後に/users/listページにリンクから移動します。ユーザリストページでもcountの値が保持できていることが確認できます。useStateによって状態の保持とコンポーネント間でのデータ共有が行えることがわかりました。

別のページでuseStateの共有した値にアクセス
別のページでuseStateの共有した値にアクセス

リンクによりaboutページからの移動ではなく/users/listページに直接アクセスを行ってください。/users/listページを開いた状態でブラウザのリロードでも問題ありません。

ブラウザで確認するとcountの値が入っていない状態でブラウザ上に表示されます。この時の値はundefinedです。定義がされていないために表示されていません。

counterの値が表示されない
counterの値が表示されない

この後、リンクを利用してaboutページに移動して戻ってくるとCounterの値が表示されます。このようにuseStateを利用した場合は定義しているコンポーネントで最初に状態の初期化を行う必要があります。

この問題を回避するためにcounter用のcomposablesを作成します。composablesディレクトリのuseCounter.tsファイルをuseStateを利用して書き換えます。useCounter.tsファイルがない場合は作成してください。


export function useCounter() {
  return useState(() => 0);
}

useCounter.tsファイルを作成後、aboutページからuseCounterを利用します。counterの初期値の0が表示され、ボタンをクリックするとcountの値が増えます。


<template>
  <div>
    <Head>
      <Title>Aboutページ</Title>
      <Meta name="description" content="Aboutページ" />
    </Head>
    <h1>About Page</h1>
    <h2>Counter</h2>
    <p>Count: {{ counter }}</p>
    <div><button @click="counter++">+</button></div>
  </div>
</template>
<script setup>
const counter = useCounter();
</script>

users.vueファイルでも同様にuseCounterを利用します。composablesでuseCounterを定義することでどのコンポーネントからcounterの値が取得できるようになります。/users/listに最初にアクセスしてもcounterの値は表示されます。


<template>
  <div>
    <h1>Usersページ</h1>
    <p>Count:{{ counter }}</p>
    <NuxtPage />
  </div>
</template>
<script setup>
const counter = useCounter();
</script>

エラーハンドリング

ここではNuxt 3におけるエラーハンドリングの設定について確認します。この章を読み終えるとNuxtErrorBoundary, ヘルパー関数createError, showError, useError, clearError, エラーページerror.vue, hooksなどの理解を深めドキュメントのError handlingの説明も理解できると思います。

NuxtErrorBoundary

NuxtErrorBoundaryはクライアント側で発生するエラーをキャッチして事前に設定したエラー内容をブラウザ上に表示させたい時に利用できるコンポーネントです。どのように設定するか確認していきます。

users.vueファイルには以下の内容を記述します。


<template>
  <div>
    <h1>Usersページ</h1>
    <NuxtPage />
  </div>
</template>

users ディレクトリの list.vue ファイルを利用してクライアント側で意図的にエラーを発生させるコードを追加するためまずは正常に動作するコードを追加します。

ref 関数で counter を定義しボタンを追加します。クリックすると counter の値が増えるボタンを追加します。ブラウザから/users/list にアクセスしてボタンをクリックすると count の数が増えます。


<template>
  <div>
    <h1>ユーザリスト</h1>
    <h2>Counter</h2>
    <p>Count: {{ counter }}</p>
    <div><button @click="inc">+</button></div>
  </div>
</template>
<script setup>
const counter = ref(0);
const inc = () => {
  counter.value++;
};
</script>
Counterの表示
Counterの表示

先ほどのコードで問題なく動作することを確認し今度は意図的にエラーを発生させるためにヘルパー関数 createError を throw させます。ヘルパー関数 createError は Nuxt から提供されている関数でクライアント側でもサーバ側でも利用することができエラーオブジェクトを作成することができます。


createError については後ほどもう少し詳しく説明しています。

<script setup>
const counter = ref(0);
const inc = () => {
  throw createError('エラー発生');
  counter.value++;
};
</script>

ボタンをクリックするとヘルパー関数createErrorによるエラーオブジェクトがthrowされブラウザのコンソールにエラーメッセージが表示されます。この時画面には何も変化はありません(エラーはブラウザ上に表示されません)。

エラーが表示
エラーが表示

次にlist.vueファイルの親コンポーネントであるusers.vueファイルでNextPageコンポーネントをNuxtErrorBoundaryコンポーネントでラップします。


<template>
  <div>
    <h1>Usersページ</h1>
    <NuxtErrorBoundary>
      <NuxtPage />
    </NuxtErrorBoundary>
  </div>
</template>

再度ボタンをクリックしてエラーを発生させます。先ほどとは異なり throw したエラーが NuxtErrorBoundary によりキャッチされるためコンソールには何もメッセージが表示されません。さらに NuxtErrorBoundary でラップしたコンポーネントの内容(list.vue ファイルの内容)もブラウザ上に表示されなくなります。

NuxtPageの内容が表示されない
NuxtPageの内容が表示されない

NuxtErrorBoundaryの名前付きSlotを利用することにより本来表示されるはずの内容(NuxtPage)とは別のエラー内容を表示させることができます。


<template>
  <div>
    <h1>Usersページ</h1>
    <NuxtErrorBoundary>
      <NuxtPage />
      <template #error="{ error }">
        <p>{{ error }}</p>
      </template>
    </NuxtErrorBoundary>
  </div>
</template>

このように NuxtErrorBoundary と template タグを組み合わせることでエラーが発生した場合には template タグを設定した場所にエラー内容が表示されます。

エラー表示
エラー表示

ブラウザ上に表示されているエラーは消したい場合には error の値を null に設定する必要があります。ヘルパー関数 clearError も存在しますがクライアント側で利用しても error の値をクリアにすることはできないため error.value で値を null にします。

ヘルパー関数 clearError は後ほど利用します。

<template>
  <div>
    <h1>Usersページ</h1>
    <NuxtErrorBoundary>
      <NuxtPage />
      <template #error="{ error }">
        <p>{{ error }}</p>
        <button @click="resetError(error)">Clear Error</button>
      </template>
    </NuxtErrorBoundary>
  </div>
</template>
<script setup>
const resetError = (error) => {
  error.value = null;
};
</script>

エラー発生後に表示される”Clear Error”ボタンをクリックするとエラー画面が消え最初に表示されていたカウントが再表示されます。

ここではNuxtPageタグをNuxtErrorBoundaryタグでラップしましたが通常のコンポーネントをラップして利用することもできます。
エラーは別のルーティングに移動しても自動でクリアされます。ダイナミックルーティング間での移動の場合にはクリアされないので注意してください。リンクを介した/users/1から/users/2への移動など。

エラーの情報を取得して別の処理を行いたい場合はエラーイベントを設定することができます。ここでは someErrorLogger 関数を追加してブラウザのコンソールに console.log で error のメッセージを表示させています。


<template>
  <div>
    <h1>Usersページ</h1>
    <NuxtErrorBoundary @error="someErrorLogger">
      <NuxtPage />
      <template #error="{ error }">
        <p>{{ error }}</p>
        <button @click="resetError(error)">Clear Error</button>
      </template>
    </NuxtErrorBoundary>
  </div>
</template>
<script setup>
const resetError = (error) => {
  error.value = null;
};
const someErrorLogger = (error) => {
  //取得したエラーで別の処理を行う
  console.log('someErrorLogger', error);
};
</script>

エラーが発生するとイベントにより someErrorLogger 関数が実行されブラウザのデベロッパーのコンソールには以下のメッセージが表示されます。


someErrorLogger Error: エラー発生
    at createError (index.mjs?v=72177548:122:12)
    at createError (error.js?v=72177548:31:16)
    at inc (list.vue:12:9)
    at callWithErrorHandling (chunk-BVQHDTV7.js:1565:18)
    at callWithAsyncErrorHandling (chunk-BVQHDTV7.js:1573:17)
    at HTMLButtonElement.invoker (chunk-BVQHDTV7.js:9397:5)

ここまで createError を利用して Error を throw していましたが createError ではなく new Error を利用した場合にどうなるのか気になる人もいるかと思います。下記のように記述しても同じように動作します。

&
lt;script setup>
const counter = ref(0);
const inc = () => {
  throw new Error('エラーが発生しました。');
  counter.value++;
};
</script>

ヘルパー関数createError


throw createError({
  statusCode: '400',
  statusMessage: 'Bad Request',
  message: 'エラー発生',
});

createErrorの引数を文字列からオブジェクトに変更してもNuxtErrorBoundaryの動作は同じでmessageを設定した場合はmessageに設定した文字列が表示されます。messageを設定していない場合にはstatusMessageが表示されます。

createErrorにはもう一つfatalというプロパティがありデフォルトではfatalはfalseですがtrueに変更します。


throw createError({
  statusCode: '400',
  message: 'エラー発生',
  fatal: true,
});

fatal プロパティの値をデフォルトの false から true に変更するとどうように影響があるのか確認します。


<script setup>
const counter = ref(0);
const inc = () => {
  throw createError({
    statusCode: '400',
    statusMessage: 'Bad Request',
    message: 'エラー発生',
    fatal: true,
  });
  counter.value++;
};
</script>

NuxtErrorBoundary を設定している場合には fatal を true に変更しても変化はありません。理由は NuxtErrorBoundary によりエラーの伝搬が止まるためです。users.vue から NuxtErrorBoundary のタグを削除してください。


<template>
  <div>
    <h1>Usersページ</h1>
    <NuxtPage />
  </div>
</template>

users.vue ファイルを更新後ボタンをクリックしてエラーを発生させます。NuxtErrorBoundary を設定せず fatal を true にしていない場合はブラウザのコンソールにエラーが表示されていましたが fatal を true に設定することでフルページのエラー画面が表示されます。画面を構成する文字列を確認すると createError の引数で設定したオブジェクトの内容であることがわかります。400 は createError で設定した statusCode プロパティの値, ”エラー発生”という文字列は message プロパティ、ブラウザのタブに設定されているのは statusMessage の”Bad Request”です。

statusMessage を設定をしていない場合にはタブには”400 – Internal Sever Error”という文字列が表示されます。
フルページのエラーメッセージ
フルページのエラーメッセージ

もしcreateErrorのオブジェクトにfatalのtrueだけを設定している場合はデフォルトとして以下の画面が表示されます。


<script setup>
const counter = ref(0);
const inc = () => {
  throw createError({
    fatal: true,
  });
  counter.value++;
};
</script>
デフォルトのエラー画面
デフォルトのエラー画面

createError についはドキュメントの説明を見るとクライアントサイドで利用した場合は fatal を true にするとフルスクリーンのエラーメッセージが表示されると記述されているので記述通りの動作することが確認できました。

ヘルパー関数showError

ヘルパー関数の showError が Nuxt3 から提供されており、createError の代わりに以下のように利用することができます。


showError({
  statusCode: 400,
  statusMessage: 'Bad Request',
  message: 'エラー発生',
});

NuxtErrorBoundary を設定している状態で list.vue ファイルの createError 関数を showError 関数に変更します。


<script setup>
const counter = ref(0);
const inc = () => {
  throw showError({
    statusCode: 400,
    statusMessage: 'Bad Request',
    message: 'エラー発生',
  });
  counter.value++;
};
</script>

showError 関数を利用している場合は NuxtErrorBoundary を設定している状態でもフルスクリーンのエラーページが表示されます。ドキュメントでは createError の利用を推奨しています。

erorr.vue

フルスクリーン表示されたエラーページをカスタマイズしたい場合にはプロジェクトディレクトリ直下にerror.vueファイルを作成することで対応することができます。エラーはヘルパー関数useErrorから取得することができます。


<template>
  <NuxtLayout>
    <p>{{ error }}</p>
  </NuxtLayout>
</template>

<script setup>
const error = useError();
</script>

error.vueファイルを作成後にエラーが発生するとフルスクリーンのエラー画面がerror.vueファイルの内容に変更されます。

error.vueファイルの内容が表示
error.vueファイルの内容が表示

useErrorではなくerrorオブジェクトをpropsとして受け取ることもできます。


<template>
  <NuxtLayout>
    <p>{{ error }}</p>
    <button @click="clearError({ redirect: '/' })">Clear Error</button>
  </NuxtLayout>
</template>

<script setup>
const props = defineProps({
  error: Object,
});
</script>

ヘルパー関数clearError

フルスクリーンで表示されるエラーについてはヘルパー関数 clearError を利用することでクリアにすることができます。error.vue ファイルにボタンを追加してクリックイベントにヘルパー関数 clearError を指定します。


<template>
  <NuxtLayout>
    <p>{{ error }}</p>
    <button @click="clearError">Clear Error</button>
  </NuxtLayout>
</template>

<script setup>
const error = useError();
</script>

エラー画面が表示された後に”Clear Error”ボタンをクリックするとエラー画面が消え、元の画面が表示されます。エラーが発生した場合に元の画面に戻っても同じエラーが発生する場合は redirect を設定することもできます。


<template>
  <NuxtLayout>
    <p>{{ error }}</p>
    <button @click="clearError({ redirect: '/' })">Clear Error</button>
  </NuxtLayout>
</template>

<script setup>
const error = useError();
</script>

サーバサイドでのcreateError

サーバサイドでヘルパー関数の createError を利用してエラーを意図的に発生させます。users/lists.vue ファイルでは useFetch を利用して JSONPLACEHolder にアクセスを行いますが存在しない URL にアクセスを行うため users.value にはデータが含まれていません。データがない場合に createError でエラーを発生させます。


<script setup>
const { data: users } = await useFetch(
  'https://jsonplaceholder.typicode.com/user/'
);
console.log('process');
if (!users.value) {
  throw createError({ statusCode: 404, statusMessage: 'Page Not Found' });
}
</script>
<template>
  <div>
    <h1>ユーザリスト</h1>
    <ul>
      <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

/users/list にブラウザから直接アクセス(/users/list でページをリロードするかブラウザの URL に直接入力してアクセスする)を行うと error.vue ファイルが存在するので以下の画面が表示されます。サーバサイドではドキュメントに記載通りフルスクリーンのエラーページが表示されることがわかります。

error.vue ファイルが存在しない場合はフルスクリーンで 404、“Page Not Found”の文字が画面に大きく表示されます。
サーバ側でのcreateErrorの実行
サーバ側でのcreateErrorの実行

別のページ(例:/about)からリンクを利用して/users/list にアクセスした場合は画面には何も表示されず元のページが表示された状態となります。

vueApp.config.errorHandler

エラーのレポーティングのシステムなどを利用している場合に vueApp.config.errorHandler を利用することで発生したエラーを通知することができます。

プラグインとして作成するので plugins フォルダに任意の名前のファイルを作成してください。ここでは vueAppErrorHandler.ts としています。エラーレポーティングのシステムを利用していないのでそのまま console.log でエラーを表示させます。


export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.config.errorHandler = (error, context) => {
    console.log('エラー', error);
    console.log('コンテキスト', context);
  };
});

users.vue ファイルでは NuxtErrorBoundary を利用していない下記の設定を行います。


<template>
  <div>
    <h1>Usersページ</h1>
    <NuxtPage />
  </div>
</template>

list.vue ファイルでは createError でエラーを発生させます。

<template>
  <div>
    <h1>ユーザリスト</h1>
    <h2>Counter</h2>
    <p>Count: {{ counter }}</p>
    <div><button @click="inc">+</button></div>
  </div>
</template>
<script setup>
const counter = ref(0);
const inc = () => {
  throw createError('エラー発生');
  counter.value++;
};
</script>

ブラウザ上のボタンをクリックするとブラウザのデベロッパーツールのコンソールには plugins の vueAppErrorHandler.ts で設定した内容のメッセージが表示されます。

console


エラー Error: エラー発生
    at createError (index.mjs?v=b0229ba6:122:12)
    at createError (error.js?v=b0229ba6:31:16)
    at inc (list.vue:12:9)
    at callWithErrorHandling (chunk-BVQHDTV7.js:1565:18)
    at callWithAsyncErrorHandling (chunk-BVQHDTV7.js:1573:17)
    at HTMLButtonElement.invoker (chunk-BVQHDTV7.js:9397:5)
vueAppErrorHandler.ts:4
コンテキスト Proxy(Object) {…}[[Handler]]: Object[[Target]]: Object[[IsRevoked]]: false

users.vue ファイルで NuxtErrorBoundary の設定を行います。

<template>
  <div>
    <h1>Usersページ</h1>
    <NuxtErrorBoundary>
      <NuxtPage />
      <template #error="{ error }">
        <p>{{ error }}</p>
      </template>
    </NuxtErrorBoundary>
  </div>
</template>

ブラウザの画面に”Error: エラー発生”の文字列が表示されますが、NuxtErrorBoundary でエラーをキャッチするとエラーが上位に伝搬されないため vueApp.config.errorHandler のエラー処理は実行されないためブラウザのコンソールには何も表示されません。

hooksによるエラーの取得

Nuxt 3 では vue:error hook も提供されているのでプラグインを利用することでエラーを取得することができます。ドキュメントの Lifecycle Hooks の App Hooks(runtime)では vue:error 以外にも app:error などがあることも確認できます。vue:error の引数には err, target,info があることもわかります。

Lifecycle Hooks
Lifecycle Hooks

pluginsにおけるHooksの設定方法は下記の通りです。


export default defineNuxtPlugin((nuxtApp) =>
    nuxtApp.hook('page:start', () => {
        /* your code goes here */
     })
})

vue:error であれば任意の名前のファイル errorHooks.ts ファイルを plugins ディレクトリの中に作成して以下のコードを記述します。動作確認のため plugins フォルダに vueAppErrorHandler.ts ファイルが存在する場合には削除しておきます。


export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('vue:error', (err, target, info) => {
    console.log('err', err);
    console.log('target', target);
    console.log('info', info);
  });
});

users.vue には NuxtErrorBoundary を利用していない下記のコードを記述します。


<template>
  <div>
    <h1>Usersページ</h1>
    <NuxtPage />
  </div>
</template>

list.vue ファイルでは createError でエラーを発生させます。


<template>
  <div>
    <h1>ユーザリスト</h1>
    <h2>Counter</h2>
    <p>Count: {{ counter }}</p>
    <div><button @click="inc">+</button></div>
  </div>
</template>
<script setup>
const counter = ref(0);
const inc = () => {
  throw createError('エラー発生');
  counter.value++;
};
</script>

ブラウザ上のボタンをクリックするとブラウザのデベロッパーツールのコンソールには plugins の errorHooks.ts で設定した内容のメッセージが表示されます。


err Error: エラー発生
    at createError (index.mjs?v=b0229ba6:122:12)
    at createError (error.js?v=b0229ba6:31:16)
    at inc (list.vue:12:9)
    at callWithErrorHandling (chunk-BVQHDTV7.js:1565:18)
    at callWithAsyncErrorHandling (chunk-BVQHDTV7.js:1573:17)
    at HTMLButtonElement.invoker (chunk-BVQHDTV7.js:9397:5)
errorHooks.ts:4 target Proxy(Object) {…}
errorHooks.ts:5 info native event handler

users.vue ファイルに vue には NuxtErrorBoundary 設定した場合にはコンソールにはエラーは表示されず、ブラウザの画面に”Error: エラー発生”の文字列が表示されます。

list.vue ファイルを更新します。


<script setup>
const { data: users } = await useFetch(
  'https://jsonplaceholder.typicode.com/user/'
);
console.log('process');
if (!users.value) {
  throw createError({ statusCode: 404, statusMessage: 'Page Not Found' });
}
</script>
<template>
  <div>
    <h1>ユーザリスト</h1>
    <ul>
      <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

サーバ側でエラーが発生するために”npm run dev”コマンドを実行したターミナルに以下のメッセージが表示されます。画面にはフルスクリーンのエラー画面が表示されます。


err H3Error: Page Not Found                                                                                                                 13:36:11
    at Module.createError (file:///Users/mac/Desktop/nuxt3-app/node_modules/h3/dist/index.mjs:127:15)
    at Module.createError (/Users/mac/Desktop/nuxt3-app/node_modules/nuxt/dist/app/composables/error.js:39:38)
    at setup (/Users/mac/Desktop/nuxt3-app/pages/users/list.vue:26:31)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  statusCode: 404,
  fatal: false,
  unhandled: false,
  statusMessage: 'Page Not Found',
  __nuxt_error: true
}
target {}
info setup function

/about から/users/list にリンクをつかって移動すると下記のエラーメッセージがコンソールに表示されます。ブラウザの画面は/about のままです。users.vue ファイルには NuxtErrorBoundary を設定していません。


err Error: Page Not Found
    at createError (index.mjs?v=b0229ba6:127:15)
    at createError (error.js?v=b0229ba6:31:16)
    at setup (list.vue:7:9)
errorHooks.ts:4 target Proxy(Object) {…}
errorHooks.ts:5 info setup function
index.mjs?v=b0229ba6:127 Uncaught (in promise) Error: Page Not Found
    at createError (index.mjs?v=b0229ba6:127:15)
    at createError (error.js?v=b0229ba6:31:16)
    at setup (list.vue:7:9)

NuxtErrorBoundary を設定した場合には下記のエラーメッセージがブラウザのコンソールに表示されます。


err Error: Page Not Found
    at createError (index.mjs?v=b0229ba6:127:15)
    at createError (error.js?v=b0229ba6:31:16)
    at setup (list.vue:7:9)
errorHooks.ts:4 target Proxy(Object) {…}
errorHooks.ts:5 info setup function

onErrorCaptured Hook

onErrorCaptured を利用してエラーを取得したい場合は users.vue ファイルに設定を行います。


<template>
  <div>
    <h1>Usersページ</h1>
    <NuxtPage />
  </div>
</template>
<script setup>
onErrorCaptured((err, instance, info) => {
  console.log('err', err);
  console.log('instance', instance.$refs);
  console.log('info', info);
});
</script>

list.vue ファイルで createError を設定してボタンをクリックします。


<template>
  <div>
    <h1>ユーザリスト</h1>
    <h2>Counter</h2>
    <p>Count: {{ counter }}</p>
    <div><button @click="inc">+</button></div>
  </div>
</template>
<script setup>
const counter = ref(0);
const inc = () => {
  throw createError('エラー発生');
  counter.value++;
};
</script>

ブラウザのコンソールに onErrorCaptured で設定した内容が表示されます。


err Error: エラー発生
    at createError (index.mjs?v=b0229ba6:122:12)
    at createError (error.js?v=b0229ba6:31:16)
    at inc (list.vue?t=1689398632638:10:34)
    at callWithErrorHandling (chunk-BVQHDTV7.js:1565:18)
    at callWithAsyncErrorHandling (chunk-BVQHDTV7.js:1573:17)
    at HTMLButtonElement.invoker (chunk-BVQHDTV7.js:9397:5)
users.vue:10 instance {}
users.vue:11 info native event handler

Server API Route

Nuxt 3 に含まれる Nitro サーバ(内部ではh3を利用)を利用することでサーバサイドの処理を行うことができます。Nitro サーバによりクライアント側からアクセス可能な API Route を作成することができます。サーバとしてデータベースへの接続も行えるのでデータベースを利用した環境でデータの取得と追加の動作確認も行います。

server ディレクトリ

API Route を作成するために server ディレクトリを作成してさらにその下に api ディレクトリを作成します。最初にクライアント側から/api/hello にアクセスすると”Hello World”を戻すシンプルな API Route を追加します。

api ディレクトリに hello.ts を作成して以下のコードを記述します。hello.ts ファイルでは defineEventHandler 関数を利用します。


export default defineEventHandler(() => 'Hello World');

server/api/ディレクトリにhello.tsファイルを作成した場合、クライアントからアクセスするURLは/api/helloとなります。

pagesのindex.vueファイルからuseFetch関数を利用して/api/helloにアクセスします。


<script setup>
const { data } = useFetch('/api/hello');
</script>
<template>
  <div>
    <h1>Main Page</h1>
    <h2>{{ data }}</h2>
  </div>
</template>

ブラウザから確認するとhello.tsで戻された”Hello World”がブラウザ上に表示されます。

Hello World
Hello World

データベースへの接続

Server API Route ではデータベースへの接続を行うことができます。データベースは SQLite データベースを利用します。SQLite データベースに直接接続することも可能ですが ORM の Prisma を利用して行います。Prisma ではデータモデルを定義することでデータベースを操作する際に SQL ではなくオブジェクトのメソッドを利用することができます。サーバからデータベースにアクセスする場合は必ず Prisma を経由することになります。

Prismaの設定

npmコマンドを利用してprismaライブラリのインストールを行います。


 % npm install prisma

Prisma用の設定ファイルを作成するためにnpx prisma initコマンドを実行します。


 % npx prisma init

コマンドを実行するとprismaディレクトリが作成されその中にschema.prismaファイルが作成されます。.envファイルも一緒に作成されます。

schema.prismaファイルはPrismaの設定ファイルでデフォルトではPostgreSQLのデータベース接続の設定が行われているのでsqliteに変更します。


generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite" //デフォルトではpostgresql
  url      = env("DATABASE_URL")
}

環境変数DABABASE_URLは.envファイルの中で定義されているのでPostgreSQLの設定からSQLiteの設定に変更します。SQLiteはファイルに情報を保存するのでdev.dbを指定します。


DATABASE_URL="file:./dev.db"

SQLite はリレーションデータベースなので通常はテーブル間のリレーションを設定しますが今回はテーブル Task のみ作成するので Task モデルを prisma.schema ファイルに定義します。モデルではテーブルの列名や型の設定を行います。id と task, completed で構成され id は自動設定され task は文字列(String), completede は Boolean(真偽値)に設定しています。


generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Task {
  id      Int      @id @default(autoincrement())
  task   String   
  completed    Boolean
}

モデルの定義が完了したらマイグレーションを行うことでDATABASE_URLで指定してdev.dbファイルがprismaディレクトリに作成されTaskモデルを元にTaskテーブルが作成されます。


% npx prisma migrate dev

コマンドを実行するとマイグレーションの名前を聞かれるので任意の名前をつけてください。コマンドが完了するとSQLiteデータベースにTaskテーブルが作成されます。

Prismaはデータベース管理のGUIのPrisma Studioも利用することができるのでnpx prisma studioコマンドで起動します。


 % npx prisma studio
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Prisma Studio is up on http://localhost:5555

デフォルトではlocalhostの5555ポートで起動するのでブラウザからhttp://localhost:5555にアクセスしてTaskテーブルを選択して”Add Record”ボタンをクリックしてデータを挿入してください。

タスクデータの挿入
タスクデータの挿入

これでPrismaの設定は完了です。

API Routeの追加

API Routeを追加するためにserver/api/task.tsファイルを作成します。task.tsファイルの中ではインストールしたprisma/clientからPrismaClientをインポートしてPrisma Clientのインスタンスを作成してtaskテーブルからfindManyメソッドで全データを取得しています。


import { PrismaClient } from '@prisma/client';

export default defineEventHandler(async () => {
  const prisma = new PrismaClient();
  const tasks = await prisma.task.findMany();

  return tasks;
});
データベースからデータを取得する際にはselect文を利用しますがPrismaを利用しているのでselect文ではなくfindManyというメソッドを利用しています。

task.tsファイルの作成後、クライアント側ではpages/index.vueファイルの中でuseFetchを利用して/api/taskにGETリクエストを送信します。GETリクエストの結果戻されるデータを利用してv-forディレクトリで展開しています。ブラウザ上にはSQLiteから取得したTaskが表示されます。


<script setup>
const { data: tasks } = useFetch('/api/task');
</script>
<template>
  <div>
    <h1>Main Page</h1>
    <ul>
      <li v-for="task in tasks" :key="task.id">{{ task.task }}</li>
    </ul>
  </div>
</template>

入力フォームの追加

API Routeを経由してデータベースからデータを取得することができたので次はデータベースへのデータの追加方法を確認していきます。

新たにタスクを追加できるようにpages/index.vueファイルに入力フォームを追加します。


<script setup>
const task = ref('');
const { data: tasks } = useFetch('/api/task');

const addTask = () => {
  console.log(task.value);
  task.value = '';
};
</script>
<template>
  <div>
    <h1>Main Page</h1>
    <ul>
      <li v-for="task in tasks" :key="task.id">{{ task.task }}</li>
    </ul>
    <form @submit.prevent="addTask">
      <div>
        <input v-model="task" />
      </div>
      <div>
        <button type="submit">タスクを登録</button>
      </div>
    </form>
  </div>
</template>

ref関数でリアクティブな変数taskを定義してinput要素のv-modelで指定します。ボタンをクリックするとsubmitイベントによりaddTask関数が実行されます。

入力フォームの追加
入力フォームの追加

POSTリクエストの送信

addTask関数の中で/api/taskに対してPOSTリクエストの送信を行います。


const addTask = () => {
  const { data } = useFetch('/api/task', {
    method: 'post',
    body: { task: task.value },
  });
  task.value = '';
};

GETリクエスト、POSTリクエストをAPI Routeの/api/taskで識別するためにeventオブジェクトを利用します。eventオブジェクトのnode.req.methodにリクエストの種類が含まれています。


import { PrismaClient } from '@prisma/client';

export default defineEventHandler(async (event) => {
  console.log(event.node.req.method);
  const prisma = new PrismaClient();
  const tasks = await prisma.task.findMany();

  return tasks;
});

API Routeの処理はサーバ側で実行されるためにnpm run devコマンドを実行したターミナルにGETリクエストでは”GET”、POSTリクエストでは”POST”と表示されます。

event.node.req.methodの値にリクエストの種類が入っていることがわかったのでそれらの値を利用して分岐を行います。POSTリクエストの中身はreadBody関数にeventを指定することで取得することができます。


import { PrismaClient } from '@prisma/client';

export default defineEventHandler(async (event) => {
  const prisma = new PrismaClient();
  if (event.node.req.method === 'GET') {
    const tasks = await prisma.task.findMany();

    return tasks;
  }
  if (event.node.req.method === 'POST') {
    const body = await readBody(event);
    return { body };
  }
});

ブラウザ上で入力フォームから文字を入力して”タスクを登録”ボタンをクリックするとbodyの中身が戻されます。

サーバ側ではbodyの中には{task:’入力した文字列’}が入っているのでbodyを利用してデータベースにタスクの追加を行います。createメソッドを利用してtaskとcompletedを設定しています。completedはデータ作成時はタスクが完了していないのでfalseにしています。データベース側で初期値をfalseにしている場合はcompletedプロパティの設定は必要ではありません。


if (event.node.req.method === 'POST') {
  const body = await readBody(event);
  const newTask = await prisma.task.create({
    data: {
      task: body.task,
      completed: false,
    },
  });
  console.log(newTask);
  return newTask;
}

入力フォームから”タスクを登録”をクリックするとデータベースへの追加が行えるようになります。データベース追加後にブラウザ上に追加したデータを表示させるためにrefresh関数を利用することで/api/taskへのGETリクエストが実行されるので即座に追加したデータがブラウザ上に表示されます。


const { data: tasks, refresh } = useFetch('/api/task');

const addTask = () => {
  const { data } = useFetch('/api/task', {
    method: 'post',
    body: { task: task.value },
  });
  refresh();
  task.value = '';
};

メソッド毎のファイル作成

task.tsファイル内でリクエストのメソッドの分岐を利用して処理をわけていましたがHTTPリクエスト毎にファイルをわけることもできます。GETリクエスト場合はtask.get.ts、POSTリクエストの場合はtask.post.ts、PUTやDELETEはそれぞれtask.put.ts, task.delete.tsとなります。

task.get.tsファイルを下記のように記述します。


import { PrismaClient } from '@prisma/client';

export default defineEventHandler(async () => {
  const prisma = new PrismaClient();
  const tasks = await prisma.task.findMany();

  return tasks;
});

task.posts.tsファイルを下記のように記述します。


import { PrismaClient } from '@prisma/client';

export default defineEventHandler(async (event) => {
  const prisma = new PrismaClient();

  const body = await readBody(event);
  const newTask = await prisma.task.create({
    data: {
      task: body.task,
      completed: false,
    },
  });
  return newTask;
});

task.tsファイルをtask.get.tsとtask.post.tsに分けても先ほどと同じように動作します。

routesディレクトリ

server ディレクトリに api ディレクトリを作成してファイルを作成すると API のエンドポイントは/api/ファイル名となりました。routes ディレクトリを作成してファイルを保存することで/ファイル名で API のエンドポイントを作成することができます。

hello.ts ファイルを routes ディレクトリを作成に作成すると/hello でアクセスすることができます。

middleware ディレクトリ

server ディレクトリの下に middleware ディレクトリを作成してファイルを作成することですべてのリクエストに対して API Routes にリクエストが入る前に処理を行うことができます。

例えばすべてのリクエストの送信先の URL を取得したい場合には api に middleware ディレクトリを作成しその下に log.ts ファイルを作成後に下記のコードを記述します。


export default defineEventHandler((event) =>
  console.log('New request: ' + event.node.req.url)
);

クライアントからAPIのエンドポイントにリクエストを送信するとnpm run devコマンドを実行したターミナルにアクセスを行ったURLが表示されます。

レンダリングモード

Nuxt 3 ではレンダリングモードをページ毎に変更することができます。

Nuxt ではレンダリングモードには CSR(Client Side Rendering), Universal Rendering, SSG(Static Site Genarators)などがあります。Nuxt のレンダリングモードのデフォルトは Universal Mode と呼ばれ CSR と SSR を組み合わせてレンダリングを行っています。

上記のレンダリングに加えて Nuxt 3 では Hybrid Rendering が登場し Route Rules を nuxt.cofig.ts 内で設定することでページ毎にレンダリングモードを変更することができます。この機能は執筆時のドキュメントによると active development(Route rules are still under active development, and subject to change.)のようなので本文書では ssr のオプションのみ利用して動作確認します。


export default defineNuxtConfig({
  routeRules: {
    // Static page generated on-demand, revalidates in background
    '/blog/**': { swr: true },
    // Static page generated on-demand once
    '/articles/**': { static: true },
    // Set custom headers matching paths
    '/_nuxt/**': { headers: { 'cache-control': 's-maxage=0' } },
    // Render these routes with SPA
    '/admin/**': { ssr: false },
    // Add cors headers
    '/api/v1/**': { cors: true },
    // Add redirect headers
    '/old-page': { redirect: '/new-page' },
    '/old-page2': { redirect: { to: '/new-page', statusCode: 302 } }
  }
})
ドキュメント上でactive developmentからproduction readyになり次第、別のオプションも動作確認を行う予定です。

動作確認の準備

動作確認を行うページ構成は以下のとおりです。

/pages/posts/index.vueファイルを作成して以下のコードを記述します。JSONPlaceHolderを利用してPosts一覧を取得しています。


<script setup>
const { data: posts } = await useFetch(
  'https://jsonplaceholder.typicode.com/posts/'
);
</script>
<template>
  <div>
    <h1>Posts一覧</h1>
    <ul>
      <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
  </div>
</template>

pagesフォルダにはindex.vueファイルを作成して以下のコードを記述しています。


<template>
  <h1>Main Page</h1>
</template>

layoutsフォルダのdefault.vueには/(ルート)と/postsのリンクを設定します。


<template>
  <div>
    <nav>
      <NuxtLink to="/">Home</NuxtLink>
      <NuxtLink to="/posts">Posts</NuxtLink>
    </nav>
    <slot />
  </div>
</template>

Universal Mode

Universal Modeはデフォルト状態で/postsに最初にアクセスした場合のネットワークタブを確認してサーバからどのようなデータが戻されているか確認します。

/postsにアクセスするとサーバ側で作成(SSR)されたHTMLがResponser Headersを見るとContext-typeのtext/htmlとして戻されていることが確認できます。このことから最初にアクセスした場合にはSSRによりサーバ側でHTMLが作成されてクライアントに戻されていることがわかります。

サーバ側でレンダリングされたHTMLが戻される
サーバ側でレンダリングされたHTMLが戻される

ページのソースを確認すると<div id=”__nuxt”>の要素の中にPostsの情報が含まれています。

/postsのページのソースを確認
/postsのページのソースを確認

SSR ではサーバ側で作成した HTML を受け取ってブラウザ上に表示するだけではなくクライアント側でもサーバ側と同様の処理を行います。クライアント側で行う処理のことを Hydration といいます。

Hydration という言葉はわかりずらいかもしれませんが非常にシンプルに説明するとサーバから受け取った HTML にイベントリスナーの設定を行いインタラクティブなページにすることです。SSR ではサーバから受け取った HTML を表示するためにすぐにページは表示されますが JavaScript の設定が完了していないため JavaScript の処理が完了するまでインタラクティブな操作を行うことができません。そのため Hydration という処理が行われます。

サーバ側と同じ処理を行うと先ほど説明しましたがブラウザのデベロッパツールの要素から script タグを見るとページを作成するために必要となる Post データも HTML とは別に受け取っていることが確認できます。クライアント側でもページを作成するために必要な JavaScript をダウンロードと受け取ったデータを利用してページを作成し要素にイベントリスナーやアプリケーションの状態など設定します。その後、最終的に JavaScript の処理が完了したインタラクティブなページへ更新されます。

scriptタグの中にデータを確認
scriptタグの中にデータを確認

次に/(ルート)にアクセスした後に/postsにアクセスを行います。ルートにアクセスするとサーバ側でレンダリングされたページが戻されます。

/にアクセスした際に戻されるデータとクリアボタン
/にアクセスした際に戻されるデータとクリアボタン

ページを移動した時に移動後のページは再度サーバから HTML を受け取るのかまたはサーバから受け取った JavaScript を利用してページをクライアント側で更新するのか確認するためにネットワークタブに表示されている”Clear”ボタンを利用して履歴をクリアします。

履歴をクリアして上部の Posts リンクを利用して/posts に移動します。最初に/posts した場合とは異なり、SSR で生成された HTML データを受け取るのではなくブラウザから直接 JSONPlaceHolder にアクセスを行いデータを取得していることがわかります。クライアント側で処理を行なっているので CSR(Client Side Rendering)と呼ばれます。

ブラウザからデータを取得
ブラウザからデータを取得

このようにデフォルトのUniversal ModeはSSRとCSRを組み合わせてレンダリングが行われることがわかります。

CSR Mode

Universal Modeの動作が理解できたので次はCSRモードへ変更します。レンダリングモードの変更はnuxt.config.tsファイルで行います。ページ毎に設定を行うことができるので/posts以下のページでssrの設定をfalseにします。falseにすることでCSRモードになります。


// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  routeRules: {
    '/posts/**': { ssr: false },
  },
});

設定変更後/postsにアクセスを行います。

先ほどと同様にサーバから戻されるページの中身をネットワークタブで見ても何も表示されていません。

ssr:falseに設定した場合にサーバから戻されるHTML
ssr:falseに設定した場合にサーバから戻されるHTML

ページのソースを見ても<div id=”__nuxt”></div>があるだけでPostに関するデータはありません。

ソースの確認
ソースの確認

Postsのデータはブラウザから直接JSONPlaceHolderにアクセスを行い取得しています。

ブラウザから直接JSONPlacdeHolderにアクセス
ブラウザから直接JSONPlacdeHolderにアクセス

Universal ModeとCSR Modeの違いを理解することができました。

まだ記事は完了していないので今後追加していく予定です。