Nuxt3をはじめる①

目的
・実務でNuxt3を扱うことがある
・AIの力に助けられながらなんとかタスクをこなせているが正直よくわかっていない→ちゃんと理解したい
・0からフロントエンドアプリケーションが構築できるようになりたい
・学習記録を残しておきたい

Nuxt3とは?
Vue.jsでよく利用されるモジュール類をひとまとめにしたフレームワークがNuxt.js(略してNuxt)。
Nuxt3では、内部のコードをすべてTypeScriptで実装されており、Nuxtプロジェクト自体もTypeScriptによるコーディングをサポートしている。また、非同期でのデータ取得や、SSR機能なども追加されている。
主な機能
機能 | 内容 |
---|---|
ファイルシステムルーティング | フォルダ構成がそのままルーティング |
レイアウト | 画面の共通レイアウトを作れる |
オートインポート | 明示的にインポートをしなくてもコンポーネントが利用できる |
ミドルウェア | 画面遷移する前に処理を挟める |
レンダリングモードの切替 | 4種類のレンダリングモードをパスごとに設定できる |

Vue3について
Vue.jsは2014年にEvan You氏によってリリースされたフレームワーク。軽量軽快に動作するフレームワークとして人気を集め、JavaScriptコード上の変数とDOM要素が自動的に連動するリアクティブシステムを採用している。2020年9月にVue3としてメジャーアップデートが実施された。
Vue3では、TypeScriptを採用したり、Viteというビルドツールを採用したりと様々なアップデートが行われた。
Vueの主な特徴
- VueRouter:シングルページアプリケーションを手軽に実現できる
- Pinia:コンポーネント間で横断でデータを管理できるステート管理モジュール
- Vitest, VueTestUtils:Vueのユニットテストを容易にできる

Nuxtプロジェクトを始める
Nuxtのプロジェクトを新たに作るにはnpx nuxi init <プロジェクト名>
コマンドを使う。
# プロジェクトの新規作成
npx nuxi init nuxt-app
...
# プロジェクトのディレクトリに移動してパッケージをインストール
cd nuxt-app
npm install
# 開発用nuxtサーバーの起動(http://localhost:3000/)
npm run dev
※ ブラウザを直接起動する
npm run dev -- -o

ディレクトリ構成
Nuxtプロジェクトを新規作成した直後のディレクトリ構成。
ディレクトリorファイル | 内容 |
---|---|
.nuxt/ | Nuxtアプリケーションが格納されたディレクトリ |
node_modules/ | 依存パッケージが格納されたディレクトリ |
public/ | Web上に公開するファイルを格納するためのディレクトリ |
server/ | APIサーバー処理を行うファイルを格納するためのディレクトリ |
.gitignore | Git対象外ファイルを管理するファイル |
.npmrc | npmコマンドの設定ファイル |
app.vue | メインのNuxtコンポーネントファイル |
nuxt.config.js | Nuxtアプリケーションの設定ファイル |
package-lock.json | npmの依存関係に関する設定ファイル |
package.json | npmに関する設定ファイル |
README.md | いわゆるりーどみーファイル |
tsconfig.json | TypeScriptに関する設定ファイル |

開発を進めていった際のディレクトリ構成
ディレクトリorファイル | 内容 |
---|---|
.nuxt/ | 同上 |
.output/ | デプロイ用のファイル一式が格納されたディレクトリ |
assets/ | 画像やCSSファイルといったファイル類を格納するためのディレクトリ |
components/ | コンポーネントファイルを格納するためのディレクトリ |
composables/ | コンポーザブル定義ファイルを格納するためのディレクトリ |
layouts/ | レイアウト用のコンポーネントファイルを格納するためのディレクトリ |
middleware/ | ミドルウェアファイルを格納するためのディレクトリ |
modules/ | 独自のモジュールを格納するためのディレクトリ |
node_modules/ | 依存パッケージが格納されたディレクトリ |
pages/ | ルーティングに必要なコンポーネントファイルを格納するためのディレクトリ |
plugins/ | プラグインを格納するためのディレクトリ |
public/ | 同上 |
server/ | 同上 |
utils/ | ヘルパー関数ファイルなどを格納するためのディレクトリ |
.env | 環境変数を設定するファイル |
.gitignore | 同上 |
.npmrc | 同上 |
app.vue | 同上 |
nuxt.config.js | 同上 |
package-lock.json | 同上 |
package.json | 同上 |
README.md | 同上 |
tsconfig.json | 同上 |

.vueファイルについて
Nuxtプロジェクトはコンポーネント単位でアプリケーションを構成し、各コンポーネントは*.vue
ファイルで表現される。*.vue
では、単一のファイルにHTML、CSS、JavaScriptといった要素をまとめて記述する。これを単一ファイルコンポーネント(Signle File Components)、略してSFCという形で表現する。
.vueファイルの基本構成
<script setup lang="ts">
// JavaScriptやTypeScriptコードを記述するスクリプトブロック
</script>
<template>
// 画面を構成するHTMLやコンポーネントを記述するテンプレートブロック
</template>
<style>
// 見た目を構成するCSSなどを記述するスタイルブロック
</style>
スクリプトブロック内で定義された変数やメソッドはテンプレートブロック内で呼び出すことが可能。
テンプレートブロック内で変数を表示する際には、マスタッシュ構文を利用する必要があり、{{}}
で囲むことで変数として扱われる。

app.vue
Nuxtアプリケーションを実行時に一番最初に呼び出されるコンポーネントがapp.vue
となる。
すべてのページやコンポーネントは本ファイルを大元としながら構成されていくことになる。
プロジェクト作成直後のapp.vue
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtWelcome />
</div>
</template>

SFCを記述する際の基本構文
Vueの機能には、一定の条件で記述された変数の値が変化すると、それに合わせて自動的に画面の表示内容も変化するリアクティブシステムという仕組みが存在する
このリアクティブな変数を定義するためには、ref()
関数やcomputed()
関数を利用する。
リアクティブな変数を定義する ref()
リアクティブ(値が変化したら画面の表示が自動的に更新される状態)な変数を定義したい場合はref()
関数を用いる。
const 変数名 = ref(初期値);
リアクティブな変数の値を変更する際は直接に値を変更することができず、.value
プロパティへアクセスする必要がある。
計算結果などをリアクティブな値したい computed()
定義済みのリアクティブな変数を用いてなんらかの計算結果を行った値をリアクティブな変数として保持したい場合はcomputed()
関数を用いる。computed()
関数は引数に関数を渡すことになっており、この際Vueにおいてはアロー関数を用いることが推奨されている。
const 変数名 = computed(
(): 戻り値のデータ型 => {
// なんらかの計算処理など
return 戻り値;
}
);
computed()
関数は計算結果を自動でリアクティブにして保持したい場合に利用する。
参照用の変数として利用するケースがほとんど。
オブジェクトをまとめてリアクティブにする reactive()
リアクティブなオブジェクトを定義したい場合はreactive()
関数を用いる。reactive()
によって定義されたリアクティブなテンプレート変数は、そのオブジェクトのプロパティという形でアクセスする(scriptブロック内で.valueは不要)。
const 変数名 = reactive(オブジェクト)
リアクティブ変数の変換を監視する watchEffect(), watch()
リアクティブな変数の変化を監視し、変化した際に任意の処理を実行させるウォッチャーという機能が存在する。ウォッチャーにはwatchEffect()
関数とwatch()
関数の2種類が存在する。
watchEffect()
では、アロー関数内に記述したリアクティブな変数を自動で検知して、それらが変化する度にアロー関数内に記述した処理が実行される。明示的にどの変数を監視するかを指定しなくて済む一方、制御がしずらいといった特徴を持つ。初回実行は自動で行われる。
watchEffect(
(): void => {
// リアクティブ変数を用いた処理
}
);
`watch()`では、明示的に指定した監視対象のリアクティブ変数が変化する度に、アロー関数内に記述した処理が実行される。`watchEffect`とは違い、自動でアロー関数内に記述したすべてのリアクティブ変数を監視対象とするわけではないため、制御がしやすいといった特徴を持つ。初回実行は`immediate`引数によって制御可能。
```ts
// newVal: 監視対象の変化後の値, oldVal: 監視対象の変化前の値
watch(監視対象のリアクティブ変数, (newVal: データ型, oldVal: データ型): void => {
// 監視対象が変化した際に実行される処理
},
{immediate: true} // 初回起動時に呼び出す場合はtrue、falseの場合は省略可
);
watchEffect()
は新旧値に関する情報をもたないため、値がどれだけ変化したか?といったチェック処理には向いていない。

ref()関数
コード
<script setup lang='ts'>
const count = ref(0) // 0で初期化されたリアクティブな変数
// リアクティブな変数を更新する処理
function increment() {
count.value++ // scriptブロックでリアクティブな変数を操作する際は.valueプロパティで参照する
}
</script>
<template>
<p>現在の数値: {{ count }}</p>
<button @click="increment">加算する</button>
</template>

computed()関数
コード
<script setup lang='ts'>
const count = ref(0)
const doubleCount = computed( // countの2倍した値を常にリアクティブにしたい
(): number => {
count.value * 2
}
);
function increment() {
count.value++
}
</script>
<template>
<p>元の数値: {{ count }}</p>
<p>2倍にした値: {{ doubleCount }}</p>
<button @click="increment">加算する</button>
</template>

reactive()関数
コード
<script setup lang='ts'>
// ユーザー情報をまとめてリアクティブな値としたい
const user = reactive({
name: "Tanaka",
age: 20
})
function incrementAge() {
user.age++
}
</script>
<template>
<p>Name: {{ user.name }}</p>
<p>Age: {{ user.age }}</p>
<button @click="incrementAge">年齢加算</button>
</template>

watchEffect()関数
コード
<script setup lang="ts">
const count = ref(0)
watchEffect() = {
console.log(`countの値が変化しました: ${count.value}`)
})
function increment() {
count.value++
}
</script>
<template>
<p>{{ count }}</p>
<button @click="increment">加算する</button>
</template>

weatch()関数
コード
<script setup lang="ts">
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`countの値が変化しました: ${oldVal} → ${newVal}`)
})
function increment() {
count.value++
}
</script>
<template>
<p>{{ count }}</p>
<button @click="increment">加算する</button>
</template>

テンプレートブロックの基本構文
動的に値を割り当てる v-bind
v-bind
はテンプレート側から子コンポーネントへのプロパティ(props)やHTML属性に動的に値を割り当てるときに使うディレクティブ。一般的にはv-bind
を省略して:
だけを使って記述することが多い。
v-bind:属性名 = テンプレート変数
// もしくは
:属性名 = テンプレート変数
イベント発火時の処理を設定する v-on
v-on
はテンプレート内でクリックやキー入力といったイベントが発火した際の処理を指定するときに使うディレクティブ。一般的にはv-on
を省略して@イベント名
の形式で記述することが多い。
v-on:イベント名 = "イベント発火時に実行するメソッド名"
// もしくは
@イベント名 = "イベント発火時に実行するメソッド名"
双方向データバインディングを行う v-model
v-model
はフォーム要素やカスタムコンポーネントとの間で双方向のデータバインディングを行うためのディレクティブ。テンプレートの入力フォームなどにv-model
を利用すると、ユーザーの入力内容をリアクティブな変数で常に同期することができるようになる。
v-model="テンプレート変数"
条件分岐を行う v-if
v-if
は条件に応じて要素の挿入・削除を切り替えるためのディレクティブ。条件がtrueとなった場合にレンダリングされる。
v-if="条件"
v-else-if="条件"
v-else
表示・非表示を切り替える v-show
v-show
は条件に応じて要素の表示・非表示を切り替えるためのディレクティブ。v-if
は条件がfalseである場合、そもそもDOM上に存在しないが、v-show
の場合は要素のdisplayCSSを切り替えるだけとなり、falseの場合でもDOM上には存在する。
使い分けは、v-if
は表示・非表示のレンダリングコストが都度かかるため、表示・非表示が画面表示段階で決まっているような要素に利用する。v-show
は初回のでレンダリングコストはかかるが表示・非表示の切替コストが低いため、画面表示後に頻繁に表示切替が発生するような要素に利用する。
v-show="条件"
ループ制御 v-for
v-for
はリストや配列を繰り返し表示するためのディレクティブ。
v-bind:keyディレクティブを記述し、Vueが要素を識別できるようにしてあげることが推奨されている。
v-for="エイリアス" in "ループ対象"

v-bind
コード
<script setup lang="ts">
const message = ref('Hello')
</script>
<template>
<ChildComponent v-bind:msg="message" />
</template>
<script setup lang="ts">
const props = defineProps<{
msg: string
}>()
</script>
<template>
<p>{{ props.msg }}</p>
</template>

v-on
コード
<script setup lang="ts">
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button v-on:click="increment">カウントアップ</button>
<p>Count:{{ count }}</p>
</template>

v-model
コード
<script setup lang="ts">
const message = ref('')
</script>
<template>
// 入力された内容が自動でmessage変数に反映される
// コード内でmessage.valueを書き換えれば自動でinputの値も変わる
<input v-model="message" type="text">
<p>入力内容: {{ message }}</p>
</template>
コード(コンポーネント)
<script setup lang="ts">
const userInput = ref('')
</script>
<template>
<ChildComponent v-model="userInput">
<p>入力値: {{ userInput }}</p>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
function handleInput(e: Event) {
const target = e.target as HTMLInputElement
emit('update:modelValue', target.value)
}
</script>
<template>
// 内部の入力値が変わったらhandleInput()で親に通知する
<input :value="props.modelValue" @input="handleInput" type="text" />
</template>

v-if
コード
<script setup lang="ts">
const isLoggedIn = ref(false)
</script>
<template>
<div v-if="isLoggedIn">
ログイン中
</div>
<div v-else>
未ログイン
</div>
</template>

v-show
コード
<script setup lang="ts">
const isVisible = ref(true)
</script>
<template>
<div v-show="isVisible">
表示・非表示の切替
</div>
</template>

v-for
コード
<script setup lang="ts">
interface Task {
id: number
title: string
done: boolean
}
const tasks = ref<Task[]>([
{ id: 1, title: "タスク1”, done: false },
{ id: 2, title: "タスク2”, done: true },
])
</script>
<template>
<ul>
<li v-for="task in tasks" :key="task.id">
{{ task.title }}
</li>
</ul>
</template>

親から子へデータを渡す仕組み Props
Propsは親のコンポーネントから子のコンポーネントへデータを渡すための仕組み。Propsを定義する際にはdefineProps()
関数を利用する。
基本的にデータの受け渡しは親→子の流れとなり、子から親へ何かを通知したり、値を変更したい場合は後述するEmitを活用する。
Propsによって子に渡されたデータは読み取り専用となるため注意が必要。
// 型定義を用いる場合
interface Props {
プロップス名: データ型
}
const props = defineProps<Props>();
// 直接定義する場合
const props = defineProps<{
プロップス名: データ型
}>()

Propsの定義
コード
<script setup lang="ts">
interface Props {
title: string
subtitle: string
}
const props = defineProps<Props>();
</script>
<template>
<h1>{{ props.title }}</h1>
<h2>{{ props.subtitle }}</h2>
</template>
<script setup lang="ts">
const mainTitle = ref('タイトル')
const subTitle = ref('サブタイトル')
</script>
<template>
<ChildComponemt :title="mainTitle" :subtitle="subTitle" />
</template>

子から親へ通知する仕組み Emit
Emitは子のコンポーネントから親のコンポーネントへ通知を送るための仕組み。ここで言う通知とは、子コンポーネントがイベントを発火させることで、親コンポーネントはそのイベントを受け取り、何かしらの処理を実施することを意味する。Propsではデータの流れが親→子へ一方向であるため、Emitを使って親へ子が情報を伝える。
簡単に表現すると、子が親のメソッドを実行する仕組み。
// typeキーワードを用いる場合
type Emits {
(event: 'イベント名', 引数: データ型, …)
}
const emit = defineEmits<Emits>();
// 直接定義する場合
const emit = defineEmits<{
(event: 'イベント名', 引数:データ型)
}>()

Emitの定義
コード
<scirpt setup lang="ts">
type Emits = {
(event: 'child-cliclked', payload: string)
}
const emit = defineEmits<Emits>();
function handleClick() {
emit('child-clicked', 'Hello from child')
}
</scirpt>
<template>
<button @click="handleClick">子コンポーネントのボタン</button>
</template>
<scirpt setup lang="ts">
function handleChildClicked(message: string) {
console.log('子でクリックが発生', message)
}
</scirpt>
<template>
<ChildComponent @child-clicked="handleChildClicked" />
</template>

ステート管理
ステート管理とは、アプリケーション内で行うデータの状態(state)をアプリケーション全体で一元的に管理し、各コンポーネントはそこからデータを取得したり、更新したりといったことをする仕組み。
アプリケーションの規模が大きくなり、コンポーネント数が増えてくると、どのデータを誰が管理してどう更新するか?が複雑になるため、その複雑さを解消する仕組みとして提供される。
useState()関数を使ったステート管理
スクリプトブロック内でuseState()
関数を実行すると、そのデータはステートとして管理され、コンポーネントを跨いで利用できるリアクティブデータとして保持される。
定義されたステートを利用する際もuseState()
関数を利用し、戻り値としてステートの値を受け取る。useState()
で取得したデータは、リアクティブな変数であるため、データにアクセスしたり更新を行う際は.value
プロパティを利用する。
// ステートの定義①
useState<データ型>(
ステート名,
(): データ型 => {
ステートの初期値生成処理
return ステートの初期値;
}
);
// ステートの利用
const ステートを格納する変数 = useState<データ型>(ステート名);
// ステート定義の方法②
const ステートを格納する変数 = userState<データ型>(ステート名, () => 初期値)
Piniaを使ったステート管理
PiniaはVue3向けのステート管理ライブラリで、Nuxt3でも標準で利用することが可能。軽量かつシンプルなことが特徴。
npm install pinia @pinia/nuxt
export default defineNuxtConfig({
modules: [
'@pinia/nuxt'
],
pinia: {
autoImports: [
// 自動インポートの設定
'defineStore'
],
},
})

useState()
コード
<script setup lang="ts">
export const useUserState = () => {
// "user"というステートを定義
const user = useState<{ name: string }>('user', () => ({ name: '' }))
function setUserName(name: string) {
user.value.name = name
}
return { user, setUserName }
}
</script>
<script setup lang="ts">
import { useUserState } from '~/comosables/useUser'
const { setUserName } = useUserState()
function onLogin() {
setUserName("Hoge")
}
</script>
<template>
<button @click="onLogin">ログイン</button>
</template>
<script setup lang="ts">
import { useUserState } from '~/comosables/useUser'
const { user } = useUserState()
</script>
<template>
<p>こんにちは、{{ user.name }}さん</p>
</template>

Pinia
コード
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// state: ストアで保持するリアクティブな値
state: () => ({
count: 0
}),
// getters: stateから派生する値。Vueのcomputedと似たイメージ
gettrers: {
doubleCount: (state) => state.count * 2,
},
// actions: stateを更新したりするメソッド
actions: {
increment() {
this.count++
},
},
})
<script setup lang="ts">
import { useCounterStore } from '~/stores/counter'
// ストアを呼び出す
const counterStore = useCounterStore()
// 値やメソッドへアクセス
function increment() {
counterStore.increment()
}
</script>
<template>
<div>
<p>Count: {{ counterStore.count }}</p>
<p>DoubleCount: {{ counterStore.doubleCount }}</p>
<button @click="increment">+1</button>
</div>
</template>