Nuxt 3 — фреймворк на основе Vue

Nuxt 3 — это Vue-фреймворк, который закрывает задачи вне одного компонента: файловый роутинг, серверный рендеринг, загрузку данных и деплой. В этом уроке ты создашь проект, страницы с данными и layout-ы, а в конце соберёшь готовый сайт.

Уровень: средний 18 разделов 3–4 часа

1. Что такое Nuxt и чем он отличается от Vue

Vue — библиотека для создания интерфейсов внутри одной страницы. Nuxt берёт Vue и добавляет мета-фреймворк: серверный рендеринг, файловую маршрутизацию, автогенерацию модулей и единую конфигурацию. Грубо говоря, Vue отвечает на вопрос «как выглядит страница», Nuxt — «как живёт и отдаётся весь сайт».

       Vue            Nuxt
│  один компонент  │  целое приложение
│  SPA в браузере  │  SSR + SSG + SPA
│  ручной роутер   │  файловый роутинг
│  свой стек      │  модули и экосистема
Всё, что ты знаешь о Vue, работает в Nuxt без изменений: шаблоны, композиция, директивы. Nuxt лишь добавляет слой поверх.

2. Установка и создание проекта

Проект создаётся одной командой. Nuxt попросит выбрать пакетный менеджер и, при желании, включит TypeScript, ESLint и тестовый раннер.

npx nuxi@latest init my-site
cd my-site
npm run dev
# Nuxt слушает порт 3000
my-site/
├── app.vue              // корневой компонент
├── nuxt.config.ts       // конфигурация
├── pages/               // страницы и маршруты
├── layouts/             // общие каркасы страниц
├── components/          // переиспользуемые компоненты
├── composables/         // функции с состоянием
└── public/              // статические файлы
Папка components подключается автоматически: файл components/PostCard.vue доступен как <PostCard /> без единого import.

3. Страницы и роутинг

Каждый файл в pages/ становится маршрутом. Динамический сегмент — это папка или файл в квадратных скобках, а его значение доступно через useRoute().

<!-- pages/index.vue — главная / -->
<template>
  <h1>Главная страница</h1>
  <NuxtLink to="/blog">Перейти в блог</NuxtLink>
</template>
<!-- pages/blog/[slug].vue — /blog/любое-значение -->
<script setup>
const route = useRoute()
const slug = route.params.slug
</script>
<template>
  <h1>Пост: {{ slug }}</h1>
</template>
<!-- переходы без перезагрузки страницы -->
<NuxtLink to="/about">О нас</NuxtLink>
ФайлАдрес
pages/index.vue/
pages/about.vue/about
pages/blog/index.vue/blog
pages/blog/[slug].vue/blog/значение
pages/blog/[...all].vue/blog/любая/вложенность
Не называй страницы начинающимися с заглавной буквы файлами в pages/ — такие файлы Nuxt игнорирует как не-страницы. Для компонентов используй отдельные папки, а не страницы.

4. Layouts: общий каркас страниц

Layout задаёт рамку для группы страниц: шапку, подвал, сайдбар. Файлы в layouts/ подключаются автоматически, а специальная страница-слот <slot /> принимает содержимое страницы.

<!-- layouts/default.vue -->
<template>
  <div>
    <header>Сайт <span>IT Уроки</span></header>
    <main>
      <slot />
    </main>
    <footer>Все права защищены</footer>
  </div>
</template>
<!-- pages/login.vue — отдельный layout без шапки -->
<script setup>
definePageMeta({ layout: 'auth' })
</script>
Макрос definePageMeta добавляет странице метаданные: layout, заголовок, middleware и права доступа. Он доступен прямо в <script setup> без импорта.

5. Data fetching: useFetch и useAsyncData

Данные на страницах Nuxt загружаются не в onMounted, а на сервере через useFetch и useAsyncData. Такой код выполняется один раз на сервере, а в браузер приходит уже готовая страница.

<!-- pages/books.vue -->
<script setup>
const { data: books, pending, error } = await useFetch(
  'https://api.example.com/books'
)
</script>
<template>
  <p v-if="pending">Загрузка...</p>
  <p v-else-if="error">Ошибка загрузки</p>
  <ul v-else>
    <li v-for="book in books" :key="book.id">{{ book.title }}</li>
  </ul>
</template>
// useAsyncData для функции-источника
const { data: user } = await useAsyncData('profile',
  () => $fetch('/api/profile')
)
Первый аргумент useAsyncData — уникальный ключ кэша. При навигации с той же страницы данные переиспользуются, а не скачиваются заново.

6. Композаблы и автогенерация

Композабл — это функция, которая переиспользует логику с состоянием. В Nuxt файлы из composables/ автоматически импортируются во все компоненты и страницы.

// composables/useCart.js
export function useCountdown() {
  const seconds = ref(60)
  let timer

  function start() {
    timer = setInterval(() => seconds.value--, 1000)
  }

  onBeforeUnmount(() => clearInterval(timer))
  return { seconds, start }
}
<!-- использование без import -->
<script setup>
const { seconds, start } = useCountdown()
</script>
Правило: имя файла и имя возвращаемой функции должны совпадать (useCountdown в useCountdown.js). Только тогда сработает автогенерация имени.

7. Модули и экосистема Nitro

Модули расширяют Nuxt без ручной настройки: Tailwind, Pinia, SEO, иконки и всё остальное. Серверная часть сайта живёт в папке server/ на движке Nitro — там же можно делать API-маршруты.

npx nuxi module add pinia
npx nuxi module add tailwindcss
// server/api/hello.js — API-маршрут /api/hello
export default defineEventHandler(() => {
  return { message: 'Привет из Nitro!' }
})
// страница запрашивает собственный API
const { data } = await useFetch('/api/hello')
Популярный модульЗачем
@pinia/nuxtглобальное состояние
@nuxtjs/tailwindcssутилитарные стили
@nuxtjs/i18nмногоязычность
nuxt-sitemapгенерация sitemap.xml
@nuxt/imageоптимизация изображений
Nitro также умеет работать как серверный рантайм вне веба: написать бэкенд-логику и API для приложения можно внутри одного проекта Nuxt.

8. SSR, SSG и гибкий рендеринг

По умолчанию Nuxt рендерит страницы на сервере (SSR). Если включить ssr: false, получится классическая SPA, а для статического сайта используется команда nuxt generate.

// nuxt.config.ts
export default defineNuxtConfig({
  ssr: true,            // серверный рендеринг по умолчанию
  routeRules: {
    '/blog/**': { swr: 3600 },   // ISR: кэш на час
    '/old/**': { redirect: '/new' }
  }
})
npm run generate   // статический сайт в dist/
npm run preview     // проверка собранного сайта
Компоненты, использующие window или document, на сервере упадут. Такие библиотеки подключай через <ClientOnly></ClientOnly> или import.meta.client.

9. Middleware, SEO и метаданные

Middleware в Nuxt выполняет код перед рендером страницы — например, проверяет авторизацию. Заголовки и мета описание задаются через useSeoMeta или useHead.

// middleware/auth.js
export default defineNuxtRouteMiddleware(() => {
  const token = useCookie('token')
  if (!token.value) {
    return navigateTo('/login')
  }
})
// подключение middleware к странице
<script setup>
definePageMeta({ middleware: 'auth' })
useSeoMeta({
  title: 'Личный кабинет',
  description: 'Настройки профиля'
})
</script>
Композабл useHead умеет больше, чем useSeoMeta: Open Graph, канонические ссылки и скрипты. Для обычных сайтов хватает useSeoMeta.

10. Деплой и production-сборка

Сборку можно проверять локально, а публиковать одним нажатием на хостинг. Nuxt умеет деплой на Node-сервер, статический хостинг, а также serverless-провайдеров: Vercel, Netlify, Cloudflare и другие.

npm run build            // универсальная сборка
node .output/server/index.mjs   // запуск Node-сервера
// nuxt.config.ts — переопределение под провайдера
export default defineNuxtConfig({
  nitro: { preset: 'vercel' }
})
// Netlify: команда и папка в UI
// Build command: npm run generate
// Publish directory: dist
Для статического сайта достаточно npm run generate и папки dist. Для SSR-приложений нужен Node-хостинг или serverless-провайдер с поддержкой серверного рендеринга.
Перед деплоем всегда запускай npm run build локально — так окно ошибок или пропущенных импортов появится раньше, чем в панели хостинга.

11. Состояние приложения: useState и Pinia

В Nuxt 3 для реактивного состояния используйте useState — он работает и на сервере, и на клиенте, сохраняя одно состояние после гидрации. Для сложных хранилищ подключите Pinia.

// composables/useCounter.js
export function useCounter() {
  return useState('counter', () => 0);
}

// component
const counter = useCounter();
const inc = () => counter.value++; // SSR-безопасно
// stores/cart.js
export const useCartStore = defineStore('cart', {
  state: () => ({ items: [] }),
  getters: { total: (s) => s.items.reduce((a, i) => a + i.price, 0) },
  actions: { add(product) { this.items.push(product); } }
});
useState уникален по ключу — не создавайте двастора с одним ключом. Значение из сервера попадает в payload и не теряется при клиентской навигации.
Для Cookie-зависимого состояния используйте useCookie: оно синхронизируется с браузером самим Nuxt.

12. Middleware и защита маршрутов

Route middleware в Nuxt выполняется перед переходом и позволяет пускать на страницу только авторизованных пользователей. Определение — в файле middleware/, применение — на странице или глобально.

// middleware/auth.js
export default defineNuxtRouteMiddleware(async (to) => {
  const { loggedIn } = useAuth();
  if (!loggedIn.value) {
    return navigateTo('/login')
  }
});
// pages/account.vue
definePageMeta({ middleware: 'auth' });
Проверку авторизации дублируйте на сервере (API-роут, серверная функция). Маршрутный middleware — удобство UX, а не граница безопасности: иначе данные появятся в payload при первой загрузке.

13. Обработка ошибок и страница 404

Nuxt перехватывает ошибки и рендерит встроенную страницу. Собственные error.vue и 404.vue в папке app/ настраивают внешний вид и поведение.

// error.vue


createError({ statusCode: 404 }) в прикладном коде отправляет на страницу ошибки с нужным кодом и сработает даже без видимого исключения.

Ловите ошибки данных в useAsyncData: у обёртки есть поле error, по которому можно показать ретраи и частичный контент вместо падения всей страницы.

14. Переменные окружения и сборка

Переменные окружения задаются в файле .env или через runtimeConfig в nuxt.config. Секреты серверного рантайма нельзя отправлять клиенту — это ключевое правило конфигурации.

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    apiSecret: process.env.API_SECRET,  // только на сервере
    public: { apiBase: 'https://api.example.com' } // на клиенте
  }
});

// использование
const config = useRuntimeConfig();
const url = `${config.public.apiBase}/items`;
Всё в public попадает в клиентский бандл и виден в HTML. `runtimeConfig.public.apiSecret` — утечка секрета, которого быть не должно: держите секреты только на серверной стороне.

Сборку проверяйте командами nuxt build и nuxt generate (SSG). Для контейнеров используйте минимальный образ с установленным только рантаймом и сборкой заранее — так деплой занимает секунды, а не минуты.

15. Создание собственного модуля

Модуль — это способ распространить повторяемую логику: подключение библиотеки, middleware, композаблы и даже авто-регистрацию компонентов. Модули Nuxt существуют как пакеты, а функция defineNuxtModule даёт обратный вызов с доступом к конфигу, hooks и nuxt.options.

// nuxt-module.ts
import { defineNuxtModule, addComponent } from '@nuxt/kit';

export default defineNuxtModule({
  meta: { name: 'my-widget', version: '1.0.0' },
  setup(options, nuxt) {
    addComponent({ name: 'MyWidget', file: './components/MyWidget.vue' });
    nuxt.hook('app:created', (app) => {
      // перехват старта приложения
    });
  }
});

В options прилетает конфиг из nuxt.config, сборка выполняется на этапе kit. Так распространяют ui-библиотеки, аналитику и авторизацию — при этом потребитель модуля не знает внутренних деталей и просто добавляет зависимость.

Публикуйте модуль как обычный npm-пакет с полем module в package.json: Nuxt найдёт его автоматически при установке.
Не выполняйте в setup тяжёлых действий, зависящих от рантайма: фаза setup — это время сборки. Для логики приложения используйте плагины, подключаемые этим же модулем.

16. Тестирование Nuxt-приложения

Nuxt поддерживает тесты с Vitest и @nuxt/test-utils: можно проверять компоненты, композаблы и страницы. Юнит-тесты монтируют компонент изолированно, а интеграционные разворачивают настоящий Nitro-сервер, чтобы проверить рендеринг и API-роуты целиком.

import { describe, it, expect } from 'vitest';
import { mountSuspended } from '@nuxt/test-utils/runtime';
import ProductCard from '~/components/ProductCard.vue';

describe('ProductCard', () => {
  it('выводит цену', async () => {
    const wrapper = await mountSuspended(
      { template: '<ProductCard price="2990" />' },
      { plugins: ['~/plugins/toast'] }
    );
    expect(wrapper.text()).toContain('2990');
  });
});

Композаблы, использующие runtime-конфиг или useState, требуют обёртки в mountSuspended с реальным Nuxt-контекстом. E2E-сценарии гоняйте через Playwright на сгенерированной сборке, а в CI обязательно прогоняйте nuxi build: ошибки только дев-режима в проде не появятся.

Тесты, использующие useHead и useSeoMeta, проверяйте интеграционно: они применяются на этапе рендеринга страницы, а не в изолированном компоненте.

17. Производительность: размер и передача данных

Первое, что видит пользователь, — размер страницы. Оптимизируйте бандл: выносите тяжёлые библиотеки в optimizeDeps.exclude или динамические импорты, а графики и редакторы подключайте через lazy. Команда nuxi analyze показывает, кто раздувает сборку.

// вместо статического импорта — ленивая загрузка
const Chart = defineAsyncComponent(() => import('~/components/Chart.vue'));

Серверные компоненты и композаблы для цитат данных передают в разметку только результат, а не библиотеку. Гидрация превращает серверную разметку в интерактивную: чем меньше клиентского состояния, тем быстрее страница становится кликабельной — выносите на клиент только то, что действительно реагирует на пользователя.

МетодЭффект
defineAsyncComponentленивая загрузка тяжёлых компонентов
серверные компонентыотдают готовую разметку, а не JS-код на клиенте
useLazyFetchне блокирует первый рендер
Следите за передачей всего объекта в useState: большие структуры (например, весь каталог) целиком сериализуются в HTML — делите на части по запросу.

18. Подводные камни и частые ошибки

Авто-импорты из components/ и composables/ удобны, но таят неожиданности: имя с дефисом превращается в PascalCase в шаблоне, а коллизии с локальными переменными решаются в пользу локальных. При ошибке «implicit import» — добавьте явный import или переименуйте файл.

// так выглядит черновик «всё дублируется на клиенте»:
// данные грузятся в plugin, а не в server route
const items = useState('items');
if (process.client) {
  await $fetch('/api/items'); // продублирует запрос
}

// верно: server-side data via useAsyncData
const { data } = await useAsyncData('items',
  () => $fetch('/api/items'));

Гидрация требует идентичности серверной и клиентской разметки: случайные значения (Date, Math.random, id-ы) в рендеринге дадут предупреждение о mismatch. Кэшируйте ключи useAsyncData стабильно и помните про разное окружение сервер/клиент для runtimeConfig.

Покрытие cookie «HttpOnly» не мешает, но на клиенте их нет: читайте сессию только на сервере (useRequestHeaders), иначе авторизация сломается.

Что дальше

Ты разобрался с ключевыми механизмами Nuxt: файловый роутинг, layout-ы, загрузка данных и рендеринг. Закрепи материал, собрав сайт-визитку с блогом на три статьи. Глубина Vue понадобится в курсе Vue 3, а для общего состояния полезен будет разбор Pinia.

← Вернуться к курсам