Nuxt.js — это meta-framework для Vue.js, который предоставляет server-side rendering (SSR), static site generation (SSG) и мощную систему маршрутизации на основе файлов. Он упрощает создание производительных, SEO-дружественных приложений на Vue. В этом руководстве рассматривается разработка на Nuxt 3 с точки зрения senior-разработчика.
Почему Nuxt.js
Nuxt предоставляет существенные преимущества:
- SEO-дружественность: server-side rendering для поисковых систем
- Производительность: автоматическое разделение кода и оптимизация
- Маршрутизация на основе файлов: конфигурация router не требуется
- Auto-imports: components и composables доступны везде
- Full-stack: API routes и server middleware встроены
Начало работы
Создание проекта
npx nuxi@latest init my-app
cd my-app
npm install
npm run dev
Структура проекта
my-app/
├── .nuxt/ # Сгенерированные файлы (.gitignore)
├── assets/ # Нескомпилированные ресурсы (Sass, изображения)
├── components/ # Компоненты Vue (auto-imported)
├── composables/ # Composable-функции Vue (auto-imported)
├── layouts/ # Layouts страниц
├── middleware/ # Route middleware
├── pages/ # Маршруты на основе файлов
├── plugins/ # Плагины Vue
├── public/ # Статические файлы (отдаются как есть)
├── server/
│ ├── api/ # API routes
│ └── middleware/ # Server middleware
├── nuxt.config.ts # Конфигурация Nuxt
└── app.vue # Корневой компонент
Маршрутизация на основе файлов
Базовые маршруты
Создайте файлы в директории pages/:
pages/
├── index.vue # /
├── about.vue # /about
├── contact.vue # /contact
└── users/
├── index.vue # /users
└── [id].vue # /users/:id (dynamic)
Динамические маршруты
pages/users/[id].vue:
<script setup>
const route = useRoute()
const userId = route.params.id
// Fetch user data
const { data: user } = await useFetch(`/api/users/${userId}`)
</script>
<template>
<div>
<h1>{{ user?.name }}</h1>
<p>{{ user?.email }}</p>
</div>
</template>
Вложенные маршруты
pages/
└── users/
├── index.vue # /users
├── [id].vue # /users/:id
└── [id]/
├── posts.vue # /users/:id/posts
└── settings.vue # /users/:id/settings
Получение данных
Composable useFetch
<script setup>
// Basic fetch
const { data, pending, error, refresh } = await useFetch('/api/posts')
// With options
const { data: posts } = await useFetch('/api/posts', {
query: { limit: 10, page: 1 },
headers: { 'Authorization': 'Bearer token' },
transform: (response) => response.data,
})
// Lazy loading (doesn't block navigation)
const { data: comments, pending } = useLazyFetch('/api/comments')
// Watch for changes and refetch
const page = ref(1)
const { data } = await useFetch('/api/posts', {
query: { page },
watch: [page],
})
</script>
<template>
<div>
<div v-if="pending">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else>
<li v-for="post in posts" :key="post.id">
{{ post.title }}
</li>
</ul>
</div>
</template>
useAsyncData для пользовательской логики
<script setup>
const { data: user } = await useAsyncData('user', async () => {
// Custom async logic
const profile = await $fetch('/api/profile')
const settings = await $fetch('/api/settings')
return {
...profile,
settings,
}
})
</script>
Управление состоянием с Pinia
Установка и настройка
npm install @pinia/nuxt
Добавьте в nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
})
Создание store
stores/counter.ts:
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
// Состояние
const count = ref(0)
// Getters
const doubleCount = computed(() => count.value * 2)
// Actions
function increment() {
count.value++
}
async function fetchCount() {
const { data } = await useFetch('/api/count')
count.value = data.value
}
return { count, doubleCount, increment, fetchCount }
})
Использование store в компоненте
<script setup>
const counter = useCounterStore()
</script>
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">+1</button>
</div>
</template>
Server routes (API)
Создание API endpoint
server/api/users/index.get.ts:
export default defineEventHandler(async (event) => {
// Доступ к query parameters
const query = getQuery(event)
const { page = 1, limit = 10 } = query
// Получение данных из базы данных или внешнего API
const users = await fetchUsers({ page, limit })
return {
data: users,
meta: { page, limit },
}
})
server/api/users/[id].get.ts:
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const user = await fetchUser(id)
if (!user) {
throw createError({
statusCode: 404,
message: 'User not found',
})
}
return user
})
Обработчик POST-запроса
server/api/users/index.post.ts:
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Валидация
if (!body.email || !body.name) {
throw createError({
statusCode: 400,
message: 'Name and email required',
})
}
// Создание пользователя
const user = await createUser(body)
setResponseStatus(event, 201)
return user
})
Layouts
Layout по умолчанию
layouts/default.vue:
<template>
<div class="layout">
<header>
<nav>
<NuxtLink to="/">Home</NuxtLink>
<NuxtLink to="/about">About</NuxtLink>
</nav>
</header>
<main>
<slot />
</main>
<footer>
© {{ new Date().getFullYear() }}
</footer>
</div>
</template>
<style scoped>
.layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
</style>
Пользовательский layout
layouts/admin.vue:
<template>
<div class="admin-layout">
<aside class="sidebar">
<!-- Admin navigation -->
</aside>
<main class="content">
<slot />
</main>
</div>
</template>
Использование на странице:
<script setup>
definePageMeta({
layout: 'admin',
})
</script>
Middleware
Route middleware
middleware/auth.ts:
export default defineNuxtRouteMiddleware((to, from) => {
const user = useAuthUser()
if (!user.value) {
return navigateTo('/login')
}
})
Применение к странице:
<script setup>
definePageMeta({
middleware: ['auth'],
})
</script>
Global middleware
middleware/analytics.global.ts:
export default defineNuxtRouteMiddleware((to, from) => {
// Отслеживание просмотра страницы
trackPageView(to.path)
})
SEO и meta
Meta для страницы
<script setup>
useHead({
title: 'My Page Title',
meta: [
{ name: 'description', content: 'Page description for SEO' },
{ property: 'og:title', content: 'My Page Title' },
{ property: 'og:image', content: '/og-image.png' },
],
link: [
{ rel: 'canonical', href: 'https://example.com/page' },
],
})
</script>
Динамические meta
<script setup>
const { data: post } = await useFetch(`/api/posts/${route.params.id}`)
useHead({
title: () => post.value?.title || 'Loading...',
meta: [
{ name: 'description', content: () => post.value?.excerpt },
],
})
</script>
Конфигурация окружения
Runtime config
nuxt.config.ts:
export default defineNuxtConfig({
runtimeConfig: {
// Приватные ключи (только на server)
apiSecret: process.env.API_SECRET,
// Публичные ключи (доступны на client)
public: {
apiBase: process.env.API_BASE_URL || '/api',
},
},
})
Доступ в коде:
<script setup>
const config = useRuntimeConfig()
// Server-side only
// config.apiSecret
// Client and server
const apiBase = config.public.apiBase
</script>
Развёртывание
Static generation
npm run generate
Генерирует статический HTML в .output/public/.
Развёртывание SSR
npm run build
Разверните директорию .output/. Для Node.js:
node .output/server/index.mjs
Serverless (AWS Lambda)
nuxt.config.ts:
export default defineNuxtConfig({
nitro: {
preset: 'aws-lambda',
},
})
Сборка и развёртывание:
npm run build
# Развёртывание .output/ в AWS Lambda
Ключевые выводы
- Маршрутизация на основе файлов: соглашения вместо конфигурации
- Auto-imports: меньше шаблонного кода, выше продуктивность
- useFetch для данных: встроенное кэширование и поддержка SSR
- Server routes: full-stack в одном проекте
- Pinia для состояния: простое, типобезопасное управление состоянием
- Гибкое развёртывание: SSR, SSG или гибрид
Nuxt 3 радикально упрощает разработку на Vue.js, одновременно предоставляя enterprise-grade возможности для SEO, производительности и full-stack функциональности.