vite-smart-img 1.1.14 → 1.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +366 -365
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,365 +1,366 @@
|
|
|
1
|
-
# vite-smart-img
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
- [
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
12
|
-
- [1. Vite
|
|
13
|
-
- [2.
|
|
14
|
-
- [3.
|
|
15
|
-
- [
|
|
16
|
-
- [CSS
|
|
17
|
-
- [data-max-size-load
|
|
18
|
-
- [
|
|
19
|
-
- [⚠️
|
|
20
|
-
- [
|
|
21
|
-
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
##
|
|
25
|
-
|
|
26
|
-
1.
|
|
27
|
-
2. **JPG, PNG, SVG
|
|
28
|
-
3. **SVG
|
|
29
|
-
4.
|
|
30
|
-
5.
|
|
31
|
-
6.
|
|
32
|
-
7. **
|
|
33
|
-
8.
|
|
34
|
-
9.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
- **
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
>
|
|
56
|
-
>
|
|
57
|
-
>
|
|
58
|
-
>
|
|
59
|
-
>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
import
|
|
73
|
-
import
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
import
|
|
131
|
-
import
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
--smi-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
--smi-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
| `
|
|
218
|
-
| `
|
|
219
|
-
| `
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
| `--smi-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
| `"
|
|
262
|
-
| `"
|
|
263
|
-
| `"
|
|
264
|
-
| `"
|
|
265
|
-
| `"
|
|
266
|
-
| `"
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
→
|
|
295
|
-
→
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
a1b2c3d4
|
|
317
|
-
a1b2c3d4-400.
|
|
318
|
-
a1b2c3d4-
|
|
319
|
-
a1b2c3d4-600.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
a1b2c3d4-2000.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
| `data-
|
|
363
|
-
| `data-original-
|
|
364
|
-
|
|
365
|
-
|
|
1
|
+
# vite-smart-img
|
|
2
|
+
|
|
3
|
+
Умный компонент ленивой загрузки изображений для **React + Vite**: автоматические превью (thumbnails), конвертация в **WebP**, обновление `src` при изменении размера элемента.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Содержание
|
|
8
|
+
|
|
9
|
+
- [Возможности](#возможности)
|
|
10
|
+
- [Установка](#установка)
|
|
11
|
+
- [Интеграция](#интеграция)
|
|
12
|
+
- [1. Плагины Vite](#1-плагины-vite)
|
|
13
|
+
- [2. Типы для TypeScript](#2-типы-для-typescript)
|
|
14
|
+
- [3. Использование SmartImg в компонентах](#3-использование-smartimg-в-компонентах)
|
|
15
|
+
- [API компонента](#api-компонента)
|
|
16
|
+
- [Кастомизация CSS](#кастомизация-css)
|
|
17
|
+
- [Атрибут data-max-size-load](#атрибут-data-max-size-load)
|
|
18
|
+
- [Как это работает — подробно](#как-это-работает--подробно)
|
|
19
|
+
- [⚠️ Исходные файлы WebP — не поддерживаются](#️-исходные-файлы-webp--не-поддерживаются)
|
|
20
|
+
- [Отладочные атрибуты](#отладочные-атрибуты)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Возможности
|
|
25
|
+
|
|
26
|
+
1. **Компонент SmartImg** — ленивый React-компонент: изображение запрашивается, когда оно попадает в область просмотра (видимость **≥ 5%**).
|
|
27
|
+
2. **JPG, PNG, SVG и внешние URL** — для каждого формата своя логика.
|
|
28
|
+
3. **SVG и внешние картинки** — после появления в зоне видимости загружаются целиком; сразу выставляется `data-max-size-load`.
|
|
29
|
+
4. **Адаптивные миниатюры (только prod)** — выбирается наименьшая миниатюра, которая покрывает **CSS-ширину элемента × DPR**; после первого события **resize** при необходимости подгружается более крупный размер.
|
|
30
|
+
5. **Плавное появление** — опционально через проп **`opacity`**.
|
|
31
|
+
6. **Кэш сессии** — загруженные URL запоминаются на время жизни страницы; при повторном монтировании восстановление без повторного fade.
|
|
32
|
+
7. **IntersectionObserver** — сетевой запрос не уходит, пока браузер не сообщит о видимости **≥ 5%**.
|
|
33
|
+
8. **Плагин сборки Vite** — хеши имён, очистка метаданных, миниатюры и варианты WebP для растра, оптимизация SVG.
|
|
34
|
+
9. **Авторазмеры** — в dev и build плагины вытаскивают **`originalW` / `originalH`** из ассетов и встраивают в модуль, чтобы контейнер с **aspect-ratio** был корректным.
|
|
35
|
+
|
|
36
|
+
Дополнительно:
|
|
37
|
+
|
|
38
|
+
- **Определение WebP** через `canvas.toDataURL('image/webp')` — один раз при первом использовании, результат кэшируется.
|
|
39
|
+
- **Debounced resize** — `window.resize` с задержкой **200 ms**.
|
|
40
|
+
- **Ошибка загрузки** — для битой картинки показывается SVG-плейсхолдер; `data-max-size-load` равен **`"svg-error"`** или **`"external-error"`**.
|
|
41
|
+
- **CSS-переменные** — `--smi-bg` (фон плейсхолдера), `--smi-fade-duration` (длительность перехода).
|
|
42
|
+
- **Проп `className`** — дополнительный класс на обёртке **`<div>`**.
|
|
43
|
+
- **Один IO и один слушатель resize** на страницу для всех экземпляров **SmartImg**.
|
|
44
|
+
- **Progressive JPEG** — миниатюры JPEG кодируются как progressive.
|
|
45
|
+
- **`decoding="async"`** на **`<img>`**, чтобы декод не блокировал основной поток.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Установка
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install vite-smart-img
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
> **Peer dependencies** — в проекте должны быть **`react >= 18`** и **`react-dom >= 18`**.
|
|
56
|
+
>
|
|
57
|
+
> **Для сборки** — плагинам Vite нужен **`sharp`** в devDependencies проекта:
|
|
58
|
+
> ```bash
|
|
59
|
+
> npm install --save-dev sharp
|
|
60
|
+
> ```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Интеграция
|
|
65
|
+
|
|
66
|
+
### 1. Плагины Vite
|
|
67
|
+
|
|
68
|
+
Подключите все четыре плагина в **`vite.config.ts`**. У каждого своя роль:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
// vite.config.ts
|
|
72
|
+
import { defineConfig } from 'vite'
|
|
73
|
+
import react from '@vitejs/plugin-react'
|
|
74
|
+
import {
|
|
75
|
+
imageThumbnails,
|
|
76
|
+
smartImgSvgAssetBuild,
|
|
77
|
+
importedAssetsAlwaysFiles,
|
|
78
|
+
smartImgLocalAssetImport,
|
|
79
|
+
} from 'vite-smart-img/plugin'
|
|
80
|
+
|
|
81
|
+
export default defineConfig({
|
|
82
|
+
plugins: [
|
|
83
|
+
// Должны идти до react() и прочих плагинов
|
|
84
|
+
importedAssetsAlwaysFiles(), // отключает inline ассетов Vite (assetsInlineLimit: 0)
|
|
85
|
+
smartImgLocalAssetImport(), // только dev: размеры JPG/PNG/SVG и { src, originalW, originalH }
|
|
86
|
+
imageThumbnails(), // только build: миниатюры, WebP, без EXIF, хеши имён файлов
|
|
87
|
+
smartImgSvgAssetBuild(), // только build: SVGO и хеш для SVG
|
|
88
|
+
|
|
89
|
+
react(),
|
|
90
|
+
],
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Что делает каждый плагин:**
|
|
95
|
+
|
|
96
|
+
| Плагин | Когда | Назначение |
|
|
97
|
+
|--------|--------|------------|
|
|
98
|
+
| `importedAssetsAlwaysFiles` | всегда | **`build.assetsInlineLimit: 0`** — мелкие файлы не уходят в base64 внутри JS, иначе ломается сборка URL миниатюр |
|
|
99
|
+
| `smartImgLocalAssetImport` | только dev | Перехватывает импорты **`*.jpg / *.png / *.svg`** из папок **`assets/`**; **sharp** (растр) или парсер SVG; экспорт **`{ src, originalW, originalH }`** |
|
|
100
|
+
| `imageThumbnails` | только build | Хеш содержимого (SHA-256, 8 символов), снятие EXIF/метаданных, миниатюры по ширинам **[400, 600, 800 … 8000]**, не шире оригинала; пары **`.jpg/.png` + `.webp`**; если миниатюра получилась больше очищенного оригинала — используется оригинал |
|
|
101
|
+
| `smartImgSvgAssetBuild` | только build | Оптимизация SVG через SVGO (**preset-default**, multipass), хеш, вывод как отдельный файл |
|
|
102
|
+
|
|
103
|
+
### 2. Типы для TypeScript
|
|
104
|
+
|
|
105
|
+
Добавьте в **`tsconfig.json`**, чтобы TypeScript понимал импорты ассетов:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"compilerOptions": {
|
|
110
|
+
"types": ["vite-smart-img/vite-asset-types"]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Или в **`*.d.ts`** проекта:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
/// <reference types="vite-smart-img/vite-asset-types" />
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
После этого импорты из **`*/assets/*.jpg`**, **`*.png`**, **`*.svg`** имеют тип **`{ src: string; originalW: number; originalH: number }`**, а не просто **`string`**.
|
|
122
|
+
|
|
123
|
+
### 3. Использование SmartImg в компонентах
|
|
124
|
+
|
|
125
|
+
#### Локальный ассет (JPG / PNG / SVG)
|
|
126
|
+
|
|
127
|
+
Импортируйте файл из папки **`assets/`**. Плагины сами добавят метаданные размеров:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import { SmartImg } from 'vite-smart-img'
|
|
131
|
+
import photo from './assets/photo.jpg'
|
|
132
|
+
import logo from './assets/logo.svg'
|
|
133
|
+
|
|
134
|
+
export const MyComponent = () => (
|
|
135
|
+
<div>
|
|
136
|
+
{/* Локальный растр — в prod миниатюры, в dev оригинал */}
|
|
137
|
+
<SmartImg asset={photo} alt="Фотография" opacity />
|
|
138
|
+
|
|
139
|
+
{/* Локальный SVG — всегда полный оригинал */}
|
|
140
|
+
<SmartImg asset={logo} alt="Логотип" opacity />
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### Внешний URL
|
|
146
|
+
|
|
147
|
+
Размеры задаются вручную (плагин на этапе сборки не видит чужие файлы):
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
import { SmartImg } from 'vite-smart-img'
|
|
151
|
+
|
|
152
|
+
export const Banner = () => (
|
|
153
|
+
<SmartImg
|
|
154
|
+
dataSrc="https://example.com/images/banner.jpg"
|
|
155
|
+
originalW={1600}
|
|
156
|
+
originalH={900}
|
|
157
|
+
alt="Баннер"
|
|
158
|
+
opacity
|
|
159
|
+
/>
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### Без плавного появления
|
|
164
|
+
|
|
165
|
+
Не передавайте **`opacity`** — картинка покажется сразу после загрузки:
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
<SmartImg asset={photo} alt="Без fade" />
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### Дополнительный класс обёртки
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
<SmartImg asset={photo} alt="Стили" className="my-image-block" opacity />
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**`className`** вешается на внешний **`<div>`**, не на **`<img>`**. Удобно для размеров и позиционирования:
|
|
178
|
+
|
|
179
|
+
```css
|
|
180
|
+
.my-image-block {
|
|
181
|
+
width: 400px;
|
|
182
|
+
border-radius: 12px;
|
|
183
|
+
overflow: hidden;
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### Соотношение сторон
|
|
188
|
+
|
|
189
|
+
Компонент создаёт контейнер с **`padding-top`** по формуле **`originalH / originalW`**, чтобы до загрузки зарезервировать место и снизить **CLS**. Отдельная высота не обязательна.
|
|
190
|
+
|
|
191
|
+
#### Плейсхолдер и длительность fade
|
|
192
|
+
|
|
193
|
+
Переопределяйте CSS-переменные глобально или локально:
|
|
194
|
+
|
|
195
|
+
```css
|
|
196
|
+
/* глобально */
|
|
197
|
+
:root {
|
|
198
|
+
--smi-bg: #1a1a2e; /* фон до загрузки */
|
|
199
|
+
--smi-fade-duration: 600ms; /* длительность fade, по умолчанию 400ms */
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* область компонента */
|
|
203
|
+
.hero-image {
|
|
204
|
+
--smi-bg: transparent;
|
|
205
|
+
--smi-fade-duration: 200ms;
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## API компонента
|
|
212
|
+
|
|
213
|
+
### Пропсы — локальный ассет
|
|
214
|
+
|
|
215
|
+
| Проп | Тип | Обязательно | Описание |
|
|
216
|
+
|------|-----|-------------|----------|
|
|
217
|
+
| `asset` | `SmartImgImportedAsset` | ✓ | Импорт из **`*/assets/*.jpg \| *.png \| *.svg`** |
|
|
218
|
+
| `alt` | `string` | ✓ | Текст для атрибута **`alt`** |
|
|
219
|
+
| `opacity` | `boolean` | — | Включить плавное появление |
|
|
220
|
+
| `className` | `string` | — | Доп. класс на обёртке **`<div>`** |
|
|
221
|
+
|
|
222
|
+
### Пропсы — внешний URL
|
|
223
|
+
|
|
224
|
+
| Проп | Тип | Обязательно | Описание |
|
|
225
|
+
|------|-----|-------------|----------|
|
|
226
|
+
| `dataSrc` | `string` | ✓ | Полный URL изображения |
|
|
227
|
+
| `originalW` | `number` | ✓ | Ширина оригинала в пикселях |
|
|
228
|
+
| `originalH` | `number` | ✓ | Высота оригинала в пикселях |
|
|
229
|
+
| `alt` | `string` | ✓ | Текст для **`alt`** |
|
|
230
|
+
| `opacity` | `boolean` | — | Плавное появление |
|
|
231
|
+
| `className` | `string` | — | Класс на обёртке **`<div>`** |
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Кастомизация CSS
|
|
236
|
+
|
|
237
|
+
Стили один раз вставляются в **`<head>`** при первом рендере. Доступны две переменные:
|
|
238
|
+
|
|
239
|
+
| Переменная | По умолчанию | Описание |
|
|
240
|
+
|------------|--------------|----------|
|
|
241
|
+
| `--smi-bg` | `#f0f0f0` | Фон плейсхолдера до загрузки |
|
|
242
|
+
| `--smi-fade-duration` | `400ms` | Длительность fade (нужен проп **`opacity`**) |
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Атрибут data-max-size-load
|
|
247
|
+
|
|
248
|
+
После подтверждения оптимального размера загрузки компонент выставляет на **`<img>`** атрибут **`data-max-size-load`**. Удобно для CSS «полностью загружено»:
|
|
249
|
+
|
|
250
|
+
```css
|
|
251
|
+
/* зелёная точка, когда картинка на финальном размере */
|
|
252
|
+
.smi-wrapper:has(img[data-max-size-load]) .my-indicator {
|
|
253
|
+
background: green;
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Возможные значения:**
|
|
258
|
+
|
|
259
|
+
| Значение | Когда |
|
|
260
|
+
|----------|--------|
|
|
261
|
+
| `"dev"` | Режим разработки — загружен оригинал |
|
|
262
|
+
| `"svg"` | SVG загружен полностью |
|
|
263
|
+
| `"external"` | Внешний URL загружен |
|
|
264
|
+
| `"800"` / `"1200"` / … | Prod — подтверждённая ширина миниатюры в px после первого resize |
|
|
265
|
+
| `"Infinity"` | Prod — загружен оригинал (подходящей миниатюры не нашлось) |
|
|
266
|
+
| `"svg-error"` | Ошибка загрузки SVG |
|
|
267
|
+
| `"external-error"` | Ошибка внешнего URL |
|
|
268
|
+
|
|
269
|
+
**Важно:** в production **`data-max-size-load`** выставляется **только после первого события `resize`**, следующего за загрузкой. Так учитываются смена ориентации и размера viewport. Повторный монтирование компонента **само по себе** этот атрибут не восстанавливает — нужен новый **resize**.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Как это работает — подробно
|
|
274
|
+
|
|
275
|
+
### Схема загрузки
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
Монтирование компонента
|
|
279
|
+
│
|
|
280
|
+
├─ Попадание в кэш? → мгновенно восстановить src (без fade, без IO)
|
|
281
|
+
│
|
|
282
|
+
└─ Нет → регистрация в IntersectionObserver (порог 5%)
|
|
283
|
+
│
|
|
284
|
+
└─ Видимость ≥ 5%
|
|
285
|
+
│
|
|
286
|
+
├─ SVG / внешний URL → loadDirectFinal → data-max-size-load="svg|external"
|
|
287
|
+
│
|
|
288
|
+
├─ Dev (JPG/PNG) → оригинал → data-max-size-load="dev"
|
|
289
|
+
│
|
|
290
|
+
└─ Prod (JPG/PNG) → pickSize(cssWidth × DPR, originalW)
|
|
291
|
+
→ миниатюра
|
|
292
|
+
│
|
|
293
|
+
└─ При resize (debounce 200ms)
|
|
294
|
+
→ повторное наблюдение видимых
|
|
295
|
+
→ при необходимости апгрейд миниатюры
|
|
296
|
+
→ data-max-size-load="<size>"
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Выбор миниатюры (`pickSize`)
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
target = ceil(cssWidth × devicePixelRatio)
|
|
303
|
+
первый размер из [400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000,
|
|
304
|
+
2500, 3000, 3500, 4000, 5000, 6000, 7000, 8000],
|
|
305
|
+
где size >= target И size <= originalW
|
|
306
|
+
|
|
307
|
+
→ null: грузить оригинальный файл
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Структура выхода сборки
|
|
311
|
+
|
|
312
|
+
Для **`photo.jpg`** (оригинальная ширина 2400px) плагин кладёт в **`dist`**:
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
dist/assets/img/
|
|
316
|
+
a1b2c3d4.jpg ← очищенный оригинал
|
|
317
|
+
a1b2c3d4-400.jpg ← миниатюра 400px, progressive JPEG
|
|
318
|
+
a1b2c3d4-400.webp
|
|
319
|
+
a1b2c3d4-600.jpg
|
|
320
|
+
a1b2c3d4-600.webp
|
|
321
|
+
…
|
|
322
|
+
a1b2c3d4-2000.jpg
|
|
323
|
+
a1b2c3d4-2000.webp
|
|
324
|
+
(размеры шире 2400px не генерируются)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Модуль импорта:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
{ src: '/assets/img/a1b2c3d4.jpg', originalW: 2400, originalH: 1600 }
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Кэш сессии
|
|
334
|
+
|
|
335
|
+
**`loadedCache`** — **`Map<dataSrc, { url, size }>`** на время сессии страницы. При повторном монтировании **SmartImg**:
|
|
336
|
+
|
|
337
|
+
1. **`useLayoutEffect`** синхронно читает кэш до первого paint.
|
|
338
|
+
2. **`imgSrc`** сразу из кэша — без вспышки пустоты и без fade.
|
|
339
|
+
3. Второй экземпляр того же изображения, ещё не загрузившийся, при появлении в viewport может получить fade.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## ⚠️ Исходные файлы WebP — не поддерживаются
|
|
344
|
+
|
|
345
|
+
**Не импортируйте `.webp` как ассеты.** Обрабатываются только **`.jpg`**, **`.jpeg`**, **`.png`**, **`.svg`**.
|
|
346
|
+
|
|
347
|
+
Если импортировать **`.webp`** из **`assets/`**:
|
|
348
|
+
|
|
349
|
+
- **dev**: плагин **`smartImgLocalAssetImport`** не перехватит импорт → Vite отдаст обычную **строку URL**, не объект **`{ src, originalW, originalH }`** — компонент сломается.
|
|
350
|
+
- **build**: **`imageThumbnails`** не трогает **`.webp`**; файл пойдёт в стандартный пайплайн Vite (возможен inline base64 и отсутствие метаданных размеров).
|
|
351
|
+
|
|
352
|
+
**Используйте JPG или PNG как исходники.** WebP для браузеров пакет соберёт сам.
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Отладочные атрибуты
|
|
357
|
+
|
|
358
|
+
Каждый **`<img>`** от **SmartImg** в DevTools имеет:
|
|
359
|
+
|
|
360
|
+
| Атрибут | Смысл |
|
|
361
|
+
|---------|--------|
|
|
362
|
+
| `data-src` | Исходный **`dataSrc`** (файл-оригинал, не URL миниатюры) |
|
|
363
|
+
| `data-original-w` | Ширина оригинала |
|
|
364
|
+
| `data-original-h` | Высота оригинала |
|
|
365
|
+
|
|
366
|
+
Только метаданные для отладки; логика компонента на них не опирается.
|
package/dist/index.js
CHANGED
|
@@ -308,6 +308,7 @@ var SmartImg = (props) => {
|
|
|
308
308
|
);
|
|
309
309
|
const paddingTop = `${(originalH / originalW * 100).toFixed(4)}%`;
|
|
310
310
|
useLayoutEffect(() => {
|
|
311
|
+
imgRef.current?.removeAttribute("data-max-size-load");
|
|
311
312
|
const hit = getLoadedUrl(dataSrc);
|
|
312
313
|
if (!hit) return;
|
|
313
314
|
hasRealSrc.current = true;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/component/SmartImg.tsx","../src/component/smartImgManager.ts","../src/component/styles.ts"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from 'react'\r\nimport type { SmartImgImportedAsset } from '../smartImgAsset'\r\nimport { register, getLoadedUrl } from './smartImgManager'\r\nimport { injectStyles, cls } from './styles'\r\n\r\n/** 1×1 прозрачный GIF — начальный src пока картинка не загружена */\r\nconst BLANK = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'\r\n\r\n/** SVG-заглушка при ошибке загрузки */\r\nconst ERROR_PLACEHOLDER =\r\n \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect width='24' height='24' fill='%2312121c'/%3E%3Cpath fill='%23444466' d='M21 5v6.59l-3-3.01-4 4.01-4-4-4 3.99-3-2.99V5c0-1.1.9-2 2-2h14c1.1 0 2 .9 2 2zm-3 6.42 3 3.01V19c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2v-6.58l3 2.99 4-4 4 4 4-4.99z'/%3E%3C/svg%3E\"\r\n\r\ntype SmartImgCommon = {\r\n alt: string\r\n /**\r\n * Передай `opacity`, чтобы картинка появлялась плавно (fade-in).\r\n * Без этого пропа — появляется мгновенно.\r\n * При перемонтировании с закешированной картинкой fade не запускается.\r\n */\r\n opacity?: boolean\r\n /** Доп. класс на обёртку. */\r\n className?: string\r\n}\r\n\r\nexport type SmartImgProps = SmartImgCommon &\r\n (\r\n | {\r\n /** Импорт из `.../assets/*` через плагины пакета (`{ src, originalW, originalH }`). */\r\n asset: SmartImgImportedAsset\r\n }\r\n | {\r\n /** Прямой URL (часто внешний) — размеры задаёшь вручную. */\r\n dataSrc: string\r\n originalW: number\r\n originalH: number\r\n }\r\n )\r\n\r\n/**\r\n * «Умная» ленивая загрузка изображений (SmartImg).\r\n *\r\n * - Локальные ассеты: проп **`asset`** из импорта с метаданными из Vite.\r\n * - Внешние URL: **`dataSrc`** + **`originalW`** / **`originalH`**.\r\n */\r\nexport const SmartImg = (props: SmartImgProps) => {\r\n injectStyles()\r\n\r\n const { alt, opacity, className } = props\r\n const { dataSrc, originalW, originalH } =\r\n 'asset' in props\r\n ? {\r\n dataSrc: props.asset.src,\r\n originalW: props.asset.originalW,\r\n originalH: props.asset.originalH,\r\n }\r\n : {\r\n dataSrc: props.dataSrc,\r\n originalW: props.originalW,\r\n originalH: props.originalH,\r\n }\r\n\r\n const wrapperRef = useRef<HTMLDivElement>(null)\r\n const imgRef = useRef<HTMLImageElement>(null)\r\n\r\n const [imgSrc, setImgSrc] = useState<string>(() => {\r\n if (typeof window === 'undefined') return BLANK\r\n return getLoadedUrl(dataSrc)?.url ?? BLANK\r\n })\r\n const [isVisible, setIsVisible] = useState<boolean>(() => {\r\n if (typeof window === 'undefined') return false\r\n return getLoadedUrl(dataSrc) !== null\r\n })\r\n const [isError, setIsError] = useState(false)\r\n\r\n const hasRealSrc = useRef(\r\n typeof window !== 'undefined' && getLoadedUrl(dataSrc) !== null,\r\n )\r\n\r\n const paddingTop = `${((originalH / originalW) * 100).toFixed(4)}%`\r\n\r\n useLayoutEffect(() => {\r\n const hit = getLoadedUrl(dataSrc)\r\n if (!hit) return\r\n hasRealSrc.current = true\r\n setImgSrc(hit.url)\r\n setIsVisible(true)\r\n setIsError(false)\r\n }, [dataSrc])\r\n\r\n useEffect(() => {\r\n const img = imgRef.current\r\n const wrapper = wrapperRef.current\r\n if (!img || !wrapper) return\r\n\r\n if (!getLoadedUrl(dataSrc)) {\r\n hasRealSrc.current = false\r\n setImgSrc(BLANK)\r\n setIsVisible(false)\r\n setIsError(false)\r\n }\r\n\r\n return register(\r\n img,\r\n wrapper,\r\n dataSrc,\r\n originalW,\r\n (url) => {\r\n hasRealSrc.current = true\r\n setIsError(false)\r\n setImgSrc(url)\r\n if (!opacity) setIsVisible(true)\r\n },\r\n () => {\r\n hasRealSrc.current = true\r\n setIsError(true)\r\n setImgSrc(ERROR_PLACEHOLDER)\r\n setIsVisible(true)\r\n },\r\n )\r\n }, [dataSrc, opacity, originalW, originalH])\r\n\r\n const imageClassName = [\r\n cls.image,\r\n opacity ? cls.fade : '',\r\n isVisible ? cls.visible : '',\r\n isError ? cls.error : '',\r\n ]\r\n .filter(Boolean)\r\n .join(' ')\r\n\r\n return (\r\n <div\r\n ref={wrapperRef}\r\n className={`${cls.wrapper}${className ? ` ${className}` : ''}`}\r\n style={{ paddingTop }}\r\n >\r\n <img\r\n ref={imgRef}\r\n src={imgSrc}\r\n alt={alt}\r\n data-src={dataSrc}\r\n data-original-w={originalW}\r\n data-original-h={originalH}\r\n className={imageClassName}\r\n onLoad={() => {\r\n if (opacity && hasRealSrc.current) {\r\n setIsVisible(true)\r\n }\r\n }}\r\n decoding=\"async\"\r\n />\r\n </div>\r\n )\r\n}\r\n","/**\r\n * Singleton-менеджер ленивой загрузки изображений (SmartImg).\r\n *\r\n * Один IntersectionObserver и один resize-listener на всю страницу.\r\n * Все компоненты SmartImg регистрируются здесь через register().\r\n */\r\n\r\n// ─── Режим сборки ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * В dev-режиме миниатюр нет — грузим оригинал напрямую.\r\n * Вся логика выбора размера, WebP и resize-апгрейдов пропускается.\r\n * Значение подставляется Vite из import.meta.env.DEV в проекте-потребителе.\r\n */\r\nconst IS_DEV: boolean =\r\n typeof import.meta !== 'undefined' &&\r\n typeof (import.meta as Record<string, unknown>).env === 'object' &&\r\n ((import.meta as { env: Record<string, unknown> }).env['DEV'] as boolean | undefined) === true\r\n\r\n// ─── Доступные размеры миниатюр (ширина в пикселях) ──────────────────────────\r\n\r\nconst SIZES = [400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000, 2500, 3000, 3500, 4000, 5000, 6000, 7000, 8000] as const\r\n\r\n// ─── WebP detection (однократная синхронная проверка через canvas) ────────────\r\n\r\nlet _webpResult: boolean | null = null\r\n\r\nfunction supportsWebP(): boolean {\r\n if (_webpResult !== null) return _webpResult\r\n try {\r\n const c = document.createElement('canvas')\r\n c.width = 1\r\n c.height = 1\r\n _webpResult = c.toDataURL('image/webp').startsWith('data:image/webp')\r\n } catch {\r\n _webpResult = false\r\n }\r\n return _webpResult\r\n}\r\n\r\n// ─── URL helpers ──────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Выбирает размер миниатюры с учётом нужного размера И оригинальных размеров.\r\n *\r\n * Правило: берём первый size из SIZES такой что:\r\n * size >= target (покрывает нужное разрешение)\r\n * size <= originalW (миниатюра с таким размером физически существует)\r\n *\r\n * null означает: грузить оригинальный dataSrc напрямую.\r\n */\r\nfunction pickSize(cssWidth: number, dpr: number, originalW: number): number | null {\r\n const target = Math.ceil(cssWidth * dpr)\r\n return SIZES.find((s) => s >= target && s <= originalW) ?? null\r\n}\r\n\r\n/**\r\n * Строит URL миниатюры из оригинального URL.\r\n * photo.jpg + 1200 + webp → photo-1200.webp\r\n * photo.jpg + 1200 + false → photo-1200.jpg\r\n */\r\nfunction buildUrl(dataSrc: string, size: number, webp: boolean): string {\r\n const base = dataSrc.replace(/\\.[^./]+$/, '')\r\n const originalExt = dataSrc.match(/\\.([^./]+)$/)?.[1] ?? 'jpg'\r\n return `${base}-${size}.${webp ? 'webp' : originalExt}`\r\n}\r\n\r\n/**\r\n * SVG: миниатюры и плагин нарезки не применяются — только оригинал.\r\n * data:... и путь с `.svg` до query/hash.\r\n */\r\nfunction isSvgDataSrc(url: string): boolean {\r\n if (url == null || typeof url !== 'string') return false\r\n if (url.startsWith('data:image/svg')) return true\r\n const base = url.replace(/[?#].*$/, '')\r\n return base.toLowerCase().endsWith('.svg')\r\n}\r\n\r\n/**\r\n * Абсолютный URL с origin, отличным от страницы: нет наших прегенерённых миниатюр.\r\n * Относительные (`/`, `./`, `../`), `data:` и `blob:` не считаем внешними.\r\n */\r\nfunction isExternalDataSrc(url: string): boolean {\r\n if (!url || url.startsWith('data:') || url.startsWith('blob:')) return false\r\n if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) return false\r\n try {\r\n const resolved = new URL(url, window.location.href)\r\n return resolved.origin !== window.location.origin\r\n } catch {\r\n return false\r\n }\r\n}\r\n\r\n/** Картинка без цепочки миниатюр SmartImg: сразу оригинал и финальная пометка. */\r\nfunction directLoadKind(dataSrc: string): 'svg' | 'external' | null {\r\n if (isSvgDataSrc(dataSrc)) return 'svg'\r\n if (isExternalDataSrc(dataSrc)) return 'external'\r\n return null\r\n}\r\n\r\n/**\r\n * Загрузка только `dataSrc`, без buildUrl / выбора размера / WebP.\r\n * Вызывается из handleIntersect (после пересечения viewport, threshold 5%).\r\n * На успех: кэш, onLoad, `data-max-size-load` = svg | external.\r\n */\r\nfunction loadDirectFinal(img: HTMLImageElement, state: Entry, kind: 'svg' | 'external'): void {\r\n const url = state.dataSrc\r\n const tempImg = new Image()\r\n tempImg.onload = () => {\r\n state.loadedSize = Infinity\r\n loadedCache.set(state.dataSrc, { url, size: Infinity })\r\n state.onLoad(url)\r\n img.setAttribute('data-max-size-load', kind)\r\n }\r\n tempImg.onerror = () => {\r\n state.onError()\r\n img.setAttribute('data-max-size-load', `${kind}-error`)\r\n }\r\n tempImg.src = url\r\n}\r\n\r\n/**\r\n * Нужен ли IntersectionObserver для этого dataSrc: false = уже есть подходящая\r\n * версия в loadedCache, повторный `new Image()` и ожидание не требуются.\r\n */\r\nfunction needsIntersectionObserver(\r\n dataSrc: string,\r\n originalW: number,\r\n wrapper: HTMLElement,\r\n cached: CacheEntry | undefined,\r\n): boolean {\r\n if (!cached) return true\r\n if (directLoadKind(dataSrc)) return false\r\n if (IS_DEV) return false\r\n const cssWidth = wrapper.clientWidth || wrapper.offsetWidth\r\n if (cssWidth === 0) return true\r\n const neededSize = pickSize(cssWidth, window.devicePixelRatio ?? 1, originalW)\r\n const loaded = cached.size\r\n if (neededSize === null) {\r\n return loaded !== Infinity\r\n }\r\n return neededSize > loaded\r\n}\r\n\r\n/** Пометить завершённую загрузку по данным кэша (без повторного IO). */\r\nfunction applyCachedCompleteMarker(img: HTMLImageElement, dataSrc: string): void {\r\n if (img.hasAttribute('data-max-size-load')) return\r\n const direct = directLoadKind(dataSrc)\r\n if (direct) {\r\n img.setAttribute('data-max-size-load', direct)\r\n return\r\n }\r\n if (IS_DEV) {\r\n img.setAttribute('data-max-size-load', 'dev')\r\n return\r\n }\r\n // prod + локальный JPG: маркер ставится только через handleResize() → handleIntersect(),\r\n // не при ре-регистрации компонента (ре-рендер не является ресайзом)\r\n}\r\n\r\n// ─── Кэш загруженных URL ──────────────────────────────────────────────────────\r\n\r\ntype CacheEntry = {\r\n /** Финальный URL который был подставлен в src (миниатюра или оригинал) */\r\n url: string\r\n /** Размер загруженной версии. Infinity = оригинал */\r\n size: number\r\n}\r\n\r\n/**\r\n * Модульный кэш: dataSrc → последний успешно загруженный url + size.\r\n * Живёт в рамках сессии страницы. При перемонтировании компонента\r\n * позволяет сразу показать картинку без fade, не дожидаясь IO.\r\n */\r\nconst loadedCache = new Map<string, CacheEntry>()\r\n\r\n/** Возвращает кэшированный результат для dataSrc, или null если ещё не грузили. */\r\nexport function getLoadedUrl(dataSrc: string): CacheEntry | null {\r\n return loadedCache.get(dataSrc) ?? null\r\n}\r\n\r\n// ─── Типы ─────────────────────────────────────────────────────────────────────\r\n\r\ntype Entry = {\r\n wrapper: HTMLElement\r\n dataSrc: string\r\n /** Оригинальная ширина картинки — ограничивает максимально доступный размер миниатюры */\r\n originalW: number\r\n /** Размер уже загруженной версии. null — картинка ещё не загружалась. */\r\n loadedSize: number | null\r\n /**\r\n * true — картинка была повторно добавлена в observer после resize,\r\n * ждём подтверждения видимости чтобы проверить нужен ли апгрейд.\r\n */\r\n pendingResizeCheck: boolean\r\n onLoad: (url: string) => void\r\n onError: () => void\r\n}\r\n\r\n// ─── Singleton state ──────────────────────────────────────────────────────────\r\n\r\nconst registry = new Map<HTMLImageElement, Entry>()\r\n\r\n/** Элементы, которые сейчас отслеживает IntersectionObserver. */\r\nconst observing = new Set<HTMLImageElement>()\r\n\r\nlet ioInstance: IntersectionObserver | null = null\r\nlet resizeTimer: ReturnType<typeof setTimeout> | null = null\r\nlet resizeAttached = false\r\n\r\n// ─── IntersectionObserver ─────────────────────────────────────────────────────\r\n\r\nfunction getIO(): IntersectionObserver {\r\n if (!ioInstance) {\r\n ioInstance = new IntersectionObserver(handleIntersect, { threshold: 0.05 })\r\n }\r\n return ioInstance\r\n}\r\n\r\n/** Добавляет в observer без дублей. */\r\nfunction safeObserve(img: HTMLImageElement): void {\r\n if (observing.has(img)) return\r\n observing.add(img)\r\n getIO().observe(img)\r\n}\r\n\r\nfunction safeUnobserve(img: HTMLImageElement): void {\r\n if (!observing.has(img)) return\r\n observing.delete(img)\r\n getIO().unobserve(img)\r\n}\r\n\r\nfunction handleIntersect(entries: IntersectionObserverEntry[]): void {\r\n for (const ioEntry of entries) {\r\n if (!ioEntry.isIntersecting) continue\r\n\r\n const img = ioEntry.target as HTMLImageElement\r\n const state = registry.get(img)\r\n if (!state) continue\r\n\r\n safeUnobserve(img)\r\n\r\n const direct = directLoadKind(state.dataSrc)\r\n if (direct) {\r\n loadDirectFinal(img, state, direct)\r\n continue\r\n }\r\n\r\n // ── Dev mode: миниатюр нет, грузим оригинал ──────────────────────────────\r\n if (IS_DEV) {\r\n const hit = loadedCache.get(state.dataSrc)\r\n if (hit) {\r\n state.loadedSize = hit.size\r\n // onLoad всегда вызываем: если компонент уже показывал картинку (imgSrc = реальный URL),\r\n // React получит то же значение → state не обновится → img.src не изменится → fade не мигнёт.\r\n // Если компонент ещё не грузил (imgSrc = BLANK, другой экземпляр) → src сменится → fade-in.\r\n state.onLoad(hit.url)\r\n if (!img.hasAttribute('data-max-size-load')) {\r\n img.setAttribute('data-max-size-load', 'dev')\r\n }\r\n continue\r\n }\r\n const url = state.dataSrc\r\n const tempImg = new Image()\r\n tempImg.onload = () => {\r\n state.loadedSize = Infinity\r\n loadedCache.set(state.dataSrc, { url, size: Infinity })\r\n state.onLoad(url)\r\n img.setAttribute('data-max-size-load', 'dev')\r\n }\r\n tempImg.onerror = () => state.onError()\r\n tempImg.src = url\r\n continue\r\n }\r\n\r\n const cssWidth = state.wrapper.clientWidth || state.wrapper.offsetWidth\r\n if (cssWidth === 0) {\r\n safeObserve(img)\r\n continue\r\n }\r\n\r\n const neededSize = pickSize(cssWidth, window.devicePixelRatio ?? 1, state.originalW)\r\n const isResizeCheck = state.pendingResizeCheck\r\n state.pendingResizeCheck = false\r\n\r\n const url = neededSize === null\r\n ? state.dataSrc\r\n : buildUrl(state.dataSrc, neededSize, supportsWebP())\r\n\r\n if (isResizeCheck && neededSize !== null && state.loadedSize !== null && neededSize <= state.loadedSize) {\r\n img.setAttribute('data-max-size-load', String(state.loadedSize))\r\n continue\r\n }\r\n\r\n const tempImg = new Image()\r\n\r\n tempImg.onload = () => {\r\n state.loadedSize = neededSize ?? Infinity\r\n loadedCache.set(state.dataSrc, { url, size: state.loadedSize })\r\n state.onLoad(url)\r\n if (isResizeCheck || neededSize === null) {\r\n img.setAttribute('data-max-size-load', String(state.loadedSize))\r\n }\r\n }\r\n\r\n tempImg.onerror = () => {\r\n state.onError()\r\n if (isResizeCheck || neededSize === null) {\r\n img.setAttribute('data-max-size-load', String(state.loadedSize ?? 0))\r\n }\r\n }\r\n\r\n tempImg.src = url\r\n }\r\n}\r\n\r\n// ─── Resize handler ───────────────────────────────────────────────────────────\r\n\r\nfunction handleResize(): void {\r\n if (IS_DEV) return\r\n\r\n for (const [img, state] of registry) {\r\n if (state.loadedSize === null) continue\r\n if (img.hasAttribute('data-max-size-load')) continue\r\n\r\n state.pendingResizeCheck = true\r\n safeObserve(img)\r\n }\r\n}\r\n\r\nfunction attachResizeListener(): void {\r\n if (resizeAttached) return\r\n resizeAttached = true\r\n window.addEventListener(\r\n 'resize',\r\n () => {\r\n if (resizeTimer) clearTimeout(resizeTimer)\r\n resizeTimer = setTimeout(handleResize, 200)\r\n },\r\n { passive: true },\r\n )\r\n}\r\n\r\n// ─── Public API ───────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Регистрирует img-элемент в менеджере ленивой загрузки.\r\n * Возвращает функцию очистки — вызвать при размонтировании компонента.\r\n */\r\nexport function register(\r\n img: HTMLImageElement,\r\n wrapper: HTMLElement,\r\n dataSrc: string,\r\n originalW: number,\r\n onLoad: (url: string) => void,\r\n onError: () => void,\r\n): () => void {\r\n if (dataSrc == null || dataSrc === '' || typeof dataSrc !== 'string') {\r\n return () => {\r\n safeUnobserve(img)\r\n registry.delete(img)\r\n }\r\n }\r\n\r\n attachResizeListener()\r\n\r\n const cached = loadedCache.get(dataSrc)\r\n const observe = needsIntersectionObserver(dataSrc, originalW, wrapper, cached)\r\n // pendingResizeCheck = false: регистрация (в т.ч. при ре-рендере) — не ресайз.\r\n // Флаг true выставляет только handleResize().\r\n const pendingResizeCheck = false\r\n\r\n registry.set(img, {\r\n wrapper,\r\n dataSrc,\r\n originalW,\r\n loadedSize: cached?.size ?? null,\r\n pendingResizeCheck,\r\n onLoad,\r\n onError,\r\n })\r\n\r\n const cleanup = () => {\r\n safeUnobserve(img)\r\n registry.delete(img)\r\n }\r\n\r\n if (cached && !observe) {\r\n applyCachedCompleteMarker(img, dataSrc)\r\n return cleanup\r\n }\r\n\r\n safeObserve(img)\r\n\r\n return cleanup\r\n}\r\n","/**\n * Runtime CSS injection for SmartImg.\n * Injects styles once into <head> on first component use.\n * Uses `smi-` prefix to avoid class name collisions.\n */\n\nconst CSS = `\n.smi-wrapper {\n position: relative;\n width: 100%;\n height: 0;\n overflow: hidden;\n background-color: var(--smi-bg, #f0f0f0);\n}\n.smi-image {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n.smi-fade {\n opacity: 0;\n transition: opacity var(--smi-fade-duration, 400ms) ease;\n}\n.smi-fade.smi-visible {\n opacity: 1;\n}\n.smi-error {\n object-fit: contain;\n padding: 15%;\n}\n`\n\nlet injected = false\n\nexport function injectStyles(): void {\n if (typeof document === 'undefined' || injected) return\n injected = true\n const style = document.createElement('style')\n style.setAttribute('data-smi', '')\n style.textContent = CSS\n document.head.appendChild(style)\n}\n\nexport const cls = {\n wrapper: 'smi-wrapper',\n image: 'smi-image',\n fade: 'smi-fade',\n visible: 'smi-visible',\n error: 'smi-error',\n} as const\n"],"mappings":"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","names":["url","tempImg"]}
|
|
1
|
+
{"version":3,"sources":["../src/component/SmartImg.tsx","../src/component/smartImgManager.ts","../src/component/styles.ts"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from 'react'\r\nimport type { SmartImgImportedAsset } from '../smartImgAsset'\r\nimport { register, getLoadedUrl } from './smartImgManager'\r\nimport { injectStyles, cls } from './styles'\r\n\r\n/** 1×1 прозрачный GIF — начальный src пока картинка не загружена */\r\nconst BLANK = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'\r\n\r\n/** SVG-заглушка при ошибке загрузки */\r\nconst ERROR_PLACEHOLDER =\r\n \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect width='24' height='24' fill='%2312121c'/%3E%3Cpath fill='%23444466' d='M21 5v6.59l-3-3.01-4 4.01-4-4-4 3.99-3-2.99V5c0-1.1.9-2 2-2h14c1.1 0 2 .9 2 2zm-3 6.42 3 3.01V19c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2v-6.58l3 2.99 4-4 4 4 4-4.99z'/%3E%3C/svg%3E\"\r\n\r\ntype SmartImgCommon = {\r\n alt: string\r\n /**\r\n * Передай `opacity`, чтобы картинка появлялась плавно (fade-in).\r\n * Без этого пропа — появляется мгновенно.\r\n * При перемонтировании с закешированной картинкой fade не запускается.\r\n */\r\n opacity?: boolean\r\n /** Доп. класс на обёртку. */\r\n className?: string\r\n}\r\n\r\nexport type SmartImgProps = SmartImgCommon &\r\n (\r\n | {\r\n /** Импорт из `.../assets/*` через плагины пакета (`{ src, originalW, originalH }`). */\r\n asset: SmartImgImportedAsset\r\n }\r\n | {\r\n /** Прямой URL (часто внешний) — размеры задаёшь вручную. */\r\n dataSrc: string\r\n originalW: number\r\n originalH: number\r\n }\r\n )\r\n\r\n/**\r\n * «Умная» ленивая загрузка изображений (SmartImg).\r\n *\r\n * - Локальные ассеты: проп **`asset`** из импорта с метаданными из Vite.\r\n * - Внешние URL: **`dataSrc`** + **`originalW`** / **`originalH`**.\r\n */\r\nexport const SmartImg = (props: SmartImgProps) => {\r\n injectStyles()\r\n\r\n const { alt, opacity, className } = props\r\n const { dataSrc, originalW, originalH } =\r\n 'asset' in props\r\n ? {\r\n dataSrc: props.asset.src,\r\n originalW: props.asset.originalW,\r\n originalH: props.asset.originalH,\r\n }\r\n : {\r\n dataSrc: props.dataSrc,\r\n originalW: props.originalW,\r\n originalH: props.originalH,\r\n }\r\n\r\n const wrapperRef = useRef<HTMLDivElement>(null)\r\n const imgRef = useRef<HTMLImageElement>(null)\r\n\r\n const [imgSrc, setImgSrc] = useState<string>(() => {\r\n if (typeof window === 'undefined') return BLANK\r\n return getLoadedUrl(dataSrc)?.url ?? BLANK\r\n })\r\n const [isVisible, setIsVisible] = useState<boolean>(() => {\r\n if (typeof window === 'undefined') return false\r\n return getLoadedUrl(dataSrc) !== null\r\n })\r\n const [isError, setIsError] = useState(false)\r\n\r\n const hasRealSrc = useRef(\r\n typeof window !== 'undefined' && getLoadedUrl(dataSrc) !== null,\r\n )\r\n\r\n const paddingTop = `${((originalH / originalW) * 100).toFixed(4)}%`\r\n\r\n useLayoutEffect(() => {\r\n imgRef.current?.removeAttribute('data-max-size-load')\r\n const hit = getLoadedUrl(dataSrc)\r\n if (!hit) return\r\n hasRealSrc.current = true\r\n setImgSrc(hit.url)\r\n setIsVisible(true)\r\n setIsError(false)\r\n }, [dataSrc])\r\n\r\n useEffect(() => {\r\n const img = imgRef.current\r\n const wrapper = wrapperRef.current\r\n if (!img || !wrapper) return\r\n\r\n if (!getLoadedUrl(dataSrc)) {\r\n hasRealSrc.current = false\r\n setImgSrc(BLANK)\r\n setIsVisible(false)\r\n setIsError(false)\r\n }\r\n\r\n return register(\r\n img,\r\n wrapper,\r\n dataSrc,\r\n originalW,\r\n (url) => {\r\n hasRealSrc.current = true\r\n setIsError(false)\r\n setImgSrc(url)\r\n if (!opacity) setIsVisible(true)\r\n },\r\n () => {\r\n hasRealSrc.current = true\r\n setIsError(true)\r\n setImgSrc(ERROR_PLACEHOLDER)\r\n setIsVisible(true)\r\n },\r\n )\r\n }, [dataSrc, opacity, originalW, originalH])\r\n\r\n const imageClassName = [\r\n cls.image,\r\n opacity ? cls.fade : '',\r\n isVisible ? cls.visible : '',\r\n isError ? cls.error : '',\r\n ]\r\n .filter(Boolean)\r\n .join(' ')\r\n\r\n return (\r\n <div\r\n ref={wrapperRef}\r\n className={`${cls.wrapper}${className ? ` ${className}` : ''}`}\r\n style={{ paddingTop }}\r\n >\r\n <img\r\n ref={imgRef}\r\n src={imgSrc}\r\n alt={alt}\r\n data-src={dataSrc}\r\n data-original-w={originalW}\r\n data-original-h={originalH}\r\n className={imageClassName}\r\n onLoad={() => {\r\n if (opacity && hasRealSrc.current) {\r\n setIsVisible(true)\r\n }\r\n }}\r\n decoding=\"async\"\r\n />\r\n </div>\r\n )\r\n}\r\n","/**\r\n * Singleton-менеджер ленивой загрузки изображений (SmartImg).\r\n *\r\n * Один IntersectionObserver и один resize-listener на всю страницу.\r\n * Все компоненты SmartImg регистрируются здесь через register().\r\n */\r\n\r\n// ─── Режим сборки ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * В dev-режиме миниатюр нет — грузим оригинал напрямую.\r\n * Вся логика выбора размера, WebP и resize-апгрейдов пропускается.\r\n * Значение подставляется Vite из import.meta.env.DEV в проекте-потребителе.\r\n */\r\nconst IS_DEV: boolean =\r\n typeof import.meta !== 'undefined' &&\r\n typeof (import.meta as Record<string, unknown>).env === 'object' &&\r\n ((import.meta as { env: Record<string, unknown> }).env['DEV'] as boolean | undefined) === true\r\n\r\n// ─── Доступные размеры миниатюр (ширина в пикселях) ──────────────────────────\r\n\r\nconst SIZES = [400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000, 2500, 3000, 3500, 4000, 5000, 6000, 7000, 8000] as const\r\n\r\n// ─── WebP detection (однократная синхронная проверка через canvas) ────────────\r\n\r\nlet _webpResult: boolean | null = null\r\n\r\nfunction supportsWebP(): boolean {\r\n if (_webpResult !== null) return _webpResult\r\n try {\r\n const c = document.createElement('canvas')\r\n c.width = 1\r\n c.height = 1\r\n _webpResult = c.toDataURL('image/webp').startsWith('data:image/webp')\r\n } catch {\r\n _webpResult = false\r\n }\r\n return _webpResult\r\n}\r\n\r\n// ─── URL helpers ──────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Выбирает размер миниатюры с учётом нужного размера И оригинальных размеров.\r\n *\r\n * Правило: берём первый size из SIZES такой что:\r\n * size >= target (покрывает нужное разрешение)\r\n * size <= originalW (миниатюра с таким размером физически существует)\r\n *\r\n * null означает: грузить оригинальный dataSrc напрямую.\r\n */\r\nfunction pickSize(cssWidth: number, dpr: number, originalW: number): number | null {\r\n const target = Math.ceil(cssWidth * dpr)\r\n return SIZES.find((s) => s >= target && s <= originalW) ?? null\r\n}\r\n\r\n/**\r\n * Строит URL миниатюры из оригинального URL.\r\n * photo.jpg + 1200 + webp → photo-1200.webp\r\n * photo.jpg + 1200 + false → photo-1200.jpg\r\n */\r\nfunction buildUrl(dataSrc: string, size: number, webp: boolean): string {\r\n const base = dataSrc.replace(/\\.[^./]+$/, '')\r\n const originalExt = dataSrc.match(/\\.([^./]+)$/)?.[1] ?? 'jpg'\r\n return `${base}-${size}.${webp ? 'webp' : originalExt}`\r\n}\r\n\r\n/**\r\n * SVG: миниатюры и плагин нарезки не применяются — только оригинал.\r\n * data:... и путь с `.svg` до query/hash.\r\n */\r\nfunction isSvgDataSrc(url: string): boolean {\r\n if (url == null || typeof url !== 'string') return false\r\n if (url.startsWith('data:image/svg')) return true\r\n const base = url.replace(/[?#].*$/, '')\r\n return base.toLowerCase().endsWith('.svg')\r\n}\r\n\r\n/**\r\n * Абсолютный URL с origin, отличным от страницы: нет наших прегенерённых миниатюр.\r\n * Относительные (`/`, `./`, `../`), `data:` и `blob:` не считаем внешними.\r\n */\r\nfunction isExternalDataSrc(url: string): boolean {\r\n if (!url || url.startsWith('data:') || url.startsWith('blob:')) return false\r\n if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) return false\r\n try {\r\n const resolved = new URL(url, window.location.href)\r\n return resolved.origin !== window.location.origin\r\n } catch {\r\n return false\r\n }\r\n}\r\n\r\n/** Картинка без цепочки миниатюр SmartImg: сразу оригинал и финальная пометка. */\r\nfunction directLoadKind(dataSrc: string): 'svg' | 'external' | null {\r\n if (isSvgDataSrc(dataSrc)) return 'svg'\r\n if (isExternalDataSrc(dataSrc)) return 'external'\r\n return null\r\n}\r\n\r\n/**\r\n * Загрузка только `dataSrc`, без buildUrl / выбора размера / WebP.\r\n * Вызывается из handleIntersect (после пересечения viewport, threshold 5%).\r\n * На успех: кэш, onLoad, `data-max-size-load` = svg | external.\r\n */\r\nfunction loadDirectFinal(img: HTMLImageElement, state: Entry, kind: 'svg' | 'external'): void {\r\n const url = state.dataSrc\r\n const tempImg = new Image()\r\n tempImg.onload = () => {\r\n state.loadedSize = Infinity\r\n loadedCache.set(state.dataSrc, { url, size: Infinity })\r\n state.onLoad(url)\r\n img.setAttribute('data-max-size-load', kind)\r\n }\r\n tempImg.onerror = () => {\r\n state.onError()\r\n img.setAttribute('data-max-size-load', `${kind}-error`)\r\n }\r\n tempImg.src = url\r\n}\r\n\r\n/**\r\n * Нужен ли IntersectionObserver для этого dataSrc: false = уже есть подходящая\r\n * версия в loadedCache, повторный `new Image()` и ожидание не требуются.\r\n */\r\nfunction needsIntersectionObserver(\r\n dataSrc: string,\r\n originalW: number,\r\n wrapper: HTMLElement,\r\n cached: CacheEntry | undefined,\r\n): boolean {\r\n if (!cached) return true\r\n if (directLoadKind(dataSrc)) return false\r\n if (IS_DEV) return false\r\n const cssWidth = wrapper.clientWidth || wrapper.offsetWidth\r\n if (cssWidth === 0) return true\r\n const neededSize = pickSize(cssWidth, window.devicePixelRatio ?? 1, originalW)\r\n const loaded = cached.size\r\n if (neededSize === null) {\r\n return loaded !== Infinity\r\n }\r\n return neededSize > loaded\r\n}\r\n\r\n/** Пометить завершённую загрузку по данным кэша (без повторного IO). */\r\nfunction applyCachedCompleteMarker(img: HTMLImageElement, dataSrc: string): void {\r\n if (img.hasAttribute('data-max-size-load')) return\r\n const direct = directLoadKind(dataSrc)\r\n if (direct) {\r\n img.setAttribute('data-max-size-load', direct)\r\n return\r\n }\r\n if (IS_DEV) {\r\n img.setAttribute('data-max-size-load', 'dev')\r\n return\r\n }\r\n // prod + локальный JPG: маркер ставится только через handleResize() → handleIntersect(),\r\n // не при ре-регистрации компонента (ре-рендер не является ресайзом)\r\n}\r\n\r\n// ─── Кэш загруженных URL ──────────────────────────────────────────────────────\r\n\r\ntype CacheEntry = {\r\n /** Финальный URL который был подставлен в src (миниатюра или оригинал) */\r\n url: string\r\n /** Размер загруженной версии. Infinity = оригинал */\r\n size: number\r\n}\r\n\r\n/**\r\n * Модульный кэш: dataSrc → последний успешно загруженный url + size.\r\n * Живёт в рамках сессии страницы. При перемонтировании компонента\r\n * позволяет сразу показать картинку без fade, не дожидаясь IO.\r\n */\r\nconst loadedCache = new Map<string, CacheEntry>()\r\n\r\n/** Возвращает кэшированный результат для dataSrc, или null если ещё не грузили. */\r\nexport function getLoadedUrl(dataSrc: string): CacheEntry | null {\r\n return loadedCache.get(dataSrc) ?? null\r\n}\r\n\r\n// ─── Типы ─────────────────────────────────────────────────────────────────────\r\n\r\ntype Entry = {\r\n wrapper: HTMLElement\r\n dataSrc: string\r\n /** Оригинальная ширина картинки — ограничивает максимально доступный размер миниатюры */\r\n originalW: number\r\n /** Размер уже загруженной версии. null — картинка ещё не загружалась. */\r\n loadedSize: number | null\r\n /**\r\n * true — картинка была повторно добавлена в observer после resize,\r\n * ждём подтверждения видимости чтобы проверить нужен ли апгрейд.\r\n */\r\n pendingResizeCheck: boolean\r\n onLoad: (url: string) => void\r\n onError: () => void\r\n}\r\n\r\n// ─── Singleton state ──────────────────────────────────────────────────────────\r\n\r\nconst registry = new Map<HTMLImageElement, Entry>()\r\n\r\n/** Элементы, которые сейчас отслеживает IntersectionObserver. */\r\nconst observing = new Set<HTMLImageElement>()\r\n\r\nlet ioInstance: IntersectionObserver | null = null\r\nlet resizeTimer: ReturnType<typeof setTimeout> | null = null\r\nlet resizeAttached = false\r\n\r\n// ─── IntersectionObserver ─────────────────────────────────────────────────────\r\n\r\nfunction getIO(): IntersectionObserver {\r\n if (!ioInstance) {\r\n ioInstance = new IntersectionObserver(handleIntersect, { threshold: 0.05 })\r\n }\r\n return ioInstance\r\n}\r\n\r\n/** Добавляет в observer без дублей. */\r\nfunction safeObserve(img: HTMLImageElement): void {\r\n if (observing.has(img)) return\r\n observing.add(img)\r\n getIO().observe(img)\r\n}\r\n\r\nfunction safeUnobserve(img: HTMLImageElement): void {\r\n if (!observing.has(img)) return\r\n observing.delete(img)\r\n getIO().unobserve(img)\r\n}\r\n\r\nfunction handleIntersect(entries: IntersectionObserverEntry[]): void {\r\n for (const ioEntry of entries) {\r\n if (!ioEntry.isIntersecting) continue\r\n\r\n const img = ioEntry.target as HTMLImageElement\r\n const state = registry.get(img)\r\n if (!state) continue\r\n\r\n safeUnobserve(img)\r\n\r\n const direct = directLoadKind(state.dataSrc)\r\n if (direct) {\r\n loadDirectFinal(img, state, direct)\r\n continue\r\n }\r\n\r\n // ── Dev mode: миниатюр нет, грузим оригинал ──────────────────────────────\r\n if (IS_DEV) {\r\n const hit = loadedCache.get(state.dataSrc)\r\n if (hit) {\r\n state.loadedSize = hit.size\r\n // onLoad всегда вызываем: если компонент уже показывал картинку (imgSrc = реальный URL),\r\n // React получит то же значение → state не обновится → img.src не изменится → fade не мигнёт.\r\n // Если компонент ещё не грузил (imgSrc = BLANK, другой экземпляр) → src сменится → fade-in.\r\n state.onLoad(hit.url)\r\n if (!img.hasAttribute('data-max-size-load')) {\r\n img.setAttribute('data-max-size-load', 'dev')\r\n }\r\n continue\r\n }\r\n const url = state.dataSrc\r\n const tempImg = new Image()\r\n tempImg.onload = () => {\r\n state.loadedSize = Infinity\r\n loadedCache.set(state.dataSrc, { url, size: Infinity })\r\n state.onLoad(url)\r\n img.setAttribute('data-max-size-load', 'dev')\r\n }\r\n tempImg.onerror = () => state.onError()\r\n tempImg.src = url\r\n continue\r\n }\r\n\r\n const cssWidth = state.wrapper.clientWidth || state.wrapper.offsetWidth\r\n if (cssWidth === 0) {\r\n safeObserve(img)\r\n continue\r\n }\r\n\r\n const neededSize = pickSize(cssWidth, window.devicePixelRatio ?? 1, state.originalW)\r\n const isResizeCheck = state.pendingResizeCheck\r\n state.pendingResizeCheck = false\r\n\r\n const url = neededSize === null\r\n ? state.dataSrc\r\n : buildUrl(state.dataSrc, neededSize, supportsWebP())\r\n\r\n if (isResizeCheck && neededSize !== null && state.loadedSize !== null && neededSize <= state.loadedSize) {\r\n img.setAttribute('data-max-size-load', String(state.loadedSize))\r\n continue\r\n }\r\n\r\n const tempImg = new Image()\r\n\r\n tempImg.onload = () => {\r\n state.loadedSize = neededSize ?? Infinity\r\n loadedCache.set(state.dataSrc, { url, size: state.loadedSize })\r\n state.onLoad(url)\r\n if (isResizeCheck || neededSize === null) {\r\n img.setAttribute('data-max-size-load', String(state.loadedSize))\r\n }\r\n }\r\n\r\n tempImg.onerror = () => {\r\n state.onError()\r\n if (isResizeCheck || neededSize === null) {\r\n img.setAttribute('data-max-size-load', String(state.loadedSize ?? 0))\r\n }\r\n }\r\n\r\n tempImg.src = url\r\n }\r\n}\r\n\r\n// ─── Resize handler ───────────────────────────────────────────────────────────\r\n\r\nfunction handleResize(): void {\r\n if (IS_DEV) return\r\n\r\n for (const [img, state] of registry) {\r\n if (state.loadedSize === null) continue\r\n if (img.hasAttribute('data-max-size-load')) continue\r\n\r\n state.pendingResizeCheck = true\r\n safeObserve(img)\r\n }\r\n}\r\n\r\nfunction attachResizeListener(): void {\r\n if (resizeAttached) return\r\n resizeAttached = true\r\n window.addEventListener(\r\n 'resize',\r\n () => {\r\n if (resizeTimer) clearTimeout(resizeTimer)\r\n resizeTimer = setTimeout(handleResize, 200)\r\n },\r\n { passive: true },\r\n )\r\n}\r\n\r\n// ─── Public API ───────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Регистрирует img-элемент в менеджере ленивой загрузки.\r\n * Возвращает функцию очистки — вызвать при размонтировании компонента.\r\n */\r\nexport function register(\r\n img: HTMLImageElement,\r\n wrapper: HTMLElement,\r\n dataSrc: string,\r\n originalW: number,\r\n onLoad: (url: string) => void,\r\n onError: () => void,\r\n): () => void {\r\n if (dataSrc == null || dataSrc === '' || typeof dataSrc !== 'string') {\r\n return () => {\r\n safeUnobserve(img)\r\n registry.delete(img)\r\n }\r\n }\r\n\r\n attachResizeListener()\r\n\r\n const cached = loadedCache.get(dataSrc)\r\n const observe = needsIntersectionObserver(dataSrc, originalW, wrapper, cached)\r\n // pendingResizeCheck = false: регистрация (в т.ч. при ре-рендере) — не ресайз.\r\n // Флаг true выставляет только handleResize().\r\n const pendingResizeCheck = false\r\n\r\n registry.set(img, {\r\n wrapper,\r\n dataSrc,\r\n originalW,\r\n loadedSize: cached?.size ?? null,\r\n pendingResizeCheck,\r\n onLoad,\r\n onError,\r\n })\r\n\r\n const cleanup = () => {\r\n safeUnobserve(img)\r\n registry.delete(img)\r\n }\r\n\r\n if (cached && !observe) {\r\n applyCachedCompleteMarker(img, dataSrc)\r\n return cleanup\r\n }\r\n\r\n safeObserve(img)\r\n\r\n return cleanup\r\n}\r\n","/**\n * Runtime CSS injection for SmartImg.\n * Injects styles once into <head> on first component use.\n * Uses `smi-` prefix to avoid class name collisions.\n */\n\nconst CSS = `\n.smi-wrapper {\n position: relative;\n width: 100%;\n height: 0;\n overflow: hidden;\n background-color: var(--smi-bg, #f0f0f0);\n}\n.smi-image {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n.smi-fade {\n opacity: 0;\n transition: opacity var(--smi-fade-duration, 400ms) ease;\n}\n.smi-fade.smi-visible {\n opacity: 1;\n}\n.smi-error {\n object-fit: contain;\n padding: 15%;\n}\n`\n\nlet injected = false\n\nexport function injectStyles(): void {\n if (typeof document === 'undefined' || injected) return\n injected = true\n const style = document.createElement('style')\n style.setAttribute('data-smi', '')\n style.textContent = CSS\n document.head.appendChild(style)\n}\n\nexport const cls = {\n wrapper: 'smi-wrapper',\n image: 'smi-image',\n fade: 'smi-fade',\n visible: 'smi-visible',\n error: 'smi-error',\n} as const\n"],"mappings":"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","names":["url","tempImg"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-smart-img",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.16",
|
|
4
4
|
"description": "Smart lazy-loading image component for React + Vite with automatic thumbnail generation",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -54,5 +54,6 @@
|
|
|
54
54
|
"tsup": "^8.0.0",
|
|
55
55
|
"typescript": "^6.0.2",
|
|
56
56
|
"vite": "^8.0.2"
|
|
57
|
-
}
|
|
57
|
+
},
|
|
58
|
+
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
|
|
58
59
|
}
|