IlmHamroh
JavaScript Full-stack/21-qism. React: media, integratsiyalar, PWA12/32-dars27 daqiqa
Mundarija (34)

Rasm UI React'da: lightbox, zoom, kesish (crop) va CDN transformatsiyalari

Qisqacha: Rasm UI to'rt qismdan iborat. Yetkazish — CDN manzilida o'lcham va format (w_400,f_auto), srcSet, sizes, fetchPriority; birinchi ekrandagi rasm uchun React 19 preload(). Kutish — rasm kelguncha placeholder (rang, BlurHash, ThumbHash). Ko'rish — lightbox (to'liq ekranda galereya) va zoom. Kesish — kvadrat maydonda siljitish va kattalashtirish, keyin canvas → kichik WebP. Kutubxonalar bor: yet-another-react-lightbox (+10.6 KB gzip), react-medium-image-zoom (+5.4), react-easy-crop (+7.3). Lekin react-easy-crop vazifalar ning Trusted Types siyosati ostida ochilmadi — kanon kesishni o'zi yozdi.

Bu darsda

  • Rasmni React'da srcSet, sizes, loading, fetchPriority va preload() bilan to'g'ri yetkazasiz.
  • CDN transformatsiya manzilini yasaysiz va nega u imzolanishi kerakligini tushuntirasiz.
  • Placeholder, klaviatura bilan boshqariladigan lightbox va zoom yozasiz.
  • Rasm kesish hisobini va canvas → WebP qadamini yozasiz.
  • Uchta kutubxonani hajmi va xavfsizlik siyosati natijasi bo'yicha tanlaysiz.

Oldin bilishingiz kerak: Rasm unumdorligi, Responsive rasmlar I: srcset va sizes, Canvas amaliyotda, Fayl yuklash UI, Fokus boshqaruvi va klaviatura navigatsiyasi.

1. Nega bu kerak?

Oldingi darsda muharrirlar bilan matnni boyitdik. Endi rasmlar. Jasur aka «Bahor» saytiga menyu galereyasini qo'shmoqchi: har taomning rasmi. Uchta talab bor:

  • telefonda sahifa tez ochilsin — 3 MB lik asl surat yuklanmasin;
  • rasm bosilsa, u ekran bo'ylab katta ochilsin va qo'shni rasmlarga o'tish mumkin bo'lsin;
  • xodimlar profil rasmini yuklaganda, uni dumaloq ramkaga moslab kesib olsin.

HTML qismida rasmni tez yetkazish atributlarini o'rgangansiz (Rasm unumdorligi). Bu dars ularni React komponentlari ichida ishlatadi va uch yangi narsani qo'shadi: CDN, lightbox va kesish.

Hayotiy o'xshatish: fotosalon. Asl surat arxivda turadi. Mijozga kerakli o'lchamda nusxa chiqariladi (CDN). Vitrinada kichik nusxa, bosilsa — katta ramkadagi nusxa (lightbox). Hujjat uchun esa suratning bir qismi kesib olinadi (crop).

2. Rasm React komponentida

2.1 Atributlar JSX'da

HTML'dagi atributlar JSX'da camelCase bo'ladi: srcset → srcSet, fetchpriority → fetchPriority. Ma'nosi o'sha (srcset va sizes, Rasm unumdorligi):

Prop Nima qiladi
srcSet, sizes brauzer ekran eniga mos faylni o'zi tanlaydi
width, height joy oldindan band — sahifa sakramaydi (CLS)
loading="lazy" ko'rinish chegarasiga yaqinlashganda yuklanadi
decoding="async" rasmni ochish (dekodlash) sahifani to'xtatmaydi
fetchPriority "high" — birinchi ekrandagi asosiy rasm uchun

Takroriy kodni bitta komponentga yig'amiz. Kod yozilganda brauzerga aynan nima ketishini ko'rish uchun DOM'dagi tegni konsolga chiqaramiz:

jsx
import { useEffect, useRef } from "react";

const WIDTHS = [400, 800, 1200];

function MenuImage({ name, alt, priority = false }) {
  const ref = useRef(null);
  useEffect(() => {
    console.log(ref.current.outerHTML);
  }, []);
  return (
    <img
      ref={ref}
      src={`/menyu/${name}-400.webp`}
      srcSet={WIDTHS.map((w) => `/menyu/${name}-${w}.webp ${w}w`)
        .join(", ")}
      sizes="(min-width: 700px) 50vw, 100vw"
      alt={alt}
      width={400}
      height={300}
      loading={priority ? "eager" : "lazy"}
      decoding="async"
      fetchPriority={priority ? "high" : "auto"}
    />
  );
}

export default function App() {
  return <MenuImage name="osh" alt="Osh, ustida bedana tuxumi" />;
}

Konsolda:

text
<img sizes="(min-width: 700px) 50vw, 100vw" alt="Osh, ustida bedana tuxumi" width="400" height="300" loading="lazy" decoding="async" fetchpriority="auto" srcset="/menyu/osh-400.webp 400w, /menyu/osh-800.webp 800w, /menyu/osh-1200.webp 1200w" src="/menyu/osh-400.webp">

Natija oynasida rasm ko'rinmaydi — bu fayllar u yerda yo'q, faqat teg muhim. Qatorni o'qing:

  • fetchPriority → fetchpriority: React 19 bu propni taniydi va HTML atributiga aylantiradi.
  • src — eng oxirida. Tekshiruvchimiz ko'rsatgan bu tartib tasodif emas: React <img> da srcset va sizes ni avval yozadi. Aks holda ba'zi brauzerlar src ni ko'rishi bilan uni yuklashni boshlab qo'yardi, keyin srcset dan boshqa faylni tanlardi — bitta rasm ikki marta yuklanardi.
  • priority — bitta prop ikki atributni boshqaradi. Birinchi ekrandagi rasmga lazy qo'yish — eng ko'p uchraydigan xato (Rasm unumdorligi darsidagi qoida), komponent buni bir joyda hal qiladi.

2.2 preload(): rasmni oldindan aytib qo'yish

Asosiy rasm (LCP) ko'pincha JavaScript chizgandan keyin ma'lum bo'ladi: React komponenti ishlaguncha brauzer u haqda bilmaydi. React 19 da react-dom dan preload() funksiyasi bor. U <head> ga <link rel="preload"> qo'shadi va brauzer rasmni darhol so'raydi:

jsx
import { useEffect } from "react";
import { preload } from "react-dom";

export default function App() {
  preload("/menyu/osh-800.webp", {
    as: "image",
    fetchPriority: "high",
  });
  useEffect(() => {
    const link = document.head.querySelector("link[rel=preload]");
    console.log(link.outerHTML);
  }, []);
  return <h1>Bugungi taom: osh</h1>;
}

Konsolda:

text
<link rel="preload" href="/menyu/osh-800.webp" as="image" fetchpriority="high">

preload ni render ichida chaqirsa bo'ladi — u hook emas, React bir xil manzilni ikki marta qo'shmaydi. imageSrcSet va imageSizes parametrlari bilan srcset'li rasmni ham oldindan aytish mumkin. Faqat bitta-ikkita eng muhim rasm uchun ishlating: hammasini "muhim" desangiz, hech biri muhim bo'lmay qoladi.

2.3 Placeholder: rasm kelguncha

Sekin internetda rasm o'rni bo'sh oq to'rtburchak bo'lib turadi. Rasm unumdorligi darsida placeholder turlarini ko'rgansiz: asosiy rang, kichik xira nusxa (LQIP), BlurHash. React'da g'oya — bitta state: yuklandi. Rasm kelguncha fon rangi ko'rinadi, onLoad dan keyin rasm silliq paydo bo'ladi.

Bu blok faqat saytda ishlaydi: tekshiruvchimiz (jsdom) rasmni umuman yuklamaydi va onLoad ni chaqirmaydi. Biz uni Chrome 155 da sinadik:

jsx
import { useState } from "react";

// Natija oynasida tashqi fayl yo'q — rasmni SVG'dan yasaymiz
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="400"
  height="300"><rect width="400" height="300" fill="#c2410c"/>
  <circle cx="200" cy="150" r="90" fill="#fde68a"/></svg>`;
const SRC = `data:image/svg+xml,${encodeURIComponent(svg)}`;

function SmoothImage({ src, alt, color }) {
  const [loaded, setLoaded] = useState(false);
  return (
    <div style={{ background: color, width: 200 }}>
      <img
        src={src}
        alt={alt}
        width={200}
        height={150}
        onLoad={() => {
          setLoaded(true);
          console.log("yuklandi");
        }}
        style={{
          opacity: loaded ? 1 : 0,
          transition: "opacity 0.4s",
        }}
      />
    </div>
  );
}

export default function App() {
  return <SmoothImage src={SRC} alt="Lag'mon" color="#9a3412" />;
}

Chrome 155 da (saytdagi natija oynasi hujjati — o'sha React, headless) konsol:

text
yuklandi

Placeholder rangi (color) rasmdan oldindan olinadi va ma'lumot bilan birga keladi: { src, color: "#9a3412" }. BlurHash va ThumbHash shu g'oyaning boyroq varianti: rasmning 20–30 baytlik "xira izi" serverda hisoblanadi va ma'lumotga qo'shiladi. Brauzerda kichik kutubxona uni xira rasmga aylantiradi (thumbHashToDataURL(bytes) kabi). ThumbHash rasm nisbatini va shaffoflikni ham saqlaydi. Bu ikki kutubxonani biz o'lchamadik — tanlasangiz, hajmini o'zingiz tekshiring.

Maslahat: prefers-reduced-motion yoqilgan bo'lsa, transition ni o'chiring (UI animatsiya tamoyillari). Rasm baribir paydo bo'ladi, faqat silliq emas, darhol.

Tekshirib ko'ring: Nega MenuImage komponentida width={400} height={300} bor, garchi CSS rasmni baribir ekran eniga cho'zsa ham?

Javob

Bu ikki son brauzerga rasm nisbatini (4:3) aytadi. Rasm hali kelmagan bo'lsa ham, brauzer uning joyini to'g'ri balandlikda band qiladi. Busiz joy 0 px bo'ladi, rasm kelganda pastdagi matn birdan siljiydi — bu CLS (Performance API va Core Web Vitals). CSS'dagi width: 100%; height: auto esa o'lchamni o'zgartiradi, nisbatni emas.

3. CDN transformatsiyalari

3.1 Bitta asl fayl — ko'p variant

MenuImage uchta faylni kutadi: osh-400.webp, osh-800.webp, osh-1200.webp. Ularni kim yasaydi? Ikki yo'l bor:

  1. Build paytida. Rasmlar loyiha ichida bo'lsa, Vite plagini vite-imagetools ularni build vaqtida variantlarga aylantiradi. Import qatoridagi so'rov parametrlari aytadi: import osh from "./osh.jpg?w=400;800&format=webp&as=srcset". Kanon bu plaginni o'lchamadi — faqat hujjatdagi g'oya.
  2. So'rov paytida — rasm CDN'i. Foydalanuvchi yuklagan rasmlar uchun (menyu ertaga o'zgaradi) build yordam bermaydi. Rasm CDN'i asl faylni bir marta saqlaydi va har variantni birinchi so'rovda yasab, keshlaydi.

CDN transformatsiyasi (image transformation) — variant manzilning o'zida so'raladi. Ikki mashhur xizmat:

Xizmat Manzil ko'rinishi
Cloudinary (bulut) …/image/upload/c_fill,w_400,h_300,f_auto,q_auto/osh.jpg
imgproxy (o'z serveringizda) …/<imzo>/rs:fill:400:300/q:80/plain/<asl manzil>@webp

Cloudinary'da c_fill — to'ldirib kesish, w_/h_ — o'lcham, f_auto — brauzer qabul qiladigan eng yaxshi format (AVIF yoki WebP), q_auto — sifat avtomatik. imgproxy — ochiq kodli dastur, uni o'z serveringizda ishga tushirasiz. Tashqi xizmatga bog'lanmaslik va maxfiylik uchun qulay.

3.2 Manzil yasovchi funksiya

Komponentlar CDN sintaksisini bilmasligi kerak — ular faqat "osh, 400 piksel" deydi. Bitta funksiya:

js
const CLOUD = "https://res.cloudinary.com/sizning_bulut";

function cdnUrl(id, { w, h, fit = "fill" }) {
  const t = [`c_${fit}`, `w_${w}`, h && `h_${h}`, "f_auto", "q_auto"];
  return `${CLOUD}/image/upload/${t.filter(Boolean).join(",")}/${id}`;
}

function cdnSrcSet(id, widths) {
  return widths.map((w) => `${cdnUrl(id, { w })} ${w}w`).join(", ");
}

console.log(cdnUrl("menyu/osh.jpg", { w: 400, h: 300 }));
console.log(cdnSrcSet("menyu/osh.jpg", [400, 800]));

Konsolda:

text
https://res.cloudinary.com/sizning_bulut/image/upload/c_fill,w_400,h_300,f_auto,q_auto/menyu/osh.jpg
https://res.cloudinary.com/sizning_bulut/image/upload/c_fill,w_400,f_auto,q_auto/menyu/osh.jpg 400w, https://res.cloudinary.com/sizning_bulut/image/upload/c_fill,w_800,f_auto,q_auto/menyu/osh.jpg 800w

sizning_bulut — o'z hisobingiz nomi bilan almashtiriladi (biz hisob ochmadik — bu manzil ishlamaydi). h && `h_${h}` — balandlik berilmasa undefined, filter(Boolean) uni tashlaydi. Xizmatni almashtirsangiz (imgproxy, Cloudflare Images), faqat shu funksiya o'zgaradi.

4. Hujumchi nigohi

Transformatsiya manzilda bo'lsa, uni har kim o'zgartira oladi. Hujumchi skript yozadi: w_401, w_402, … w_5000 — har biri yangi variant. CDN har birini yasaydi (protsessor vaqti), saqlaydi (disk) va hisobingizga yozadi. O'n minglab so'rov — oy oxirida katta hisob yoki xizmatning to'xtashi. imgproxy'da esa plain/ dan keyingi manbaga boshqa saytdagi rasmni qo'yib, serveringizni begona rasmlar uchun ishlatish mumkin.

Himoya — imzolangan manzil (signed URL). Server maxfiy kalit bilan manzilning qolgan qismidan HMAC imzo hisoblaydi. CDN o'sha kalit bilan qayta hisoblaydi: bitta harf o'zgarsa — imzo mos kelmaydi, so'rov rad etiladi. imgproxy uslubidagi imzo (Node'da, kalit — namuna):

js
import { createHmac } from "node:crypto";

const KEY = Buffer.from("736563726574", "hex"); // namuna kalit
const SALT = Buffer.from("68656c6c6f", "hex"); // namuna "tuz"

function signPath(path) {
  return createHmac("sha256", KEY)
    .update(SALT)
    .update(path)
    .digest("base64url");
}

const source = "https://example.com/menyu/osh.jpg";
const path = `/rs:fill:400:300/q:80/plain/${source}@webp`;
console.log(`/${signPath(path)}${path}`);
const path2 = `/rs:fill:4000:3000/q:80/plain/${source}@webp`;
console.log(signPath(path2) === signPath(path));

Konsolda:

text
/RuJmEa8veoFq6_Ky5_GPiHArC0KXY62fr1yHrGXukyg/rs:fill:400:300/q:80/plain/https://example.com/menyu/osh.jpg@webp
false

Ikkinchi qator — hujumchining urinishi: o'lchamni 4000 ga o'zgartirsa, imzo boshqa bo'ladi. Kalitsiz to'g'ri imzoni topib bo'lmaydi. Uchta qoida:

  • Kalit faqat serverda. Brauzer kodiga tushgan kalit — ochiq kalit: har kim DevTools'da ko'radi. Shuning uchun imzolangan manzillarni backend beradi (API javobida tayyor src). Bu backend qismlarida (24-qismdan) yoziladi.
  • Ruxsat etilgan o'lchamlar ro'yxati. Cloudinary'da "strict transformations" rejimi faqat oldindan ruxsat berilgan variantlarni yasaydi. O'z kodingizda ham WIDTHS = [400, 800, 1200] kabi qisqa ro'yxat.
  • Manba cheklovi. imgproxy'ga faqat o'z omboringizdagi rasmlarni ochishga ruxsat bering.

Tekshirib ko'ring: Nega imzo hisoblashda SALT ham bor, faqat KEY yetmasmidi?

Javob

Kalit yetadi — asosiy himoya o'sha. Tuz (salt) imzoni shu o'rnatishga xos qiladi: bir xil kalit tasodifan boshqa joyda ishlatilsa ham, imzolar mos kelmaydi. imgproxy ikkalasini ham talab qiladi, ikkalasi ham maxfiy. Muhim narsa — tuz kalitning o'rnini bosmaydi.

5. Lightbox: rasm to'liq ekranda

5.1 Talablar

Lightbox ("yorug' quti") — galereyadagi rasm bosilganda uni sahifa ustida, katta qilib ko'rsatadigan oyna. Fon qorayadi, o'rtada rasm, yonida "keyingi" va "oldingi". Bu — modal oyna, shuning uchun talablar o'sha:

  • ochilganda fokus oyna ichiga o'tadi, yopilganda — bosilgan rasmga qaytadi;
  • Esc — yopadi, ← va → — oldingi va keyingi rasm;
  • ekran o'quvchi oynani nomi bilan e'lon qiladi (role="dialog", aria-label);
  • katta rasm faqat ochilganda yuklanadi — galereyada kichik nusxa.

5.2 O'z lightbox'imiz

Bobur faqat klaviatura bilan ishlaydi. Quyidagi blokni u qanday ishlatishini qadamlar bilan sinaymiz: «Manti» ni bosadi, ikki marta →, keyin Esc:

jsx
import { useEffect, useRef, useState } from "react";

const svg = (color) =>
  "data:image/svg+xml," +
  encodeURIComponent(
    `<svg xmlns="http://www.w3.org/2000/svg" width="4" height="3">` +
      `<rect width="4" height="3" fill="${color}"/></svg>`,
  );

const PHOTOS = [
  { title: "Osh", alt: "Osh, qazi bilan", src: svg("#c2410c") },
  { title: "Manti", alt: "Manti, qatiq bilan", src: svg("#e5e7eb") },
  { title: "Lag'mon", alt: "Lag'mon, ko'kat bilan",
    src: svg("#b45309") },
];

function Lightbox({ index, onClose, onMove }) {
  const closeButton = useRef(null);
  useEffect(() => closeButton.current.focus(), []);
  const photo = PHOTOS[index];

  function handleKey(event) {
    if (event.key === "Escape") onClose();
    if (event.key === "ArrowRight") onMove(1);
    if (event.key === "ArrowLeft") onMove(-1);
  }

  return (
    <div
      role="dialog"
      aria-modal="true"
      aria-label={photo.title}
      onKeyDown={handleKey}
      style={{ position: "fixed", inset: 0, background: "#000c" }}
    >
      <img src={photo.src} alt={photo.alt} width={320} height={240} />
      <p style={{ color: "white" }}>
        {index + 1} / {PHOTOS.length} — {photo.title}
      </p>
      <button ref={closeButton} onClick={onClose}>
        Yopish
      </button>
    </div>
  );
}

export default function App() {
  const [open, setOpen] = useState(null); // ochiq rasm indeksi
  const opener = useRef(null); // qaysi tugma ochdi

  function close() {
    setOpen(null);
    // yopilgach fokus — ochgan tugmaga
    requestAnimationFrame(() => {
      opener.current.focus();
      console.log("fokus:", document.activeElement.textContent);
    });
  }

  return (
    <>
      <ul>
        {PHOTOS.map((photo, i) => (
          <li key={photo.title}>
            <button
              onClick={(event) => {
                opener.current = event.currentTarget;
                setOpen(i);
              }}
            >
              <img src={photo.src} alt="" width={40} />{" "}
              {photo.title}
            </button>
          </li>
        ))}
      </ul>
      {open !== null && (
        <Lightbox
          index={open}
          onClose={close}
          onMove={(step) =>
            setOpen((i) => (i + step + PHOTOS.length) % PHOTOS.length)
          }
        />
      )}
    </>
  );
}

Konsolda:

text
fokus: Manti

Ekranda:

text
Osh
Manti
Lag'mon

// harakat: dagi ekran(...) qadamlari oraliq holatni tekshirdi. Bobur ko'rgan narsa qadamma-qadam:

Qadam Holat (open) Ekranda Fokus
«Manti» bosildi 1 «2 / 3 — Manti», qora fon «Yopish»
→ 2 «3 / 3 — Lag'mon» «Yopish»
→ 0 (aylanib) «1 / 3 — Osh» «Yopish»
Esc null faqat ro'yxat «Manti» tugmasi

Oxirgi qator — yuqoridagi Ekranda: blokining o'zi: oyna yopildi, uchta tugma qoldi. Muhim joylar:

  • Aylanma o'tish. (i + step + PHOTOS.length) % PHOTOS.length — oxirgidan keyin birinchisi, birinchidan oldin oxirgisi. + PHOTOS.length manfiy qoldiqdan saqlaydi: JavaScript'da -1 % 3 — -1, indeks bo'la olmaydi.
  • Fokus kirdi — useEffect «Yopish» tugmasiga fokus berdi. Klaviatura hodisalari endi oyna ichidagi tugmadan div ning onKeyDown iga ko'tariladi.
  • Fokus qaytdi — opener ref'i bosilgan tugmani eslab qoldi. Yopilgandan keyin «Manti» yana fokusda. Bobur sahifadagi joyini yo'qotmadi.

Bu blok — o'quv modeli. Unda ikki narsa yetishmaydi: Tab bilan oynadan chiqib ketish mumkin (fokus tuzog'i yo'q) va orqadagi sahifa ekran o'quvchi uchun "ochiq". Loyihada <dialog> ning showModal() usuli ikkalasini ham bepul beradi: orqa sahifa inert bo'ladi, Esc o'zi yopadi (<dialog> va Popover API). vazifalar kanoni ham 20-qismdan beri modal oynalar uchun native <dialog> ishlatadi.

5.3 yet-another-react-lightbox

Galereya katta bo'lsa — barmoq bilan surish, kichik rasmlar qatori, zoom, video — tayyor kutubxona olgan ma'qul. Eng ko'p ishlatiladiganlaridan biri — yet-another-react-lightbox (YARL). Saytdagi natija oynasida u yo'q, kod hujjat bo'yicha:

jsx
import Lightbox from "yet-another-react-lightbox";
import Zoom from "yet-another-react-lightbox/plugins/zoom";
import "yet-another-react-lightbox/styles.css";

function Gallery({ photos }) {
  const [index, setIndex] = useState(-1); // -1 — yopiq
  return (
    <>
      {photos.map((p, i) => (
        <button key={p.id} onClick={() => setIndex(i)}>
          <img src={p.thumb} alt={p.alt} />
        </button>
      ))}
      <Lightbox
        open={index >= 0}
        index={index}
        close={() => setIndex(-1)}
        slides={photos.map((p) => ({ src: p.big, alt: p.alt }))}
        plugins={[Zoom]}
      />
    </>
  );
}

slides — rasmlar ro'yxati, plugins — qo'shimcha imkoniyatlar (Zoom, Thumbnails, Fullscreen, Captions). Klaviatura, surish va fokus kutubxona ichida. Kanon tajribasida (3.32.2) u +10.6 KB JS va +1.4 KB CSS qo'shdi. vazifalar ning CSP va Trusted Types siyosati ostida ochildi va 0 buzilish berdi.

6. Zoom: rasmni joyida kattalashtirish

Lightbox — galereya uchun. Bitta rasmni (masalan, chek yoki xarita) yaqindan ko'rish uchun zoom yetadi: rasm bosilganda o'z joyidan silliq kattalashib, ekran markaziga chiqadi. react-medium-image-zoom shunday qiladi — oddiy <img> ni o'rab qo'yasiz:

jsx
import Zoom from "react-medium-image-zoom";
import "react-medium-image-zoom/dist/styles.css";

<Zoom>
  <img src="/menyu/osh-400.webp" alt="Osh" width={400} height={300} />
</Zoom>;

Kanon tajribasida (5.4.9): +5.4 KB JS, +0.6 KB CSS, CSP 0. Telefonda esa ko'pincha kutubxona kerak emas: brauzerning o'zi ikki barmoq bilan kattalashtiradi. Faqat CSS'da touch-action: none qo'yib, buni o'chirib qo'ymang.

Tekshirib ko'ring: Galereyada 40 ta rasm. Lightbox'dagi katta rasmlarni sahifa ochilishi bilan yuklab qo'yish kerakmi?

Javob

Yo'q. Katta rasm faqat ochilganda kerak. 40 ta katta rasmni oldindan yuklash — o'nlab megabayt, ko'pchiligi hech qachon ochilmaydi. Galereyada kichik nusxa (thumb) va loading="lazy", lightbox'da — ochilgan rasm va eng ko'pi bilan qo'shnilari (keyingi bosish tez bo'lsin). YARL ham sukut bo'yicha joriy rasm atrofidagi bir nechtasini yuklaydi.

7. Rasmni kesish (crop)

7.1 Kesish — bu hisob

Profil rasmi dumaloq ramkada turadi, foydalanuvchi esa keng surat yuklaydi. Unga maydon beramiz: rasm kvadrat ichida, foydalanuvchi uni siljitadi va kattalashtiradi. Ko'rinadigan qism — kesib olinadigan qism.

Hammasi uchta sonda: zoom (1–3), x va y (−1 dan 1 gacha — chap chetdan o'ng chetgacha). Ulardan ikki narsa hisoblanadi:

  1. Ko'rinish — rasm ekranda qanday o'lchamda va qayerda turadi (CSS transform).
  2. Manba kvadrati — asl rasmning qaysi piksellari olinadi (canvas uchun).

vazifalar kanonidagi kesish.ts aynan shu ikkisini sof funksiya bilan hisoblaydi. Uning yadrosi (kanon kodi, turlari qisqartirilgan):

ts
const KESISH_OLCHAMI = 256;

type Kesish = { zoom: number; x: number; y: number };

function kesishOrni(
  eni: number,
  boyi: number,
  { zoom, x, y }: Kesish,
  maydon = KESISH_OLCHAMI,
) {
  const masshtab = Math.max(maydon / eni, maydon / boyi) * zoom;
  const korinishEni = eni * masshtab;
  const korinishBoyi = boyi * masshtab;
  const ortiqX = korinishEni - maydon;
  const ortiqY = korinishBoyi - maydon;
  const chap = -ortiqX / 2 - (x * ortiqX) / 2 + 0;
  const tepa = -ortiqY / 2 - (y * ortiqY) / 2 + 0;
  return {
    korinishEni,
    korinishBoyi,
    chap,
    tepa,
    manba: {
      x: -chap / masshtab + 0,
      y: -tepa / masshtab + 0,
      tomon: maydon / masshtab,
    },
  };
}

const markaz = kesishOrni(800, 400, { zoom: 1, x: 0, y: 0 });
console.log(markaz.korinishEni, markaz.chap, markaz.manba);
console.log(kesishOrni(800, 400, { zoom: 1, x: 1, y: 0 }).manba.x);
console.log(kesishOrni(400, 400, { zoom: 2, x: 1, y: 1 }).manba);

Konsolda:

text
512 -128 { x: 200, y: 0, tomon: 400 }
400
{ x: 200, y: 200, tomon: 200 }

Birinchi qatorni qadam-qadam o'qiymiz. Rasm 800×400, maydon 256×256:

  • masshtab = max(256/800, 256/400) = 0.64. Kattasi olinadi — rasm maydonni to'liq yopsin (CSS'dagi object-fit: cover g'oyasi). Kichigi olinsa, yonlarda bo'sh joy qolardi.
  • Ko'rinishda rasm 512×256. Maydondan 256 piksel ortiq, markazda turishi uchun chapga 128 surilgan: chap = -128.
  • Asl rasmda bu — x: 200 dan boshlanadigan 400×400 kvadrat. 800 lik rasmning o'rtasi.

Ikkinchi qator: x: 1 — o'ng chet, manba 400 dan 800 gacha. Uchinchisi: zoom: 2 — manba tomoni ikki baravar kichik (200), ya'ni yaqinroq ko'rinadi. + 0 — -0 ni 0 ga aylantiradi: -0 ekranda farq qilmaydi, lekin testdagi deepEqual uni ajratadi.

7.2 Boshqaruv: avval slider, keyin sudrash

Rasmni sichqoncha bilan sudrash qulay. Lekin Bobur sichqoncha ishlatmaydi, Malika ekranni ko'rmaydi. Shuning uchun kanonning asosiy boshqaruvi — uchta <input type="range"> (yorliqli). Ular klaviatura bilan (← →) ishlaydi va ekran o'quvchi qiymatini o'qiydi. Sudrash — qo'shimcha qulaylik, o'rniga emas.

Sliderdan kelgan qiymat bilan hisob qanday o'zgarishini ko'ramiz. Natija oynasida slider'ni suring:

jsx
import { useState } from "react";

const SIDE = 256;

function cropSquare(w, h, zoom) {
  const scale = Math.max(SIDE / w, SIDE / h) * zoom;
  return Math.round(SIDE / scale);
}

export default function App() {
  const [zoom, setZoom] = useState(1);
  return (
    <>
      <label htmlFor="zoom">Kattalashtirish</label>{" "}
      <input
        id="zoom"
        type="range"
        min={1}
        max={3}
        step={0.5}
        value={zoom}
        onChange={(event) => setZoom(Number(event.target.value))}
      />
      <p>
        Zoom {zoom}: 800×400 rasmdan {cropSquare(800, 400, zoom)} px
        lik kvadrat olinadi
      </p>
    </>
  );
}

Ekranda:

text
Kattalashtirish
Zoom 1.5: 800×400 rasmdan 267 px lik kvadrat olinadi

Zoom 1 da — 400 px (rasm balandligi to'liq), 1.5 da — 267, 3 da — 133. Kvadrat kichraygani sari natija yaqinroq, lekin xiraroq: 133 pikseldan 256 piksel yasalsa, har asl piksel taxminan ikki baravar cho'ziladi.

7.3 Canvas: kesib, WebP qilish

Hisob tayyor bo'lsa, haqiqiy kesish — Canvas amaliyotda darsidagi drawImage ning 9 argumentli shakli: manba kvadrati → natija o'lchami. Keyin toBlob — fayl. Bu blok faqat saytda ishlaydi (jsdom'da canvas yo'q). Rasm sifatida boshqa canvas'ni olamiz — tashqi fayl kerak emas:

jsx
function makeSource() {
  // 800×400 sinov rasmi: chapda to'q sariq, o'ngda yashil
  const c = document.createElement("canvas");
  c.width = 800;
  c.height = 400;
  const ctx = c.getContext("2d");
  ctx.fillStyle = "#c2410c";
  ctx.fillRect(0, 0, 400, 400);
  ctx.fillStyle = "#1f6f4a";
  ctx.fillRect(400, 0, 400, 400);
  return c;
}

function crop(source, box, size, done) {
  const canvas = document.createElement("canvas");
  canvas.width = size;
  canvas.height = size;
  canvas.getContext("2d").drawImage(
    source,
    box.x, box.y, box.tomon, box.tomon, // manbadan
    0, 0, size, size, // natijaga
  );
  canvas.toBlob(done, "image/webp", 0.85);
}

export default function App() {
  return (
    <button
      onClick={() =>
        crop(makeSource(), { x: 200, y: 0, tomon: 400 }, 256, (b) =>
          console.log(b.type, b.size > 0),
        )
      }
    >
      Kesish
    </button>
  );
}

Chrome 155 da «Kesish» bosilgach konsol:

text
image/webp true

Hajm aniq son emas — u rasmga bog'liq. vazifalar da o'lchangani: 512×512 PNG 7.5 KB → kesilgan 256×256 WebP 2.2 KB. Serverga asl fayl emas, kichigi ketadi. Yana bir foyda: canvas faqat piksellarni chizadi. Suratdagi yashirin ma'lumotlar (EXIF: telefon modeli, ba'zan GPS koordinatalari) yangi faylga o'tmaydi.

7.4 react-easy-crop va Trusted Types

Tayyor yechim — react-easy-crop: sudrash, barmoq bilan kattalashtirish, dumaloq maska, onCropComplete da tayyor piksel koordinatalari:

jsx
import Cropper from "react-easy-crop";

<div style={{ position: "relative", width: 300, height: 300 }}>
  <Cropper
    image={url}
    crop={crop}
    zoom={zoom}
    aspect={1}
    cropShape="round"
    onCropChange={setCrop}
    onZoomChange={setZoom}
    onCropComplete={(_, pixels) => setPixels(pixels)}
  />
</div>;

Kutubxona kichik — kanon tajribasida (6.2.4) +7.3 KB. Lekin vazifalar CSP'si va Trusted Types ostida komponent umuman chizilmadi. Konsoldagi haqiqiy xatolar (kutubxonalar-21.txt):

text
This document requires 'TrustedHTML' assignment. The action has been blocked.
Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment

Tarjimasi: "Bu hujjat TrustedHTML talab qiladi, amal to'sildi" va "innerHTML xususiyatiga yozib bo'lmadi". react-easy-crop o'z CSS'ini sahifaga <style> qilib, innerHTML orqali qo'shadi. Trusted Types esa innerHTML ga oddiy satrni taqiqlaydi. Siyosatsiz loyihada kutubxona ishlaydi (tajribada CSP'siz — ochildi). Ikki yo'l bor edi: siyosatga istisno qo'shish yoki o'zimiz yozish. Kesish hisobi izohlari bilan 60 qatorcha ekan — kanon ikkinchisini tanladi.

Rasm UI kutubxonalari: JS (gzip, React'dan tashqari)
  • react-medium-image-zoom 5.4.9+0.6 KB CSS, CSP 05,4 KB
  • react-easy-crop 6.2.4Trusted Types ostida ochilmadi7,3 KB
  • yet-another-react-lightbox 3.32.2+1.4 KB CSS, CSP 010,6 KB
  • o'z kesish (vazifalar, holat 12)birinchi yuklash farqi1 KB

Manba: O'lchandi: kanon tajribasi, Vite 8.3.3, gzip -9, Chrome 155, natijalar/tajriba/kutubxonalar-21.txt va tekshiruv-12.txt, 2026-10-17

8. Vazifalar qadami: profil rasmini kesish (holat 12)

8.1 Qayerga qo'shiladi

Fayl yuklash UI darsida vazifalar ga profil rasmi qo'shilgan edi: tanlash, oldindan ko'rish, «Saqlash». Endi oraga bitta ixtiyoriy qadam qo'shiladi — «Kesish…». Oqim:

flowchart TB
  A["Fayl tanlandi"] --> B["blob: URL — oldindan ko'rish"]
  B --> C{"«Kesish…» bosildimi?"}
  C -- yo'q --> S["«Saqlash» — asl fayl"]
  C -- ha --> D["RasmKesish: 3 slider + sudrash"]
  D --> E["kesishOrni — manba kvadrati"]
  E --> F["canvas drawImage → 256×256"]
  F --> G["toBlob WebP 0.85 → File"]
  G --> S2["«Saqlash» — kesilgan fayl"]
  D -- «Bekor qilish» --> B

Diagrammaga qarang: kesish majburiy emas. Asl rasm ham saqlanadi — foydalanuvchi tanlaydi.

bash
git switch -c feat/rasm-kesish

Uchta yangi fayl: kesish.ts (hisob — yuqorida), RasmKesish.tsx (komponent), tekshiruv/kesish.test.js (3 test). ProfilRasmi.tsx va asos.css o'zgaradi.

8.2 RasmKesish.tsx

Kanon komponentining boshidagi izoh qarorni o'zi aytadi:

tsx
// RasmKesish.tsx — profil rasmini kvadratga kesish
// (21/#12, 19/#36 davomi).
// O'z komponentimiz: react-easy-crop 6.2.4 o'lchandi
// (+7.3 KB gzip) — Trusted Types ostida ochilmadi
// (o'z <style> ini innerHTML bilan qo'shadi;
// natijalar/tajriba/kutubxonalar-21.txt). Bu yerda:
//  - boshqaruv — uchta oddiy slider (kattalashtirish,
//    gorizontal, vertikal): klaviatura va ekran o'quvchi
//    bilan ishlaydi (sudrash — qo'shimcha)
//  - rasm — CSS transform (style prop: CSSOM,
//    CSP style-src 'self' ga mos)
//  - natija — <canvas> da 256×256 WebP: serverga asl fayl
//    emas, kichigi ketadi

"style prop: CSSOM" — muhim detal. React style={{ transform: … }} ni HTML atributi qilib yozmaydi, balki element.style orqali o'rnatadi (CSS Object Model). CSP'ning style-src 'self' qoidasi <style> teglarini va style="…" atributlarini to'sadi, CSSOM'ni esa to'smaydi. Shuning uchun React style prop'i CSP bilan ishlaydi, react-easy-crop'ning <style> tegi esa yo'q.

Komponentning rasmi va kesish funksiyasi (kanon kodi aynan):

tsx
        <img
          ref={rasm}
          src={url}
          alt=""
          draggable={false}
          onLoad={(event) =>
            setOlcham({
              eni: event.currentTarget.naturalWidth,
              boyi: event.currentTarget.naturalHeight,
            })
          }
          style={
            orin === null
              ? { visibility: "hidden" }
              : {
                  width: orin.korinishEni,
                  height: orin.korinishBoyi,
                  transform: `translate(${orin.chap}px, ${orin.tepa}px)`,
                }
          }
        />

Rasm yuklanmaguncha uning o'lchami noma'lum — orin null, rasm yashirin. onLoad asl o'lchamni (naturalWidth) beradi, keyin hisob ishlaydi. alt="" — bu rasm bezak: yonidagi guruh aria-label="Rasmni kesish" va slider'lar yetarli ma'lumot beradi.

tsx
    const { manba } = orin;
    ctx.drawImage(
      element,
      manba.x,
      manba.y,
      manba.tomon,
      manba.tomon,
      0,
      0,
      KESISH_OLCHAMI,
      KESISH_OLCHAMI,
    );
    canvas.toBlob(
      (blob) => {
        if (blob !== null) {
          onTayyor(
            new File([blob], "profil.webp", { type: blob.type }),
          );
        }
      },
      "image/webp",
      0.85,
    );

blob.type — haqiqiy tur. WebP'ni bilmaydigan brauzer toBlob da PNG qaytaradi. Nom profil.webp qolsa ham, server turi bo'yicha to'g'ri ish qiladi.

8.3 ProfilRasmi.tsx: fokus qaytishi

Kesish oynasi yopilganda (Kesish yoki Bekor qilish) fokus qayerga tushadi? Oyna DOM'dan yo'qoladi va fokus <body> ga "yiqiladi" — Bobur sahifa boshiga qaytib qoladi. Kanon diff'i:

diff
+          onBekor={() => {
+            flushSync(() => setKesilmoqda(false));
+            kesTugmasi.current?.focus();
+          }}

Muammo: setKesilmoqda(false) dan keyin «Kesish…» tugmasi hali DOM'da yo'q — React uni keyinroq chizadi. flushSync (State II darsida ko'rgansiz) React'ni darhol chizishga majbur qiladi, keyingi qatorda tugma bor va fokus oladi. Kanon avval requestAnimationFrame ni sinab ko'rgan — e2e testida beqaror chiqdi, flushSync barqaror. Bu xatoni brauzer testi topdi. Yuqoridagi lightbox o'quv modelida requestAnimationFrame ishlatdik — kichik misolda yetadi, lekin ishonchli yo'l — flushSync.

8.4 Natija va narx

Tekshiruvlar (haqiqiy, holat 12):

  • npm test — node:test 224 (+3 kesish testi), Vitest 65; lint, format:check, tip, knip — exit 0.
  • axe — 0 buzilish, yangi ko'rinish «profil rasmini kesish» bilan (43 qoida o'tdi).
  • e2e-21 — 36 ta, kesishga oidlari:
text
✅ kesish: 3 slider (yorliqli), maydon 256 px: [["Kattalashtirish","Gorizontal","Vertikal"],256]
✅ klaviatura: zoom 1.5 — rasm 384 px: ["1.5","384px"]
   Kesildi: 256×256, 2.2 KB (asli 7.5 KB). Saqlashga tayyor.
✅ natija: 256×256, xabarda yangi va asl hajm: [256,256,true]
✅ fokus «Kesish…» ga qaytdi: "rasm-kes"
✅ kesish: CSP buzilishi yo'q: []

Ikkinchi qatorni hisob bilan solishtiring: kvadrat rasm, zoom 1.5 → 256 × 1.5 = 384 piksel. Birinchi yuklash JS — 220.5 → 221.5 KB gzip (+1.0), CSS — 4.99 KB.

bash
git add -A
git commit

Commit (kanon): feat: profil rasmini kesish — o'z komponent, canvas → 256×256 WebP.

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
birinchi ekrandagi rasm kech chiqadi unga ham loading="lazy" priority prop: eager + fetchPriority="high"
rasm kelganda matn sakraydi width/height yo'q nisbat beradigan ikki son
CDN hisobi kutilmaganda katta imzosiz transformatsiya manzili imzo + ruxsat etilgan o'lchamlar
lightbox yopilgach fokus sahifa boshida ochgan tugma eslab qolinmagan ref + flushSync bilan fokus qaytarish
-1 % 3 → -1, rasm topilmadi manfiy qoldiq (i + step + n) % n
kesilgan rasm cho'zilgan drawImage da nisbat buzilgan manba ham, natija ham kvadrat
konsolda requires 'TrustedHTML' kutubxona innerHTML bilan <style> qo'shadi siyosat yoki o'z komponent
kesish oynasi rasm o'rnida bo'sh o'lcham onLoad dan oldin o'qilgan naturalWidth ni onLoad da oling

10. Mashqlar

Mashqlar kurs/mashqlar/21/12-rasm-ui/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): bo'sh joylarni to'ldiring

  • Birinchi ekrandagi asosiy rasmga qo'yiladigan prop qiymati: fetchPriority=""
  • Rasm kelguncha joyni band qiladigan ikki atribut: width va
  • React 19 da rasmni <head> orqali oldindan so'raydigan funksiya:
  • Imzosiz CDN manzilida hujumchi o'zgartiradigan qism:
  • react-easy-crop vazifalar da ochilmadi, chunki u <style> ni bilan qo'shadi
Yechim
  • high — brauzer bu rasmni boshqa rasmlardan oldin so'raydi.
  • height — width bilan birga nisbatni beradi, CLS bo'lmaydi.
  • preload — react-dom dan, render ichida chaqirsa bo'ladi.
  • o'lcham (yoki boshqa transformatsiya parametri) — har yangi qiymat yangi variant va xarajat.
  • innerHTML — Trusted Types oddiy satrni innerHTML ga yozishni taqiqlaydi.

2-mashq (o'rta): lightbox'da Home va End

«O'z lightbox'imiz» blokini oling va ikki tugma qo'shing: Home — birinchi rasm, End — oxirgisi. Rasm sarlavhasi yonida «(oxirgi)» so'zi chiqsin, agar oxirgi rasm ochiq bo'lsa. Sinov: «Osh» ni bosing, End, keyin Escape — konsolda fokus: Osh.

Ishora: onMove o'rniga onJump(index) prop'i qo'shing; handleKey ga ikki if. Fokus qaytishi — o'sha opener ref'i.

Yechim
jsx
import { useEffect, useRef, useState } from "react";

const PHOTOS = [
  { title: "Osh", alt: "Osh, qazi bilan" },
  { title: "Manti", alt: "Manti, qatiq bilan" },
  { title: "Lag'mon", alt: "Lag'mon, ko'kat bilan" },
];

function Lightbox({ index, onClose, onJump }) {
  const closeButton = useRef(null);
  useEffect(() => closeButton.current.focus(), []);
  const last = PHOTOS.length - 1;
  const photo = PHOTOS[index];

  function handleKey(event) {
    if (event.key === "Escape") onClose();
    if (event.key === "Home") onJump(0);
    if (event.key === "End") onJump(last);
  }

  return (
    <div role="dialog" aria-modal="true" aria-label={photo.title}
      onKeyDown={handleKey}>
      <p>
        {index + 1} / {PHOTOS.length} — {photo.title}
        {index === last && " (oxirgi)"}
      </p>
      <button ref={closeButton} onClick={onClose}>
        Yopish
      </button>
    </div>
  );
}

export default function App() {
  const [open, setOpen] = useState(null);
  const opener = useRef(null);

  function close() {
    setOpen(null);
    requestAnimationFrame(() => {
      opener.current.focus();
      console.log("fokus:", document.activeElement.textContent);
    });
  }

  return (
    <>
      {PHOTOS.map((photo, i) => (
        <button
          key={photo.title}
          onClick={(event) => {
            opener.current = event.currentTarget;
            setOpen(i);
          }}
        >
          {photo.title}
        </button>
      ))}
      {open !== null && (
        <Lightbox index={open} onClose={close} onJump={setOpen} />
      )}
    </>
  );
}

Konsolda:

text
fokus: Osh

onJump={setOpen} — state funksiyasini to'g'ridan-to'g'ri prop qilib berish mumkin: u indeksni oladi. index === last && " (oxirgi)" — shart yolg'on bo'lsa false chiqadi, React uni chizmaydi.

3-mashq (qiyin): CDN srcset va ruxsat etilgan o'lchamlar

cdnSrcSet(id, widths) funksiyasini xavfsizroq qiling:

  1. Faqat ALLOWED = [320, 640, 960, 1280] dagi o'lchamlar ishlatilsin; ro'yxatda yo'q o'lcham eng yaqin kattasiga almashsin (700 → 960). 1280 dan kattasi — 1280.
  2. Takror o'lchamlar bir marta chiqsin (700 va 900 ikkalasi 960 bo'lsa — bitta).
  3. Natija o'sish tartibida.

Sinov: cdnSrcSet("osh.jpg", [700, 300, 900, 2000]) → uchta variant: 320, 960, 1280.

Ishora: eng yaqin kattasi — ALLOWED.find((a) => a >= w) ?? 1280; takrorsiz — Set; tartib — sort((a, b) => a - b).

Yechim
js
const CLOUD = "https://res.cloudinary.com/sizning_bulut";
const ALLOWED = [320, 640, 960, 1280];

function cdnUrl(id, w) {
  return `${CLOUD}/image/upload/c_fill,w_${w},f_auto,q_auto/${id}`;
}

function cdnSrcSet(id, widths) {
  const max = ALLOWED[ALLOWED.length - 1];
  const safe = widths.map((w) => ALLOWED.find((a) => a >= w) ?? max);
  return [...new Set(safe)]
    .sort((a, b) => a - b)
    .map((w) => `${cdnUrl(id, w)} ${w}w`);
}

for (const line of cdnSrcSet("osh.jpg", [700, 300, 900, 2000])) {
  console.log(line);
}

Konsolda:

text
https://res.cloudinary.com/sizning_bulut/image/upload/c_fill,w_320,f_auto,q_auto/osh.jpg 320w
https://res.cloudinary.com/sizning_bulut/image/upload/c_fill,w_960,f_auto,q_auto/osh.jpg 960w
https://res.cloudinary.com/sizning_bulut/image/upload/c_fill,w_1280,f_auto,q_auto/osh.jpg 1280w

Endi kod qancha xilma-xil o'lcham so'ramasin, CDN faqat to'rt variant yasaydi — kesh samarali, xarajat oldindan ma'lum. Funksiya massiv qaytaradi; srcSet uchun .join(", ") qilasiz. Bu klient tomondagi tartib, himoya emas: hujumchi manzilni qo'lda yozishi mumkin. Himoya — imzo va CDN sozlamasidagi ro'yxat.

4-mashq: Vazifalar qadami

feat/rasm-kesish branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling: kesish.ts va uning 3 testi, RasmKesish.tsx, ProfilRasmi.tsx dagi «Kesish…» tugmasi va flushSync bilan fokus, asos.css dagi .kesish-maydon.

Yechim

Tekshiruv ro'yxati:

  • npm test — node 224, Vitest 65; npm run tip, lint, knip — toza.
  • Sozlamalar → rasm tanlang → «Kesish…»: dumaloq maydon, uchta yorliqli slider. Klaviatura bilan kattalashtirish ishlaydi, sichqoncha bilan sudrash ham.
  • «Kesish» — xabarda yangi va asl hajm («Kesildi: 256×256, … (asli …)»), fokus «Kesish…» tugmasida.
  • «Bekor qilish» — asl rasm joyida, fokus yana «Kesish…» da.
  • DevTools konsolida CSP xatosi yo'q; npm run build — birinchi yuklash ~221.5 KB.

11. Real ishda

  • Onlayn do'konlar va menyular — mahsulot galereyasi lightbox bilan, rasmlar CDN'dan srcset bilan. Rasm CDN'i (Cloudinary, imgproxy, Cloudflare Images) deyarli har katta loyihada bor.
  • Profil va hujjatlar — avatar kesish, pasport rasmini yuklashdan oldin qirqish. Ko'p jamoalar react-easy-crop ishlatadi; qattiq CSP'li jamoalar — o'z komponentini.
  • Kod ko'rigida so'raladi: "LCP rasmga lazy qo'yilmaganmi?", "CDN manzili imzolanganmi?", "lightbox fokusni qaytaradimi?".
  • Intervyu savollari: "srcset va sizes qanday ishlaydi?", "placeholder qanday qilinadi?", "rasmni brauzerda qanday kesasiz?", "imzolangan URL nega kerak?".

Xulosa

  • React'da rasm: srcSet, sizes, width/height, loading, decoding, fetchPriority; asosiy rasm uchun preload(). React src ni oxirida yozadi.
  • Placeholder — rang yoki BlurHash/ThumbHash, onLoad dan keyin rasm.
  • CDN transformatsiyasi variantni manzilda so'raydi; manzil serverda imzolanadi, o'lchamlar ro'yxat bilan cheklanadi.
  • Lightbox — modal oyna: fokus ichkariga va qaytib, Esc va strelkalar; loyihada <dialog> yoki YARL (+10.6 KB).
  • Kesish = hisob (max masshtab, siljish ulushi) + canvas drawImage + toBlob WebP; boshqaruv — slider, sudrash qo'shimcha.
  • react-easy-crop Trusted Types ostida ochilmadi → kanon o'z komponenti (+1.0 KB).

Keyingi dars: Video va audio — React'da pleyer: <video> ref bilan, holatni hodisalardan olish, HLS oqimi, YouTube embed va Media Session.

Manbalar

  • React: <img> (fetchPriority), preload — react.dev/reference/react-dom/components/img, react.dev/reference/react-dom/preload
  • MDN: HTMLImageElement.fetchPriority, CanvasRenderingContext2D.drawImage(), HTMLCanvasElement.toBlob()
  • Cloudinary: "Image transformations", "Strict transformations" — cloudinary.com/documentation
  • imgproxy: "Signing a URL", "Processing options" — docs.imgproxy.net
  • yet-another-react-lightbox — yet-another-react-lightbox.com; react-medium-image-zoom — npm; react-easy-crop — npm
  • ThumbHash — evanw.github.io/thumbhash; BlurHash — blurha.sh; vite-imagetools — npm
  • Kanon: vazifalar holat 12, natijalar/tekshiruv-12.txt, 12-axe.txt, brauzer-tekshiruv/natija-21-12.txt, natijalar/tajriba/kutubxonalar-21.txt (2026-10-17)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Rasm UI React'da: lightbox, zoom, kesish (crop) va CDN transformatsiyalari — IlmHamroh