はじめに
Nuxtは、Vue.jsをベースにしたフルスタックフレームワークです。
Vue.jsの使いやすさはそのままに、ファイルベースのルーティング・サーバーサイドレンダリング(SSR)・自動インポートなど、本格的なWebアプリケーション開発に必要な機能が最初から揃っています。
この記事では、カウンターアプリを題材にしながら、Nuxtの基本的な考え方と使い方を丁寧に解説します。
1. Nuxtとは
NuxtはVue.jsアプリケーションの開発をより快適にするためのフレームワークです。
「設定より規約(Convention over Configuration)」の思想に基づいており、ファイルを決まった場所に置くだけで多くの機能が自動的に有効になります。
NuxtとVue.jsの違い
| 機能 | Vue.js単体 | Nuxt |
|---|---|---|
| ルーティング | 手動で設定 | ファイル構成から自動生成 |
| SSR/SSG | 自分でセットアップ | 設定一つで切り替え可能 |
| importの記述 | 毎回書く必要あり | 自動インポートで省略可能 |
| メタタグ管理 | 別ライブラリが必要 |
useHead で簡単に設定 |
| サーバーAPI | 別途用意が必要 |
server/api/ に置くだけ |
レンダリングモード
Nuxtは用途に合わせてレンダリング方式を選べます。
| モード | 説明 | 向いている用途 |
|---|---|---|
| SSR | サーバーでHTMLを生成 | ECサイト・ニュースサイト |
| SSG | ビルド時にHTMLを生成 | ブログ・ドキュメントサイト |
| SPA | クライアントで描画 | 管理画面・社内ツール |
2. 環境セットアップ
Node.js(v18以上推奨)がインストールされていることを確認してください。
node -v
プロジェクトの作成
nuxi コマンドを使って新しいプロジェクトを作成します。
npx nuxi@latest init my-counter-app
cd my-counter-app
npm install
npm run dev
ブラウザで http://localhost:3000 を開くと、Nuxtアプリが起動しています。
3. プロジェクト構成を理解する
Nuxtプロジェクトには、役割ごとに決まったディレクトリがあります。
それぞれのディレクトリを正しく使うことで、Nuxtの機能を最大限に活用できます。
my-counter-app/
├── pages/ # ページコンポーネント(URLと対応)
├── components/ # 再利用可能なUIコンポーネント
├── composables/ # 再利用可能なロジック(useXxx)
├── server/
│ └── api/ # サーバーサイドAPI
├── public/ # 静的ファイル(画像など)
├── assets/ # CSSや画像(ビルド対象)
├── app.vue # アプリ全体のルートコンポーネント
└── nuxt.config.ts # Nuxtの設定ファイル
ポイント: これらのディレクトリは存在しなければ機能しません。
必要になったタイミングで自分で作成してください。
4. ページとルーティング
Nuxtでは pages/ ディレクトリにファイルを作るだけで、URLが自動的に生成されます。
Vue Routerの設定を手動で書く必要はありません。
ファイルとURLの対応
pages/
├── index.vue → /
├── about.vue → /about
└── counter/
└── index.vue → /counter
pages/index.vue の例
<template>
<div>
<h1>トップページ</h1>
<!-- NuxtLink でページ間を移動します -->
<NuxtLink to="/counter">カウンターへ</NuxtLink>
</div>
</template>
<NuxtLink> はVue Routerの <RouterLink> に相当するNuxt組み込みのコンポーネントです。
通常の <a> タグと異なり、ページ全体をリロードせずに移動できます。
動的ルート(パラメーター付きURL)
ファイル名を [id].vue のように角括弧で囲むと、動的なURLを作れます。
pages/
└── counter/
└── [id].vue → /counter/1, /counter/abc など
<script setup>
// URLのパラメーターを取得
const route = useRoute();
const id = route.params.id;
</script>
<template>
<p>カウンターID:{{ id }}</p>
</template>
5. コンポーネントを作る
components/ ディレクトリに .vue ファイルを作成すると、自動的にインポートされ、どのページからでもすぐに使えます。
components/CountDisplay.vue
<script setup>
// propsの定義
const props = defineProps({
count: {
type: Number,
required: true,
},
});
// countの値に応じて色を返す
const color = computed(() => {
if (props.count > 0) return "#22c55e";
if (props.count < 0) return "#ef4444";
return "#333";
});
</script>
<template>
<p class="count" :style="{ color }">
{{ count }}
</p>
</template>
<style scoped>
.count {
font-size: 5rem;
font-weight: bold;
text-align: center;
transition: color 0.3s;
}
</style>
ページで使う
<!-- pages/counter/index.vue -->
<template>
<div>
<!-- importなしでそのまま使えます -->
<CountDisplay :count="42" />
</div>
</template>
サブディレクトリのコンポーネント
サブディレクトリに配置したコンポーネントは、ディレクトリ名がプレフィックスになります。
components/
└── Counter/
├── Display.vue → <CounterDisplay />
└── Buttons.vue → <CounterButtons />
6. 自動インポートを理解する
Nuxtの大きな特徴の一つが自動インポートです。
VueのAPIやcomposables、コンポーネントを import 文なしで使えます。
自動インポートされるもの
<script setup>
// 通常のVue.jsでは以下が必要ですが…
// import { ref, computed, watch } from 'vue'
// import { useRoute } from 'vue-router'
// Nuxtでは書かなくてもそのまま使えます
const count = ref(0); // ✅ import不要
const route = useRoute(); // ✅ import不要
const doubled = computed(() => count.value * 2); // ✅ import不要
</script>
| 自動インポートの対象 | 例 |
|---|---|
| VueのComposition API |
ref, computed, watch, onMounted など |
| Vue Routerのcomposables |
useRoute, useRouter
|
| Nuxt組み込みcomposables |
useState, useHead, useFetch など |
composables/ のファイル |
自分で作った useCounter など |
7. reactiveとrefでstateを管理する
NuxtはVueがベースとなっているため、Vue.jsの ref と reactive を使ってリアクティブなデータを管理します。
ref:プリミティブ値に使う
文字列・数値・真偽値などの単純な値には ref を使います。
<script setup>
const count = ref(0); // 数値
const name = ref("Nuxt"); // 文字列
const isVisible = ref(true); // 真偽値
// .value でアクセス・更新(template内では不要)
count.value++;
console.log(count.value); // 1
</script>
<template>
<!-- template内では .value 不要 -->
<p>{{ count }}</p>
</template>
reactive:オブジェクトに使う
複数の値をまとめて管理する場合は reactive が便利です。
<script setup>
const state = reactive({
count: 0,
step: 1,
history: [],
});
// .value 不要でアクセスできます
state.count += state.step;
state.history.push(state.count);
</script>
<template>
<p>カウント:{{ state.count }}(ステップ:{{ state.step }})</p>
</template>
8. はじめてのカウンター
学んだことを組み合わせて、シンプルなカウンターページを作ってみましょう。
<!-- pages/counter/index.vue -->
<script setup>
const count = ref(0);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
function reset() {
count.value = 0;
}
</script>
<template>
<div class="container">
<h1>カウンター</h1>
<!-- カウントの表示 -->
<p
class="count"
:class="{
positive: count > 0,
negative: count < 0,
}"
>
{{ count }}
</p>
<!-- ボタン群 -->
<div class="buttons">
<button @click="decrement">−</button>
<button @click="reset">リセット</button>
<button @click="increment">+</button>
</div>
</div>
</template>
<style scoped>
.container {
text-align: center;
padding: 2rem;
}
.count {
font-size: 5rem;
font-weight: bold;
color: #333;
transition: color 0.3s;
}
.count.positive { color: #22c55e; }
.count.negative { color: #ef4444; }
.buttons {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 1rem;
}
button {
padding: 0.5rem 1.5rem;
font-size: 1.2rem;
cursor: pointer;
}
</style>
computed でメッセージを自動計算する
computed を使うと、stateの変化に応じて自動的に更新される値を定義できます。
<script setup>
const count = ref(0);
// countが変わるたびに自動的に再計算されます
const message = computed(() => {
if (count.value > 10) return "10を超えました!";
if (count.value < 0) return "マイナスです";
return "通常範囲です";
});
</script>
<template>
<p>{{ count }} — {{ message }}</p>
</template>
9. useStateでstateを共有する
Vue.jsの ref は同じコンポーネント内でしか使えませんが、Nuxtの useState を使うと複数のコンポーネントやページ間でstateを共有できます。
<!-- components/Counter/Display.vue -->
<script setup>
// どのコンポーネントからも同じ "count" にアクセスできます
const count = useState("count", () => 0);
</script>
<template>
<p class="count">{{ count }}</p>
</template>
<!-- components/Counter/Buttons.vue -->
<script setup>
// 同じキー "count" を指定すると同じstateが共有されます
const count = useState("count", () => 0);
function increment() { count.value++; }
function decrement() { count.value--; }
function reset() { count.value = 0; }
</script>
<template>
<div class="buttons">
<button @click="decrement">−</button>
<button @click="reset">リセット</button>
<button @click="increment">+</button>
</div>
</template>
<!-- pages/counter/index.vue -->
<template>
<div class="container">
<!-- どちらも同じstateを参照します -->
<CounterDisplay />
<CounterButtons />
</div>
</template>
ポイント:
useStateの第一引数はキー名です。
同じキー名を指定すれば、異なるコンポーネントでも同じstateを共有できます。
10. composableに切り出す
カウンターのロジックが複雑になってきたら、composables/ に切り出して再利用しやすくしましょう。
composablesのファイル名は慣習として use で始めます。
composables/useCounter.ts
// composables/useCounter.ts
export function useCounter(initialValue = 0) {
const count = useState("count", () => initialValue);
const history = useState<number[]>("counter-history", () => []);
function increment(step = 1) {
count.value += step;
history.value.push(count.value);
}
function decrement(step = 1) {
count.value -= step;
history.value.push(count.value);
}
function reset() {
count.value = initialValue;
history.value = [];
}
// countの絶対値(表示用)
const absCount = computed(() => Math.abs(count.value));
// カウントの状態
const status = computed(() => {
if (count.value > 0) return "positive";
if (count.value < 0) return "negative";
return "zero";
});
return {
count: readonly(count), // 外部から直接変更できないようにする
history: readonly(history),
absCount,
status,
increment,
decrement,
reset,
};
}
コンポーネントで使う
<!-- pages/counter/index.vue -->
<script setup>
// importなしで使えます(自動インポート)
const { count, status, increment, decrement, reset } = useCounter(0);
</script>
<template>
<div class="container">
<p class="count" :class="status">{{ count }}</p>
<div class="buttons">
<button @click="decrement()">−</button>
<button @click="reset">リセット</button>
<button @click="increment()">+</button>
</div>
</div>
</template>
11. カウンターを完成させる
最終的なカウンターアプリのファイル構成と完全なコードをまとめます。
ファイル構成
my-counter-app/
├── composables/
│ └── useCounter.ts # カウンターのロジック
├── components/
│ └── Counter/
│ ├── Display.vue # 数値の表示
│ ├── Buttons.vue # 操作ボタン
│ └── History.vue # 操作履歴
└── pages/
└── counter/
└── index.vue # カウンターページ
components/Counter/Display.vue
<script setup>
const { count, status } = useCounter();
</script>
<template>
<p class="count" :class="status">{{ count }}</p>
</template>
<style scoped>
.count {
font-size: 5rem;
font-weight: bold;
transition: color 0.3s;
}
.zero { color: #6b7280; }
.positive { color: #22c55e; }
.negative { color: #ef4444; }
</style>
components/Counter/Buttons.vue
<script setup>
const { increment, decrement, reset } = useCounter();
</script>
<template>
<div class="buttons">
<button class="btn btn-minus" @click="decrement()">−</button>
<button class="btn btn-reset" @click="reset">リセット</button>
<button class="btn btn-plus" @click="increment()">+</button>
</div>
</template>
<style scoped>
.buttons {
display: flex;
gap: 1rem;
justify-content: center;
}
.btn {
width: 64px;
height: 64px;
font-size: 1.5rem;
border: none;
border-radius: 50%;
cursor: pointer;
}
.btn-minus { background: #ef4444; color: #fff; }
.btn-plus { background: #22c55e; color: #fff; }
.btn-reset { background: #6b7280; color: #fff; font-size: 0.9rem; }
</style>
components/Counter/History.vue
<script setup>
const { history } = useCounter();
</script>
<template>
<div v-if="history.length" class="history">
<h3>操作履歴</h3>
<ul>
<li v-for="(val, i) in history" :key="i">
{{ i + 1 }}回目:{{ val }}
</li>
</ul>
</div>
</template>
pages/counter/index.vue
<script setup>
// ページのメタ情報を設定します
useHead({
title: "カウンター",
meta: [{ name: "description", content: "Nuxt製カウンターアプリ" }],
});
</script>
<template>
<div class="container">
<CounterDisplay />
<CounterButtons />
<CounterHistory />
</div>
</template>
<style scoped>
.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
padding: 3rem 1rem;
min-height: 100vh;
}
</style>
12. まとめ
この記事で学んだNuxtの基本をおさらいしましょう。
| 概念 | 役割 | 例 |
|---|---|---|
| pages/ | URLと対応するページ |
pages/counter/index.vue → /counter
|
| components/ | 再利用可能なUI部品 | <CounterDisplay /> |
| composables/ | 再利用可能なロジック | useCounter() |
| 自動インポート |
import 文を省略 |
ref, computed, 自作composable |
| useState | ページをまたぐstate共有 | useState("count", () => 0) |
| useHead | ページのメタ情報設定 | useHead({ title: "..." }) |
Vue.jsとNuxtのどちらを選ぶ?
| 要件 | 選択 |
|---|---|
| シンプルなSPAを作りたい | Vue.js単体で十分です |
| SEOが必要(ニュース・EC) | Nuxt(SSR)がおすすめです |
| ビルド時に静的化したい | Nuxt(SSG)がおすすめです |
| APIも一緒に管理したい | Nuxt(server/api/)がおすすめです |
次のステップ
-
useFetch/useAsyncData— 外部APIからのデータ取得 -
server/api/— サーバーサイドAPIの作成 -
Nuxt Modules —
@nuxtjs/tailwindcssなどのモジュール活用 - Pinia — より本格的なグローバルstate管理
- nuxt generate — 静的サイト生成(SSG)
参考リンク
この記事がNuxt学習の第一歩になれば幸いです。