Nuxt 3 — фреймворк на основе Vue
Nuxt 3 — это Vue-фреймворк, который закрывает задачи вне одного компонента: файловый роутинг, серверный рендеринг, загрузку данных и деплой. В этом уроке ты создашь проект, страницы с данными и layout-ы, а в конце соберёшь готовый сайт.
1. Что такое Nuxt и чем он отличается от Vue
Vue — библиотека для создания интерфейсов внутри одной страницы. Nuxt берёт Vue и добавляет мета-фреймворк: серверный рендеринг, файловую маршрутизацию, автогенерацию модулей и единую конфигурацию. Грубо говоря, Vue отвечает на вопрос «как выглядит страница», Nuxt — «как живёт и отдаётся весь сайт».
Vue Nuxt
│ один компонент │ целое приложение
│ SPA в браузере │ SSR + SSG + SPA
│ ручной роутер │ файловый роутинг
│ свой стек │ модули и экосистема2. Установка и создание проекта
Проект создаётся одной командой. Nuxt попросит выбрать пакетный менеджер и, при желании, включит TypeScript, ESLint и тестовый раннер.
npx nuxi@latest init my-site
cd my-site
npm run dev
# Nuxt слушает порт 3000my-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>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 | оптимизация изображений |
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: distnpm 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); } }
});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' });- name — именованные проверки для отдельных страниц.
- global — middleware в файле с суффиксом .global.js.
- in функциях — navigateTo возвращает Promise, не забывайте return.
13. Обработка ошибок и страница 404
Nuxt перехватывает ошибки и рендерит встроенную страницу. Собственные error.vue и 404.vue в папке app/ настраивают внешний вид и поведение.
// error.vue
{{ error.statusCode }} — {{ error.message }}
Ловите ошибки данных в 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`;Сборку проверяйте командами 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-библиотеки, аналитику и авторизацию — при этом потребитель модуля не знает внутренних деталей и просто добавляет зависимость.
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: ошибки только дев-режима в проде не появятся.
- Дата-атрибуты data-testid — для стабильных локаторов e2e.
- Мокайте HTTP-клиенты и fetch для предсказуемых данных в тестах.
- Покрывайте главные маршруты, auth-флоу и обработку ошибок прежде всего.
17. Производительность: размер и передача данных
Первое, что видит пользователь, — размер страницы. Оптимизируйте бандл: выносите тяжёлые библиотеки в optimizeDeps.exclude или динамические импорты, а графики и редакторы подключайте через lazy. Команда nuxi analyze показывает, кто раздувает сборку.
// вместо статического импорта — ленивая загрузка
const Chart = defineAsyncComponent(() => import('~/components/Chart.vue'));Серверные компоненты и композаблы для цитат данных передают в разметку только результат, а не библиотеку. Гидрация превращает серверную разметку в интерактивную: чем меньше клиентского состояния, тем быстрее страница становится кликабельной — выносите на клиент только то, что действительно реагирует на пользователя.
| Метод | Эффект |
|---|---|
| defineAsyncComponent | ленивая загрузка тяжёлых компонентов |
| серверные компоненты | отдают готовую разметку, а не JS-код на клиенте |
| useLazyFetch | не блокирует первый рендер |
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.
- Не укладывайте window/document в код модулей — на сервере их нет.
- Сравнивайте ответы useAsyncData с пометкой server на разных страницах: ключи должны отличаться.
- При миграции с Vue 3 проверяйте жизнь lifecycle: у Nuxt есть свои (page:start, app:mounted).
Что дальше
Ты разобрался с ключевыми механизмами Nuxt: файловый роутинг, layout-ы, загрузка данных и рендеринг. Закрепи материал, собрав сайт-визитку с блогом на три статьи. Глубина Vue понадобится в курсе Vue 3, а для общего состояния полезен будет разбор Pinia.