← Курс/Введение в Nuxt 3#245 из 257+30 XP

Введение в Nuxt 3

Что такое Nuxt и зачем он нужен

**Nuxt** — это мета-фреймворк над Vue. Если Vue даёт строительные блоки, то Nuxt — готовую архитектуру:

| Vue | Nuxt |

|-----|------|

| Нужно настраивать Vue Router | Файловый роутинг из коробки |

| Нужно настраивать SSR вручную | SSR/SSG/CSR — единая конфигурация |

| Импорты вручную | Авто-импорт компонентов и composables |

| Нет серверного кода | server/ директория для API |

| Нужно настраивать TypeScript | TypeScript из коробки |

Структура проекта Nuxt 3

my-nuxt-app/
├── pages/              # Файловый роутинг
│   ├── index.vue       # → /
│   ├── about.vue       # → /about
│   └── users/
│       ├── index.vue   # → /users
│       └── [id].vue    # → /users/42 (динамический)
├── components/         # Авто-импортируемые компоненты
│   └── MyButton.vue    # Используется без импорта
├── composables/        # Авто-импортируемые composables
│   └── useCounter.ts   # Доступен во всех компонентах
├── server/             # Серверный код (Node.js/Nitro)
│   └── api/
│       └── users.get.ts  # GET /api/users
├── layouts/            # Layouts страниц
│   └── default.vue
├── middleware/         # Навигационные guard├── plugins/            # Vue-плагины
├── public/             # Статические файлы
├── app.vue             # Корневой компонент
└── nuxt.config.ts      # Конфигурация

Авто-импорт

Nuxt автоматически импортирует:

// В pages/index.vue — НЕТ НУЖДЫ писать:
import { ref, computed } from 'vue'       // ← не нужно
import { useRoute } from 'vue-router'     // ← не нужно
import { useMyComposable } from '@/composables/useMyComposable'  // ← не нужно

// Всё доступно напрямую:
const count = ref(0)
const route = useRoute()
const { data } = useMyComposable()

server/ директория

Nuxt использует **Nitro** — серверный движок. Файлы в server/api/ создают API-маршруты:

// server/api/users.get.ts
export default defineEventHandler(async (event) => {
  const users = await db.query('SELECT * FROM users')
  return users  // автоматически сериализуется в JSON
})

// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  return await db.findUser(id)
})

useRuntimeConfig

Разделение конфигурации на сервер и клиент:

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // Только серверная сторона (секретные ключи)
    databaseUrl: process.env.DATABASE_URL,
    apiSecret: process.env.API_SECRET,

    // Публичная — доступна на клиенте тоже
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api',
      appName: 'My App',
    }
  }
})
// В компоненте
const config = useRuntimeConfig()
console.log(config.public.apiBase)  // '/api' — доступно везде
// config.databaseUrl — только на сервере!

Режимы рендеринга

// nuxt.config.ts
export default defineNuxtConfig({
  ssr: true,       // SSR по умолчанию
  // ssr: false,   // CSR (SPA режим)

  // Hybrid rendering (разные режимы для разных маршрутов):
  routeRules: {
    '/':        { prerender: true },   // SSG — статически при сборке
    '/blog/**': { swr: 3600 },         // ISR — кэш на 1 час
    '/admin/**': { ssr: false },       // CSR — без SSR
    '/api/**':  { cors: true },        // CORS для API
  }
})

Примеры

Симуляция авто-импорта Nuxt и файловой системы маршрутов — как Nuxt читает структуру папок

// Симулируем, как Nuxt генерирует маршруты из файловой структуры
// и как работает авто-импорт.

// --- Файловый роутинг ---
function generateRoutes(fileTree) {
  const routes = []

  function processDir(entries, prefix = '') {
    for (const [name, value] of Object.entries(entries)) {
      if (typeof value === 'object') {
        // Директория
        const segmentName = name.startsWith('[') && name.endsWith(']')
          ? ':' + name.slice(1, -1)   // [id] → :id (динамический параметр)
          : name
        processDir(value, prefix + '/' + segmentName)
      } else {
        // Файл .vue
        const file = name.replace('.vue', '')
        let routePath

        if (file === 'index') {
          routePath = prefix || '/'   // index.vue → родительский маршрут
        } else {
          const seg = file.startsWith('[') && file.endsWith(']')
            ? ':' + file.slice(1, -1)
            : file
          routePath = prefix + '/' + seg
        }

        routes.push({
          path: routePath,
          file: (prefix || '') + '/' + name,
          isDynamic: routePath.includes(':'),
        })
      }
    }
  }

  processDir(fileTree)
  return routes.sort((a, b) => {
    // Статические маршруты раньше динамических
    if (a.isDynamic === b.isDynamic) return a.path.localeCompare(b.path)
    return a.isDynamic ? 1 : -1
  })
}

// --- Авто-импорт ---
class AutoImporter {
  constructor() {
    this._imports = new Map()
    this._modules = new Map()
  }

  // Регистрируем модуль (аналог файла в composables/)
  registerModule(name, exports) {
    this._modules.set(name, exports)
    // Авто-импорт всех экспортов
    for (const [key, value] of Object.entries(exports)) {
      this._imports.set(key, { from: name, value })
    }
    console.log(`[AutoImport] Зарегистрирован модуль "${name}": ${Object.keys(exports).join(', ')}`)
  }

  // Получить авто-импортируемое значение
  resolve(name) {
    const imp = this._imports.get(name)
    if (!imp) throw new Error(`[AutoImport] "${name}" не найден. Проверьте composables/ или components/`)
    return imp.value
  }

  // Список всех авто-импортов
  list() {
    return [...this._imports.entries()].map(([name, { from }]) => ({ name, from }))
  }
}

// --- useRuntimeConfig симуляция ---
function createRuntimeConfig(config) {
  const isServer = typeof window === 'undefined'

  return {
    ...config.public,                    // публичное — всегда
    ...(isServer ? config : {}),         // приватное — только сервер
    public: config.public,
  }
}

// === Демо: файловый роутинг ===
console.log('=== Файловый роутинг ===')
const fileStructure = {
  'index.vue': 'component',      // → /
  'about.vue': 'component',      // → /about
  'blog': {
    'index.vue': 'component',    // → /blog
    '[slug].vue': 'component',   // → /blog/:slug
  },
  'users': {
    'index.vue': 'component',    // → /users
    '[id]': {
      'index.vue': 'component',  // → /users/:id
      'edit.vue': 'component',   // → /users/:id/edit
    }
  },
  'admin': {
    'index.vue': 'component',    // → /admin
    'settings.vue': 'component', // → /admin/settings
  }
}

const routes = generateRoutes(fileStructure)
routes.forEach(r => {
  console.log(`  ${r.isDynamic ? '[dynamic]' : '[static] '} ${r.path.padEnd(25)}${r.file}`)
})

// === Демо: авто-импорт ===
console.log('\n=== Авто-импорт ===')
const autoImport = new AutoImporter()

// Регистрируем как Nuxt регистрирует composables/
autoImport.registerModule('composables/useCounter', {
  useCounter: () => ({ count: 0, increment: () => {} })
})
autoImport.registerModule('composables/useFetch', {
  useFetch: async (url) => ({ data: null, pending: true }),
  useAsyncData: async (key, fn) => ({ data: await fn(), pending: false }),
})

// Используем без явного import
const useCounter = autoImport.resolve('useCounter')
const counter = useCounter()
console.log('useCounter():', counter)

console.log('\nВсе авто-импорты:')
autoImport.list().forEach(({ name, from }) =>
  console.log(`  ${name} (из ${from})`)
)

// === Демо: runtimeConfig ===
console.log('\n=== runtimeConfig ===')
const runtimeConfig = createRuntimeConfig({
  databaseUrl: 'postgres://...',  // только сервер
  apiSecret: 'super-secret',       // только сервер
  public: {
    apiBase: '/api',
    appName: 'My Nuxt App',
  }
})
console.log('public.apiBase:', runtimeConfig.apiBase)
console.log('public.appName:', runtimeConfig.appName)