Справочник
Типы TypeScript
Все публичные типы экспортируются из корня пакета:
import type {
ImageStatus, // 'idle' | 'loading' | 'loaded' | 'error'
SrcSet, // { avif?: string; webp?: string; fallback: string }
ResponsiveSrc, // Record<string, string | SrcSet> — брейкпоинт → URL, либо набор форматов для этого брейкпоинта
BreakpointMap, // Record<string, string> — брейкпоинт → CSS media query
VImageKitOptions, // { breakpoints?: BreakpointMap }
LazyImgOptions, // { src, placeholder?, rootMargin?, threshold?, onLoad?, onError? }
ObjectFit, // 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
FocalPoint, // { x: number; y: number } — доли 0–1
Densities, // number[] | Record<number, string> — дескрипторы плотности
ImageMeta, // форма записи манифеста CLI / импорта `?vik`, для пропа `image`
Layout, // 'fixed' | 'responsive' | 'fill' — проп `layout`
} from 'vue-image-kit'ImageStatus
type ImageStatus = 'idle' | 'loading' | 'loaded' | 'error'Конечный автомат переходит по порядку: idle → loading → loaded или idle → loading → error.
SrcSet
interface SrcSet {
avif?: string // Опциональный URL AVIF-источника
webp?: string // Опциональный URL WebP-источника
fallback: string // Обязателен — используется как fallback для <img src>
}LazyImgOptions
interface LazyImgOptions {
src: string
placeholder?: string
rootMargin?: string
threshold?: number
onLoad?: () => void
onError?: (e: Event) => void
}Директива v-lazy-img принимает либо обычную string (это src), либо объект LazyImgOptions.
Работа с типизированными опциями в v-lazy-img
import type { LazyImgOptions } from 'vue-image-kit'
const bgOptions: LazyImgOptions = {
src: '/hero.jpg',
placeholder: 'data:image/jpeg;base64,...',
rootMargin: '100px',
onLoad: () => analytics.track('hero_loaded'),
}<div v-lazy-img="bgOptions" class="hero" />Совместимость с SSR
| Сценарий | Поведение |
|---|---|
Серверный рендер — <VImage> | Рендерит <img loading="lazy"> с src и alt; без IO, без canvas |
| Серверный рендер — aspect-ratio | Рендерится <div> с aspect-ratio: width/height, когда заданы width и height |
| Blurhash на сервере | Код canvas внутри onMounted — не выполняется; вместо этого рендерится пустой контейнер |
IntersectionObserver на сервере | Не используется; сервер рендерит обычный <img> |
| Гидратация | После монтирования onMounted настраивает IO (если lazy: true) либо сразу начинает загрузку (если lazy: false) |
v-lazy-img на сервере | Хуки директивы (mounted, unmounted) не вызываются во время SSR — IO не создаётся |
useLazyLoad на сервере | Немедленно возвращает { isIntersecting: true } — вызывающий код действует так, будто уже во вьюпорте |
Использование в Nuxt:
Особая настройка не требуется. Компонент корректно рендерится и в SSR, и в клиентском режиме. Если нужно узнать, смонтировался ли клиент, используйте onMounted из Vue:
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const mounted = ref(false)
onMounted(() => {
mounted.value = true
})
</script>
<template>
<VImage v-if="mounted" src="/photo.jpg" alt="Photo" blurhash="..." />
<div v-else style="aspect-ratio: 16/9; background: #e5e7eb;" />
</template>Архитектура
VImage.vue
│ props: src, alt, width, height,
│ blurhash, thumbhash, placeholder,
│ widths, sizes, sources, breakpoints,
│ lazy, rootMargin, threshold, fit,
│ maxRetries, retryDelay,
│ fetchpriority, decoding
│
├──▶ useImage(options)
│ │
│ ├── useLazyLoad({ rootMargin, threshold })
│ │ IntersectionObserver (SSR-safe)
│ │ isIntersecting: Ref<boolean>
│ │ observe(elRef) → starts watching
│ │
│ ├── State machine
│ │ idle → loading → loaded
│ │ → error (retryCount >= maxRetries)
│ │ → idle → loading (retry, exponential backoff)
│ │ lazy=true → watch(isIntersecting) → loading
│ │ lazy=false → onMounted → loading
│ │
│ └── imgAttrs: ComputedRef
│ src = fallback URL
│ srcset = generateSrcset(src, widths)
│ sizes = generateSizes(sizes)
│ style = { objectFit: fit }
│
├──▶ useBlurhash({ blurhash, width, height })
│ onMounted → decodeBlurhash(hash, width, height)
│ → new ImageData(pixels, width, height)
│ → ctx.putImageData(imageData, 0, 0)
│ canvasRef: Ref<HTMLCanvasElement | null>
│ SSR: returns null ref (canvas code never runs)
│
├──▶ useBreakpoints(breakpoints?)
│ Merges local breakpoints prop with global plugin breakpoints
│ resolveMediaSources(sources) → sorted [{ media, src }]
│
├──▶ effectivePlaceholder: ComputedRef<string | undefined>
│ placeholder prop → used as-is (LQIP base64)
│ thumbhash prop → decodeThumbHash(hash) → PNG data URL
│ neither → undefined (no blur-up placeholder)
│
├──▶ Template structure (client)
│ <span wrapper :style="{ aspectRatio, position: relative }">
│ <canvas v-if="blurhash && width && height && !isError" />
│ ← BlurHash canvas placeholder
│ <img aria-hidden
│ v-if="effectivePlaceholder && !isError" />
│ ← LQIP / ThumbHash blur-up
│ <span v-if="isError"> ← error state
│ <slot name="error"><svg .../></slot>
│ </span>
│ <picture v-if="shouldRenderImg && !isError && needsPicture">
│ ← format/art-direction sources
│ <source v-for media/srcset /> ← responsive art direction
│ <source type="image/avif" />
│ <source type="image/webp" />
│ <img v-bind="imgAttrs" :decoding :fetchpriority @load @error />
│ </picture>
│ <img v-if="shouldRenderImg && !isError && !needsPicture"
│ v-bind="imgAttrs" :decoding :fetchpriority @load @error />
│ ← simple img (no picture)
│ <span v-if="isIdle && !blurhash && !effectivePlaceholder" />
│ ← grey background (no placeholder)
│ </span>
│
└──▶ Template structure (SSR)
<img :src :alt :width :height :decoding :fetchpriority
:loading="lazy ? 'lazy' : 'eager'" />
vLazyImg (Directive)
│ mounted(el, binding)
│ resolveOptions(binding) → { src, placeholder, rootMargin, ... }
│ createObserver(el, options)
│ IntersectionObserver → on intersect:
│ if placeholder: el.style.backgroundImage = url(placeholder)
│ new Image()
│ onload → el.style.backgroundImage = url(src); onLoad()
│ onerror → onError(e)
│ updated → disconnect old observer, create new one
│ unmounted → observer.disconnect()
Utils (pure functions, zero Vue deps)
│ blurhash-decode.ts
│ decodeBlurhash(hash, width, height) → Uint8ClampedArray ← RGBA pixels
│
│ thumbhash-decode.ts
│ decodeThumbHash(hash: string | Uint8Array) → string ← PNG data URL
│
└── srcset.ts
generateSrcset(src, widths) → string
generateSizes(sizes?) → string
buildSizes(map, breakpoints) → string
generatePreloadLink(href, options) → stringРазмер бандла и peer-зависимости
| Точка входа | Raw | Gzip | Peer-зависимости |
|---|---|---|---|
vue-image-kit ESM | 42.1 kB | 13.0 kB | vue ^3.0 |
vue-image-kit CJS | 31.9 kB | 11.3 kB | vue ^3.0 |
vue-image-kit/cdn ESM | 10.8 kB | 2.4 kB | — |
Измерено по реальному результату сборки (npm run build), не поддерживается вручную — CI падает, если это отклоняется от порогов в .github/workflows/ci.yml.
Поставляется как tree-shakeable ESM (vue-image-kit.js) и CommonJS (vue-image-kit.cjs). "sideEffects": false в package.json — неиспользуемые экспорты удаляются бандлером. Если вы импортируете только vLazyImg или один composable, бандлер исключит всё остальное (VImage, декодер blurhash и т. д.).
Пример tree-shaking — использование только директивы:
// В бандл попадает только vLazyImg и его логика IO.
// VImage, useBlurhash, decodeBlurhash не импортированы → не бандлятся.
import { vLazyImg } from 'vue-image-kit'
app.directive('lazy-img', vLazyImg)Лицензия
MIT