| name | nuxt-helper |
| description | Nuxt.js/Vue.js開発を支援するスキル。コンポーネント作成、Composition API、Pinia、Vuetify、i18nのベストプラクティスを提供します。 |
Nuxt Helper Skill
Nature-Spotsプロジェクトにおける Nuxt.js/Vue.js 開発を効率化するスキルです。
機能
1. コンポーネント作成パターン
基本的なコンポーネント(Composition API)
<template>
<v-card>
<v-card-title>{{ title }}</v-card-title>
<v-card-text>
<slot />
</v-card-text>
</v-card>
</template>
<script setup lang="ts">
interface Props {
title: string
}
const props = defineProps<Props>()
</script>
フォームコンポーネント(v-model対応)
<template>
<v-text-field
:model-value="modelValue"
:label="label"
@update:model-value="emit('update:modelValue', $event)"
/>
</template>
<script setup lang="ts">
interface Props {
modelValue: string
label: string
}
interface Emits {
(e: 'update:modelValue', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
</script>
リスト表示コンポーネント(v-for最適化)
<template>
<div>
<!-- ✅ keyにユニークなIDを使用 -->
<spot-card
v-for="spot in spots"
:key="spot.id"
:spot="spot"
@click="handleClick(spot)"
/>
</div>
</template>
<script setup lang="ts">
import type { Spot } from '~/types'
interface Props {
spots: Spot[]
}
const props = defineProps<Props>()
const handleClick = (spot: Spot) => {
// 処理
}
</script>
2. Composition API のベストプラクティス
Composableの作成
// composables/useSpotSearch.ts
import { ref, computed } from 'vue'
import type { Spot } from '~/types'
export const useSpotSearch = () => {
const query = ref('')
const spots = ref<Spot[]>([])
const isLoading = ref(false)
const error = ref<Error | null>(null)
const filteredSpots = computed(() => {
if (!query.value) return spots.value
return spots.value.filter((spot) =>
spot.name.toLowerCase().includes(query.value.toLowerCase())
)
})
const search = async (searchQuery: string) => {
isLoading.value = true
error.value = null
try {
const response = await $fetch<Spot[]>('/api/v1/spots', {
params: { q: searchQuery },
})
spots.value = response
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
}
return {
query,
spots,
isLoading,
error,
filteredSpots,
search,
}
}
使用例
<script setup lang="ts">
const { query, filteredSpots, isLoading, search } = useSpotSearch()
watch(query, (newQuery) => {
if (newQuery.length >= 2) {
search(newQuery)
}
})
</script>
3. Pinia ストアの設計
ストアの作成(Composition Store)
// stores/spot.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Spot } from '~/types'
export const useSpotStore = defineStore('spot', () => {
// State
const spots = ref<Spot[]>([])
const currentSpot = ref<Spot | null>(null)
const isLoading = ref(false)
// Getters
const favoriteSpots = computed(() =>
spots.value.filter((spot) => spot.isFavorite)
)
const spotCount = computed(() => spots.value.length)
// Actions
const fetchSpots = async () => {
isLoading.value = true
try {
const response = await $fetch<Spot[]>('/api/v1/spots')
spots.value = response
} finally {
isLoading.value = false
}
}
const setCurrentSpot = (spot: Spot) => {
currentSpot.value = spot
}
const addSpot = (spot: Spot) => {
spots.value.push(spot)
}
const reset = () => {
spots.value = []
currentSpot.value = null
}
return {
// State
spots,
currentSpot,
isLoading,
// Getters
favoriteSpots,
spotCount,
// Actions
fetchSpots,
setCurrentSpot,
addSpot,
reset,
}
})
ストアの使用(storeToRefs)
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useSpotStore } from '~/stores/spot'
const spotStore = useSpotStore()
// ✅ リアクティビティを保持
const { spots, isLoading } = storeToRefs(spotStore)
// ✅ アクションは直接使用
const { fetchSpots, addSpot } = spotStore
onMounted(() => {
fetchSpots()
})
</script>
4. Vuetify コンポーネントの活用
レスポンシブレイアウト
<template>
<v-container>
<v-row>
<!-- モバイル: 12列(全幅)、タブレット: 6列(半分)、デスクトップ: 4列(1/3) -->
<v-col
v-for="spot in spots"
:key="spot.id"
cols="12"
sm="6"
md="4"
>
<spot-card :spot="spot" />
</v-col>
</v-row>
</v-container>
</template>
フォームバリデーション
<template>
<v-form ref="formRef" @submit.prevent="handleSubmit">
<v-text-field
v-model="name"
:label="$t('spot.form.name')"
:rules="[rules.required, rules.maxLength(50)]"
/>
<v-textarea
v-model="description"
:label="$t('spot.form.description')"
:rules="[rules.required]"
/>
<v-btn type="submit" :loading="isSubmitting">
{{ $t('common.submit') }}
</v-btn>
</v-form>
</template>
<script setup lang="ts">
const formRef = ref()
const name = ref('')
const description = ref('')
const isSubmitting = ref(false)
const rules = {
required: (v: string) => !!v || 'Required',
maxLength: (max: number) => (v: string) =>
!v || v.length <= max || `Max ${max} characters`,
}
const handleSubmit = async () => {
const { valid } = await formRef.value.validate()
if (!valid) return
isSubmitting.value = true
try {
// API呼び出し
} finally {
isSubmitting.value = false
}
}
</script>
ダイアログ管理
<template>
<div>
<v-btn @click="dialog = true">Open Dialog</v-btn>
<v-dialog v-model="dialog" max-width="600">
<v-card>
<v-card-title>{{ $t('dialog.title') }}</v-card-title>
<v-card-text>
<!-- コンテンツ -->
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="dialog = false">
{{ $t('common.cancel') }}
</v-btn>
<v-btn color="primary" @click="handleConfirm">
{{ $t('common.confirm') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script setup lang="ts">
const dialog = ref(false)
const handleConfirm = () => {
// 処理
dialog.value = false
}
</script>
5. i18n(多言語対応)
コンポーネント内での使用
<template>
<div>
<!-- テンプレートで直接使用 -->
<h1>{{ $t('spot.title') }}</h1>
<!-- プレースホルダー付き -->
<p>{{ $t('spot.count', { count: spots.length }) }}</p>
<!-- 複数形対応 -->
<p>{{ $tc('spot.review', reviewCount) }}</p>
</div>
</template>
<script setup lang="ts">
// Composition API で使用
const { t, locale } = useI18n()
const message = computed(() => t('welcome.message', { name: userName.value }))
const changeLanguage = (lang: 'ja' | 'en') => {
locale.value = lang
}
</script>
動的キー(注意が必要)
// ❌ NG: 完全に動的なキーは避ける
const message = $t(variableKey)
// ✅ OK: 制限された選択肢から選ぶ
const statusKey = `status.${status}` as const
const message = $t(statusKey)
// より安全: switch文で明示的に
const getMessage = (status: string) => {
switch (status) {
case 'active':
return $t('status.active')
case 'inactive':
return $t('status.inactive')
default:
return $t('status.unknown')
}
}
6. TypeScript型定義
コンポーネントProps/Emits
// types/components.ts
export interface SpotCardProps {
spot: Spot
showActions?: boolean
}
export interface SpotCardEmits {
(e: 'click', spot: Spot): void
(e: 'favorite', spotId: string): void
}
API レスポンス型
// types/api.ts
export interface ApiResponse<T> {
data: T
message?: string
}
export interface PaginatedResponse<T> {
data: T[]
total: number
page: number
perPage: number
}
export interface SpotResponse {
id: string
name: string
description: string
latitude: number
longitude: number
createdAt: string
}
7. パフォーマンス最適化
遅延ローディング
// nuxt.config.ts で設定
export default defineNuxtConfig({
components: {
dirs: [
{
path: '~/components',
pathPrefix: false,
},
],
},
})
<!-- コンポーネントは自動インポート -->
<template>
<!-- 重いコンポーネントは遅延ロード -->
<LazySpotMap v-if="showMap" :spots="spots" />
</template>
Computed vs Watch
// ✅ OK: 派生値には computed を使う
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// ✅ OK: 副作用には watch を使う
watch(searchQuery, async (newQuery) => {
if (newQuery.length >= 2) {
await fetchResults(newQuery)
}
})
// ❌ NG: watch で派生値を作らない
watch([firstName, lastName], ([first, last]) => {
fullName.value = `${first} ${last}` // computedを使うべき
})
不要な再レンダリングの防止
<script setup lang="ts">
// ✅ OK: コンポーネント外で定数を定義
const STATIC_OPTIONS = ['Option 1', 'Option 2', 'Option 3']
// ❌ NG: setup内で毎回生成される
const options = ['Option 1', 'Option 2', 'Option 3']
</script>
8. Nuxt.js 固有機能
useFetch / useAsyncData
<script setup lang="ts">
// ✅ SSRで自動的にデータ取得
const { data: spots, pending, error, refresh } = await useFetch<Spot[]>(
'/api/v1/spots',
{
key: 'spots-list',
// リアクティブなパラメータ
query: computed(() => ({ genre: selectedGenre.value })),
}
)
// クライアントサイドのみ
onMounted(() => {
refresh()
})
</script>
ミドルウェア
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const authStore = useAuthStore()
if (!authStore.isLoggedIn) {
return navigateTo('/login')
}
})
<!-- pages/mypage.vue -->
<script setup lang="ts">
definePageMeta({
middleware: 'auth',
})
</script>
プロジェクト固有の規約
ディレクトリ構造
components/
├── beforeLogin/ # 認証前のコンポーネント
├── loggedIn/ # 認証後のコンポーネント
├── spot/ # スポット関連
├── ui/ # 再利用可能なUI
└── user/ # ユーザー関連
composables/ # Composition関数
stores/ # Piniaストア
pages/ # ファイルベースルーティング
types/ # TypeScript型定義
命名規則
- コンポーネント: PascalCase(
UserProfile.vue) - Composables:
use+ PascalCase(useAuth.ts) - Stores: 機能名 +
Store(authStore,spotStore)
よく使うコマンド
# 開発サーバー起動
cd front && yarn dev
# 型チェック
yarn type-check
# Lint + Format
yarn check
# ビルド
yarn build
ベストプラクティスまとめ
- Composition API を使用(Options APIは使わない)
- 型定義 を明示的に(
any禁止) - i18n でハードコードテキストを避ける
- Pinia でグローバル状態を管理
- storeToRefs でリアクティビティを保持
- computed で派生値、watch で副作用
- useFetch でSSR対応のデータ取得
- 遅延ローディング で初期ロードを最適化