← Курс/Nuxt: получение данных#247 из 257+25 XP

Nuxt: получение данных

Три способа получения данных в Nuxt

Nuxt предоставляет специальные composables, оптимизированные для SSR:

1. useFetch() — основной инструмент

// Автоматически:
// - Работает на сервере и клиенте
// - Дедуплицирует запросы (один запрос для SSR + гидрации)
// - Реактивен к изменениям URL

const { data, pending, error, refresh } = await useFetch('/api/users')

// С параметрами
const { data: user } = await useFetch(() => `/api/users/${userId.value}`, {
  watch: [userId],   // перезапрос при изменении
  lazy: false,       // ждать данные перед рендером
})

2. useAsyncData() — явный контроль

// Полный контроль над ключом кэширования и функцией загрузки
const { data, pending, error } = await useAsyncData(
  'unique-key',         // ключ для дедупликации и кэша
  async () => {
    const [users, stats] = await Promise.all([
      $fetch('/api/users'),
      $fetch('/api/stats'),
    ])
    return { users, stats }
  },
  {
    lazy: true,          // не блокировать рендер
    server: false,       // только на клиенте
    transform: (data) => data.users.slice(0, 10),
    default: () => [],   // значение по умолчанию
  }
)

3. $fetch — низкоуровневый fetch

// Простой HTTP-клиент без кэширования (ofetch под капотом)
// Используйте для POST/PUT/DELETE или внутри обработчиков событий

// В обработчике события — $fetch не создаёт дублирующих запросов SSR
async function submitForm() {
  const result = await $fetch('/api/users', {
    method: 'POST',
    body: { name: 'Иван' },
  })
}

// В server/ — $fetch делает серверные запросы эффективно

Опции useFetch / useAsyncData

const { data, pending, error, refresh, clear } = await useFetch('/api/data', {
  // Обработка данных перед сохранением
  transform: (response) => response.items,

  // Значение пока данные загружаются
  default: () => [],

  // Не блокировать навигацию (контент появится позже)
  lazy: true,

  // Только клиентская сторона (не SSR)
  server: false,

  // Следить за реактивными значениями и перезапрашивать
  watch: [page, filter],

  // Дополнительные заголовки/параметры
  headers: { Authorization: `Bearer ${token.value}` },
  query: { page: page.value, limit: 20 },

  // Кэширование (по умолчанию — кэшируется)
  getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key],
})

// Ручное обновление
await refresh()   // повторный запрос
clear()           // сброс данных и статуса

Режимы server/client

// Только серверная загрузка (данные передаются через payload)
const { data } = await useAsyncData('key', fn, { server: true, client: false })

// Только клиентская загрузка
const { data } = await useAsyncData('key', fn, { server: false })

// По умолчанию — сервер + гидрация данных на клиент

Обработка ошибок

const { data, error } = await useFetch('/api/users')

// error — реактивный ref<FetchError | null>
if (error.value) {
  // error.value.statusCode — HTTP статус
  // error.value.message — сообщение
}

// Глобальная обработка в onResponseError:
const { data } = await useFetch('/api/data', {
  onResponseError({ response }) {
    if (response.status === 401) navigateTo('/login')
  }
})

// throw createError() для страницы ошибки:
throw createError({ statusCode: 404, message: 'Не найдено' })

Кэширование и дедупликация

// Nuxt автоматически сериализует данные в payload
// Один и тот же запрос НЕ повторяется на клиенте если уже выполнен на сервере

// Очистка кэша:
const nuxtApp = useNuxtApp()
nuxtApp.payload.data['my-key'] = null

// useAsyncData с кастомным кэшем:
const { data } = await useAsyncData('users', () => $fetch('/api/users'), {
  getCachedData: (key, nuxtApp) => {
    return nuxtApp.static.data[key]  // SSG кэш
  }
})

Примеры

Реализация useFetch — дедупликация запросов, кэш, reactive refresh, обработка ошибок

// Реализуем useFetch/useAsyncData без Vue-рантайма:
// ключевые паттерны — дедупликация, кэш, lazy loading.

class DataStore {
  constructor() {
    this._cache = new Map()
    this._pending = new Map()  // дедупликация одновременных запросов
  }

  async fetch(key, fetcher, options = {}) {
    const {
      lazy = false,
      transform = null,
      defaultValue = null,
      server = true,
      forceRefresh = false,
    } = options

    // Проверяем кэш
    if (!forceRefresh && this._cache.has(key)) {
      console.log(`[Cache HIT] "${key}"`)
      return this._createRef(this._cache.get(key), false, null)
    }

    // Дедупликация: если запрос уже летит — ждём его
    if (this._pending.has(key)) {
      console.log(`[Dedup] "${key}" — ждём уже выполняющийся запрос`)
      const existing = await this._pending.get(key)
      return this._createRef(existing, false, null)
    }

    if (lazy) {
      // Не блокируем — возвращаем сразу с defaultValue
      const ref = this._createRef(defaultValue, true, null)
      fetcher().then(result => {
        const transformed = transform ? transform(result) : result
        this._cache.set(key, transformed)
        ref._update(transformed, false, null)
      }).catch(err => {
        ref._update(null, false, err.message)
      })
      return ref
    }

    // Синхронный запрос (блокирует рендер в SSR)
    const promise = fetcher().then(result => {
      const transformed = transform ? transform(result) : result
      this._cache.set(key, transformed)
      this._pending.delete(key)
      return transformed
    }).catch(err => {
      this._pending.delete(key)
      throw err
    })

    this._pending.set(key, promise)

    try {
      const result = await promise
      return this._createRef(result, false, null)
    } catch (err) {
      return this._createRef(null, false, err.message)
    }
  }

  _createRef(initialData, initialPending, initialError) {
    let data = initialData
    let pending = initialPending
    let error = initialError
    const listeners = new Set()

    const ref = {
      get data() { return data },
      get pending() { return pending },
      get error() { return error },
      subscribe(fn) {
        listeners.add(fn)
        fn({ data, pending, error })
        return () => listeners.delete(fn)
      },
      _update(newData, newPending, newError) {
        data = newData; pending = newPending; error = newError
        listeners.forEach(fn => fn({ data, pending, error }))
      }
    }
    return ref
  }

  clearCache(key) {
    this._cache.delete(key)
  }
}

// --- Симуляция API ---
const store = new DataStore()

let apiCallCount = 0
async function fakeAPI(endpoint) {
  apiCallCount++
  await new Promise(r => setTimeout(r, 30))
  if (endpoint === '/api/error') throw new Error('500 Internal Server Error')
  if (endpoint === '/api/users') return [{ id: 1, name: 'Иван' }, { id: 2, name: 'Пётр' }]
  if (endpoint.startsWith('/api/users/')) {
    const id = endpoint.split('/').at(-1)
    return { id: Number(id), name: `Пользователь #${id}`, email: `user${id}@mail.ru` }
  }
  return { status: 'ok' }
}

// === Тесты ===
async function runTests() {
  console.log('=== Базовый запрос ===')
  const ref1 = await store.fetch('users', () => fakeAPI('/api/users'))
  console.log('data:', ref1.data.map(u => u.name))
  console.log('pending:', ref1.pending)
  console.log('error:', ref1.error)

  console.log('\n=== Кэш (второй вызов) ===')
  const ref2 = await store.fetch('users', () => fakeAPI('/api/users'))
  console.log('Кэш работает:', ref1.data === ref2.data)
  console.log('Вызовов API:', apiCallCount)  // 1

  console.log('\n=== Дедупликация ===')
  apiCallCount = 0
  store.clearCache('dedup-key')
  const [r1, r2, r3] = await Promise.all([
    store.fetch('dedup-key', () => fakeAPI('/api/users')),
    store.fetch('dedup-key', () => fakeAPI('/api/users')),
    store.fetch('dedup-key', () => fakeAPI('/api/users')),
  ])
  console.log('3 вызовов → реальных запросов:', apiCallCount)  // 1

  console.log('\n=== Transform ===')
  store.clearCache('transformed')
  const ref3 = await store.fetch('transformed', () => fakeAPI('/api/users'), {
    transform: users => users.map(u => u.name.toUpperCase())
  })
  console.log('Transformed:', ref3.data)

  console.log('\n=== Lazy loading ===')
  store.clearCache('lazy-data')
  const ref4 = await store.fetch('lazy-data', () => fakeAPI('/api/users/5'), {
    lazy: true,
    defaultValue: [],
  })
  console.log('Сразу (lazy):', ref4.data, ref4.pending)
  await new Promise(r => setTimeout(r, 100))
  console.log('После загрузки:', ref4.data)

  console.log('\n=== Обработка ошибки ===')
  const ref5 = await store.fetch('error-data', () => fakeAPI('/api/error'))
  console.log('error:', ref5.error)
  console.log('data:', ref5.data)
}

runTests()