При сборке большого приложения весь код попадает в один бандл. Тяжёлые компоненты (графики, редакторы, карты) загружаются даже если пользователь их никогда не откроет. **Асинхронные компоненты** решают эту проблему: компонент загружается только когда он действительно нужен.
import { defineAsyncComponent } from 'vue'
const HeavyChart = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
)Динамический import() возвращает Promise — Vite/Webpack автоматически выделяют этот модуль в отдельный чанк. Компонент скачивается с сервера только при первом рендере.
const AsyncModal = defineAsyncComponent({
// Функция-загрузчик
loader: () => import('./Modal.vue'),
// Компонент, отображаемый пока идёт загрузка
loadingComponent: LoadingSpinner,
// Компонент, отображаемый при ошибке загрузки
errorComponent: ErrorBoundary,
// Задержка перед показом loadingComponent (мс)
// Предотвращает мигание при быстрой загрузке
delay: 200,
// Максимальное время ожидания загрузки (мс)
// После истечения показывается errorComponent
timeout: 10000,
// Вызывается при ошибке (Vue 3.3+)
onError(error, retry, fail, attempts) {
if (attempts <= 3) retry() // Повторить попытку
else fail() // Показать errorComponent
},
})<Suspense> — экспериментальный компонент Vue, который управляет состоянием загрузки дерева async-компонентов:
<Suspense>
<!-- Основной контент — может содержать async компоненты -->
<template #default>
<HeavyChart :data="chartData" />
</template>
<!-- Показывается пока default слот загружается -->
<template #fallback>
<LoadingSpinner message="Загружаем график..." />
</template>
</Suspense>Отличие от loadingComponent: Suspense работает на уровне дерева компонентов и поддерживает async setup().
// AsyncUserProfile.vue
const { data: user } = await useFetch('/api/user')
// ^ await в setup() работает только внутри <Suspense>// router/index.js — каждый маршрут в отдельном чанке
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'),
},
{
path: '/reports',
component: defineAsyncComponent({
loader: () => import('./views/Reports.vue'),
loadingComponent: PageSkeleton,
delay: 300,
}),
},
]Асинхронная загрузка модулей с состояниями loading/error/success — аналог defineAsyncComponent
// Реализуем аналог defineAsyncComponent:
// асинхронная загрузка с состояниями и retry-логикой.
function defineAsyncComponent(options) {
// Нормализуем: функция → объект с loader
if (typeof options === 'function') {
options = { loader: options }
}
const {
loader,
delay = 200,
timeout = null,
onError = null,
} = options
let cachedComponent = null
let status = 'idle' // idle | loading | loaded | error
let attempts = 0
async function load() {
if (cachedComponent) return cachedComponent
status = 'loading'
attempts++
const loadPromise = loader()
let timedOut = false
// Таймаут
const timeoutPromise = timeout
? new Promise((_, reject) =>
setTimeout(() => {
timedOut = true
reject(new Error(`Timeout: компонент не загружен за ${timeout}мс`))
}, timeout)
)
: null
try {
const result = await (timeoutPromise
? Promise.race([loadPromise, timeoutPromise])
: loadPromise)
// Динамический import возвращает { default: Component }
cachedComponent = result.default ?? result
status = 'loaded'
console.log(`[AsyncComp] Загружен за попытку #${attempts}`)
return cachedComponent
} catch (err) {
status = 'error'
if (onError) {
let resolved = false
await new Promise((resolve, reject) => {
onError(
err,
() => { resolved = true; resolve() }, // retry
() => reject(err), // fail
attempts
)
if (!resolved) reject(err)
}).catch(() => {})
if (resolved) {
console.log(`[AsyncComp] Повторная попытка #${attempts + 1}`)
return load() // рекурсия — retry
}
}
throw err
}
}
return { load, getStatus: () => status, getAttempts: () => attempts }
}
// --- Симуляция загрузки модуля ---
function mockImport(name, { failTimes = 0, delay: ms = 100 } = {}) {
let calls = 0
return async () => {
calls++
await new Promise(r => setTimeout(r, ms))
if (calls <= failTimes) {
throw new Error(`Сетевая ошибка при загрузке ${name} (попытка ${calls})`)
}
return { default: { name, template: `<div>${name}</div>` } }
}
}
// === Тест 1: Успешная загрузка ===
async function test1() {
console.log('=== Тест 1: Простая загрузка ===')
const AsyncComp = defineAsyncComponent(mockImport('HeavyChart'))
const comp = await AsyncComp.load()
console.log('Компонент:', comp.name, '| Статус:', AsyncComp.getStatus())
// Кэш — второй вызов без запроса
await AsyncComp.load()
console.log('Попыток загрузки (всего 1 реальная):', AsyncComp.getAttempts())
}
// === Тест 2: Retry при ошибке ===
async function test2() {
console.log('\n=== Тест 2: Retry (провал 2 раза) ===')
const AsyncComp = defineAsyncComponent({
loader: mockImport('DataGrid', { failTimes: 2, delay: 50 }),
onError(err, retry, fail, attempts) {
console.log(`Ошибка #${attempts}:`, err.message)
if (attempts < 3) retry()
else fail()
}
})
try {
const comp = await AsyncComp.load()
console.log('Успех после', AsyncComp.getAttempts(), 'попыток:', comp.name)
} catch (e) {
console.log('Финальная ошибка:', e.message)
}
}
// === Тест 3: Таймаут ===
async function test3() {
console.log('\n=== Тест 3: Таймаут ===')
const AsyncComp = defineAsyncComponent({
loader: mockImport('SlowMap', { delay: 500 }),
timeout: 200,
})
try {
await AsyncComp.load()
} catch (e) {
console.log('Поймали:', e.message, '| Статус:', AsyncComp.getStatus())
}
}
test1().then(() => test2()).then(() => test3())
При сборке большого приложения весь код попадает в один бандл. Тяжёлые компоненты (графики, редакторы, карты) загружаются даже если пользователь их никогда не откроет. **Асинхронные компоненты** решают эту проблему: компонент загружается только когда он действительно нужен.
import { defineAsyncComponent } from 'vue'
const HeavyChart = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
)Динамический import() возвращает Promise — Vite/Webpack автоматически выделяют этот модуль в отдельный чанк. Компонент скачивается с сервера только при первом рендере.
const AsyncModal = defineAsyncComponent({
// Функция-загрузчик
loader: () => import('./Modal.vue'),
// Компонент, отображаемый пока идёт загрузка
loadingComponent: LoadingSpinner,
// Компонент, отображаемый при ошибке загрузки
errorComponent: ErrorBoundary,
// Задержка перед показом loadingComponent (мс)
// Предотвращает мигание при быстрой загрузке
delay: 200,
// Максимальное время ожидания загрузки (мс)
// После истечения показывается errorComponent
timeout: 10000,
// Вызывается при ошибке (Vue 3.3+)
onError(error, retry, fail, attempts) {
if (attempts <= 3) retry() // Повторить попытку
else fail() // Показать errorComponent
},
})<Suspense> — экспериментальный компонент Vue, который управляет состоянием загрузки дерева async-компонентов:
<Suspense>
<!-- Основной контент — может содержать async компоненты -->
<template #default>
<HeavyChart :data="chartData" />
</template>
<!-- Показывается пока default слот загружается -->
<template #fallback>
<LoadingSpinner message="Загружаем график..." />
</template>
</Suspense>Отличие от loadingComponent: Suspense работает на уровне дерева компонентов и поддерживает async setup().
// AsyncUserProfile.vue
const { data: user } = await useFetch('/api/user')
// ^ await в setup() работает только внутри <Suspense>// router/index.js — каждый маршрут в отдельном чанке
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'),
},
{
path: '/reports',
component: defineAsyncComponent({
loader: () => import('./views/Reports.vue'),
loadingComponent: PageSkeleton,
delay: 300,
}),
},
]Асинхронная загрузка модулей с состояниями loading/error/success — аналог defineAsyncComponent
// Реализуем аналог defineAsyncComponent:
// асинхронная загрузка с состояниями и retry-логикой.
function defineAsyncComponent(options) {
// Нормализуем: функция → объект с loader
if (typeof options === 'function') {
options = { loader: options }
}
const {
loader,
delay = 200,
timeout = null,
onError = null,
} = options
let cachedComponent = null
let status = 'idle' // idle | loading | loaded | error
let attempts = 0
async function load() {
if (cachedComponent) return cachedComponent
status = 'loading'
attempts++
const loadPromise = loader()
let timedOut = false
// Таймаут
const timeoutPromise = timeout
? new Promise((_, reject) =>
setTimeout(() => {
timedOut = true
reject(new Error(`Timeout: компонент не загружен за ${timeout}мс`))
}, timeout)
)
: null
try {
const result = await (timeoutPromise
? Promise.race([loadPromise, timeoutPromise])
: loadPromise)
// Динамический import возвращает { default: Component }
cachedComponent = result.default ?? result
status = 'loaded'
console.log(`[AsyncComp] Загружен за попытку #${attempts}`)
return cachedComponent
} catch (err) {
status = 'error'
if (onError) {
let resolved = false
await new Promise((resolve, reject) => {
onError(
err,
() => { resolved = true; resolve() }, // retry
() => reject(err), // fail
attempts
)
if (!resolved) reject(err)
}).catch(() => {})
if (resolved) {
console.log(`[AsyncComp] Повторная попытка #${attempts + 1}`)
return load() // рекурсия — retry
}
}
throw err
}
}
return { load, getStatus: () => status, getAttempts: () => attempts }
}
// --- Симуляция загрузки модуля ---
function mockImport(name, { failTimes = 0, delay: ms = 100 } = {}) {
let calls = 0
return async () => {
calls++
await new Promise(r => setTimeout(r, ms))
if (calls <= failTimes) {
throw new Error(`Сетевая ошибка при загрузке ${name} (попытка ${calls})`)
}
return { default: { name, template: `<div>${name}</div>` } }
}
}
// === Тест 1: Успешная загрузка ===
async function test1() {
console.log('=== Тест 1: Простая загрузка ===')
const AsyncComp = defineAsyncComponent(mockImport('HeavyChart'))
const comp = await AsyncComp.load()
console.log('Компонент:', comp.name, '| Статус:', AsyncComp.getStatus())
// Кэш — второй вызов без запроса
await AsyncComp.load()
console.log('Попыток загрузки (всего 1 реальная):', AsyncComp.getAttempts())
}
// === Тест 2: Retry при ошибке ===
async function test2() {
console.log('\n=== Тест 2: Retry (провал 2 раза) ===')
const AsyncComp = defineAsyncComponent({
loader: mockImport('DataGrid', { failTimes: 2, delay: 50 }),
onError(err, retry, fail, attempts) {
console.log(`Ошибка #${attempts}:`, err.message)
if (attempts < 3) retry()
else fail()
}
})
try {
const comp = await AsyncComp.load()
console.log('Успех после', AsyncComp.getAttempts(), 'попыток:', comp.name)
} catch (e) {
console.log('Финальная ошибка:', e.message)
}
}
// === Тест 3: Таймаут ===
async function test3() {
console.log('\n=== Тест 3: Таймаут ===')
const AsyncComp = defineAsyncComponent({
loader: mockImport('SlowMap', { delay: 500 }),
timeout: 200,
})
try {
await AsyncComp.load()
} catch (e) {
console.log('Поймали:', e.message, '| Статус:', AsyncComp.getStatus())
}
}
test1().then(() => test2()).then(() => test3())
Реализуй функцию `createAsyncLoader(loaderFn, options)`, которая возвращает объект с методами: `load()` — возвращает Promise с результатом загрузки (если уже загружено — возвращает кэш), `getState()` — возвращает строку "idle" | "loading" | "success" | "error", `getError()` — возвращает последнюю ошибку или null. Опции: `timeout` (мс, по умолчанию нет), `retries` (число повторных попыток при ошибке, по умолчанию 0).
Для таймаута используй Promise.race([loaderFn(), new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout))]). В attemptLoad при ошибке проверяй: if (attemptsLeft > 0) return attemptLoad(attemptsLeft - 1), иначе throw err. Кэш проверяй через if (cache !== null) в начале load().
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке