Nuxt предоставляет специальные composables, оптимизированные для SSR:
// Автоматически:
// - Работает на сервере и клиенте
// - Дедуплицирует запросы (один запрос для SSR + гидрации)
// - Реактивен к изменениям URL
const { data, pending, error, refresh } = await useFetch('/api/users')
// С параметрами
const { data: user } = await useFetch(() => `/api/users/${userId.value}`, {
watch: [userId], // перезапрос при изменении
lazy: false, // ждать данные перед рендером
})// Полный контроль над ключом кэширования и функцией загрузки
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: () => [], // значение по умолчанию
}
)// Простой HTTP-клиент без кэширования (ofetch под капотом)
// Используйте для POST/PUT/DELETE или внутри обработчиков событий
// В обработчике события — $fetch не создаёт дублирующих запросов SSR
async function submitForm() {
const result = await $fetch('/api/users', {
method: 'POST',
body: { name: 'Иван' },
})
}
// В server/ — $fetch делает серверные запросы эффективно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() // сброс данных и статуса// Только серверная загрузка (данные передаются через 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()
Nuxt предоставляет специальные composables, оптимизированные для SSR:
// Автоматически:
// - Работает на сервере и клиенте
// - Дедуплицирует запросы (один запрос для SSR + гидрации)
// - Реактивен к изменениям URL
const { data, pending, error, refresh } = await useFetch('/api/users')
// С параметрами
const { data: user } = await useFetch(() => `/api/users/${userId.value}`, {
watch: [userId], // перезапрос при изменении
lazy: false, // ждать данные перед рендером
})// Полный контроль над ключом кэширования и функцией загрузки
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: () => [], // значение по умолчанию
}
)// Простой HTTP-клиент без кэширования (ofetch под капотом)
// Используйте для POST/PUT/DELETE или внутри обработчиков событий
// В обработчике события — $fetch не создаёт дублирующих запросов SSR
async function submitForm() {
const result = await $fetch('/api/users', {
method: 'POST',
body: { name: 'Иван' },
})
}
// В server/ — $fetch делает серверные запросы эффективно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() // сброс данных и статуса// Только серверная загрузка (данные передаются через 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()
Реализуй функцию `createDataFetcher()`, возвращающую объект с методом `useData(key, fetchFn, options)`. Метод должен: сохранять результат в кэш по ключу key, возвращать объект `{ data, loading, error, refresh }`. При повторном вызове с тем же key — возвращать кэшированные данные (без нового запроса). Опция `transform(data)` — преобразует данные перед кэшированием. `refresh()` — очищает кэш и перезапрашивает. Опция `defaultValue` — значение data пока загружается.
В начале useData проверяй: if (cache.has(key)) { return { data: cache.get(key), loading: false, error: null, refresh } }. В load(): loading = true; try { let result = await fetchFn(); if (transform) result = transform(result); cache.set(key, result); data = result; } catch(e) { error = e.message; } finally { loading = false; }. В refresh(): cache.delete(key); data = defaultValue; await load().
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке