IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar36/36-dars28 daqiqa
Mundarija (31)

React'da fayl yuklash UI: tanlash, sudrab tashlash, preview, progress va bekor qilish

Qisqacha: Fayl yuklash UI besh qismdan iborat. Tanlash — <input type="file" accept> asosiy yo'l, sudrab tashlash qo'shimcha. Tekshiruv — tur va hajm: mehmonga tez javob uchun, himoya emas. Oldindan ko'rish — URL.createObjectURL(file) → blob: manzil, ish tugagach revokeObjectURL. Yuborish jarayoni — fetch yuborish progressini bermaydi, shuning uchun XMLHttpRequest ning upload "progress" hodisasi. Bekor qilish — abort. Katta fayllar to'g'ridan bulutga — presigned URL bilan. Fayl turi va hajmini server baribir qayta tekshiradi.

Bu darsda

  • Fayl tanlash maydonini accept va multiple bilan yozasiz, File obyektidan nom, tur va hajmni o'qiysiz.
  • Tur va hajmni tekshirib, o'zbekcha xabar berasiz — va nega bu himoya emasligini tushunasiz.
  • Rasmni createObjectURL bilan oldindan ko'rsatasiz va xotirani revokeObjectURL bilan bo'shatasiz.
  • Sudrab tashlash zonasini, progress va «Bekor qilish» ni quraysiz; presigned URL va bo'lib yuklash g'oyasini bilasiz.
  • vazifalar ga profil rasmini qo'shib, 19-qismni yakunlaysiz (v9).

Oldin bilishingiz kerak: File va Blob, Faylni o'qish va oldindan ko'rish, Fayl yuklash va progress, Drag and Drop API, IndexedDB kutubxonalari.

1. Nega bu kerak?

13-qismda faylni brauzer API'lari bilan yuklagansiz: File, FormData, XMLHttpRequest. Endi xuddi shu ishni React komponentida, foydalanuvchi uchun qulay qilib qilamiz. Jasur aka «Bahor» xodimlari profil rasmini qo'yishini xohlaydi, va ro'yxat oddiy emas:

  • Rasm tanlanganda darhol ko'rinsin — "to'g'ri rasmni tanladimmi?".
  • 15 MB lik telefon surati yoki PDF tanlansa — darhol tushunarli xabar, serverga bormasdan.
  • Yuborish sekin internetda uzoq: progress — "qancha qoldi?", va «Bekor qilish».
  • Kompyuterda faylni papkadan oynaga sudrab tashlash ham ishlasin.

Hayotiy o'xshatish: pochtaga posilka topshirish. Operator avval ko'radi (preview), o'lchaydi va tortadi (tur va hajm), keyin kvitansiyada yo'lini kuzatasiz (progress), xohlasangiz qaytarib olasiz (bekor qilish). Lekin chegaradagi bojxona posilkani baribir o'zi ochib tekshiradi — operatorning "ichida kitob" degan yozuviga ishonmaydi. Bojxona — server.

Diqqat: Bu darsdagi jonli misollar faylni hech qayerga yubormaydi: natija oynasidan tarmoqqa chiqib bo'lmaydi. Progress — soxta (taymer bilan), bu ochiq aytiladi. Haqiqiy yuborish — vazifalar qadamida, «Bahor» mashq serveriga (u faylni saqlamaydi, faqat hajmini qaytaradi).

2. Fayl tanlash

2.1 <input type="file">

Asosiy yo'l — brauzerning o'z maydoni: telefonda galereya va kamera, kompyuterda fayl oynasi, klaviatura va ekran o'quvchi bilan ishlaydi. Tanlangan fayllar — event.target.files (FileList), har biri — File (File va Blob):

jsx
import { useState } from "react";

function sizeText(bytes) {
  if (bytes < 1000) return `${bytes} B`;
  if (bytes < 1_000_000) return `${(bytes / 1000).toFixed(1)} KB`;
  return `${(bytes / 1_000_000).toFixed(1)} MB`;
}

export default function App() {
  const [files, setFiles] = useState([]);
  // Tekshiruvchi fayl oynasini ocha olmaydi — sinov tugmasi
  function addSamples() {
    setFiles([
      new File(["x".repeat(120_000)], "osh.png", {
        type: "image/png",
      }),
      new File(["%PDF"], "menyu.pdf", { type: "application/pdf" }),
    ]);
  }
  return (
    <div>
      <label htmlFor="photos">Rasmlar</label>{" "}
      <input
        id="photos"
        type="file"
        accept="image/png,image/jpeg,image/webp"
        multiple
        onChange={(e) => setFiles([...e.target.files])}
      />
      <button type="button" onClick={addSamples}>
        Sinov fayllari
      </button>
      <ul>
        {files.map((f) => (
          <li key={f.name}>
            {f.name} — {f.type || "turi noma'lum"}, {sizeText(f.size)}
          </li>
        ))}
      </ul>
    </div>
  );
}

Ekranda:

text
Rasmlar Sinov fayllari
osh.png — image/png, 120.0 KB
menyu.pdf — application/pdf, 4 B

«Ishga tushir» dan keyin maydonni bosib, o'z rasmingizni tanlang — ro'yxatda u ham chiqadi. Sinov tugmasi bizning tekshiruvchi uchun: u fayl oynasini ocha olmaydi, shuning uchun new File([...], nom, { type }) bilan fayl yasadik.

Atributlar:

Atribut Nima qiladi
accept="image/png,image/jpeg" fayl oynasida faqat shu turlar ko'rinadi (MIME yoki .png kengaytma)
accept="image/*" har qanday rasm; telefonda kamera ham taklif qilinadi
multiple bir nechta fayl
capture="environment" telefonda to'g'ridan orqa kamera (galereyasiz)

[...e.target.files] — FileList massiv emas, spread bilan massivga aylantiramiz (spread). E'tibor bering: menyu.pdf ro'yxatga tushdi, garchi accept faqat rasmlarni aytsa ham. Demak, accept — fayl oynasi uchun maslahat, xolos: "Barcha fayllar" ni tanlab yoki sudrab tashlab, uni chetlab o'tish mumkin. Tekshiruv — alohida.

Fayl maydoni uncontrolled: uning value sini React boshqara olmaydi (xavfsizlik uchun — sayt foydalanuvchi diskidagi yo'lni yozib qo'ya olmaydi). Shuning uchun RHF da register("photo") FileList beradi, Controller esa kerak emas (RHF I).

2.2 Tur va hajm tekshiruvi

Fayl tanlangan zahoti tekshiramiz: tur va hajm. Sof funksiya — React'siz, testlanadigan (Zod chuqurroq dagi kabi alohida fayl):

jsx
import { useState } from "react";

const ALLOWED = ["image/png", "image/jpeg", "image/webp"];
const MAX = 1_000_000; // 1 MB

function checkFile(file) {
  if (!ALLOWED.includes(file.type)) {
    return `«${file.name}» — rasm emas (PNG, JPEG yoki WebP kerak).`;
  }
  if (file.size === 0) return `«${file.name}» bo'sh.`;
  if (file.size > MAX) {
    const mb = (file.size / 1_000_000).toFixed(1);
    return `«${file.name}» juda katta (${mb} MB; ko'pi bilan 1 MB).`;
  }
  return null;
}

const SAMPLES = [
  ["matn.txt", "text/plain", 300],
  ["katta.jpg", "image/jpeg", 2_400_000],
  ["bo'sh.png", "image/png", 0],
  ["osh.webp", "image/webp", 85_000],
];

export default function App() {
  const [log, setLog] = useState([]);
  function pick([name, type, size]) {
    const file = new File(["x".repeat(size)], name, { type });
    setLog((l) => [...l, checkFile(file) ?? `«${name}» — yaroqli`]);
  }
  return (
    <div>
      {SAMPLES.map((s) => (
        <button key={s[0]} type="button" onClick={() => pick(s)}>
          {s[0]}
        </button>
      ))}
      <ol>
        {log.map((line, i) => (
          <li key={i}>{line}</li>
        ))}
      </ol>
    </div>
  );
}

Ekranda:

text
matn.txtkatta.jpgbo'sh.pngosh.webp
«matn.txt» — rasm emas (PNG, JPEG yoki WebP kerak).
«katta.jpg» juda katta (2.4 MB; ko'pi bilan 1 MB).
«bo'sh.png» bo'sh.
«osh.webp» — yaroqli

Birinchi qatorda to'rt tugma yopishib turibdi — ular orasida bo'shliq yo'q (tekshiruvchining matn ko'rinishi; saytda tugmalar chegarasi bilan ajralib ko'rinadi). Tekshiruv tartibi: avval tur (eng ko'p uchraydigan xato), keyin bo'sh fayl, keyin hajm. Xabarda fayl nomi — bir nechta fayl tanlanganda qaysi biri ekani aniq bo'lsin. SVG ruxsat etilganlar ichida yo'q — sababi «Hujumchi nigohi» bo'limida.

Tekshirib ko'ring: Mehmon virus.exe ni osh.png deb qayta nomladi va tanladi. checkFile nima deydi?

Javob

"Yaroqli" — agar hajmi 1 MB dan kichik bo'lsa. file.type faylning ichidan emas, kengaytmasidan taxmin qilinadi: .png → image/png. Brauzer faylning tarkibini tekshirmaydi. Shuning uchun brauzerdagi tekshiruv — mehmon xato qilganda tez javob berish uchun, hujumdan himoya uchun emas. Haqiqiy tekshiruv (faylning birinchi baytlari — "sehrli raqamlar", hajm, rasm sifatida ochiladimi) — serverda.

3. Oldindan ko'rish (preview)

3.1 createObjectURL va revokeObjectURL

Rasmni ko'rsatish uchun unga manzil kerak. Ikki yo'l bor (Faylni o'qish va oldindan ko'rish):

Yo'l Natija Narxi
URL.createObjectURL(file) blob:https://…/3f2a… — faylga ko'rsatkich darhol, nusxa yo'q; bo'shatish kerak
FileReader.readAsDataURL(file) data:image/png;base64,iVBOR… butun fayl o'qiladi, base64 — +33%

createObjectURL tezroq va xotirani tejaydi, lekin bitta majburiyat bor: URL kerak bo'lmay qolganda URL.revokeObjectURL(url). Aks holda brauzer faylni sahifa yopilguncha xotirada ushlab turadi — mehmon 20 marta rasm almashtirsa, 20 ta rasm xotirada.

Bu misol faqat saytda ishlaydi: bizning tekshiruvchi (jsdom) createObjectURL ni bilmaydi. Biz uni Chrome 155 da alohida sinadik (natija pastda):

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

// 1×1 piksel PNG — sinov uchun (o'z rasmingizni ham tanlang)
const PNG = Uint8Array.from(
  atob(
    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ" +
      "AAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==",
  ),
  (c) => c.charCodeAt(0),
);

export default function App() {
  const [preview, setPreview] = useState(null);
  const current = useRef(null);

  function show(file) {
    if (current.current !== null) {
      URL.revokeObjectURL(current.current); // eskisini bo'shat
      console.log("bo'shatildi");
    }
    const url = URL.createObjectURL(file);
    current.current = url;
    setPreview(url);
    console.log("yangi:", url.startsWith("blob:"));
  }

  // Komponent yopilsa — oxirgisini ham bo'shat
  useEffect(
    () => () => {
      if (current.current !== null) {
        URL.revokeObjectURL(current.current);
      }
    },
    [],
  );

  return (
    <div>
      <label htmlFor="photo">Rasm</label>{" "}
      <input
        id="photo"
        type="file"
        accept="image/*"
        onChange={(e) => e.target.files[0] && show(e.target.files[0])}
      />
      <button
        type="button"
        onClick={() =>
          show(new File([PNG], "nuqta.png", { type: "image/png" }))
        }
      >
        Sinov rasmi
      </button>
      {preview && (
        <img
          src={preview}
          alt="Tanlangan rasm (hali saqlanmagan)"
          width={96}
          height={96}
        />
      )}
    </div>
  );
}

Chrome 155.0.8059.40 da (saytdagi natija oynasi hujjati — o'sha React va o'giruvchi, headless; ikki marta «Sinov rasmi») konsol — rasm ham chizildi (blob: manzil, kengligi 1 piksel):

text
yangi: true
bo'shatildi
yangi: true

Ikkinchi tanlovda birinchisi bo'shatildi. Uch nuqta:

  • URL hodisada yaratiladi, render yoki effektda emas. Render paytida createObjectURL chaqirilsa, har renderda yangi URL va eskisi bo'shatilmay qoladi. Effektda yaratib state'ga yozish esa ortiqcha render (Sizga effekt kerak emas) — kanondagi ESLint qoidasi (set-state-in-effect) buni ushlaydi.
  • useRef — joriy URL (useRef): uni bo'shatish uchun eslab qolamiz, lekin uning o'zgarishi render chaqirmasin.
  • alt — "hali saqlanmagan" — Malika (ekran o'quvchi) bu preview ekanini bilsin.

Maslahat: blob: manzilni boshqa tabda ochib bo'lmaydi va u sahifa yopilganda o'ladi. Uni serverga yubormang va localStorage ga yozmang — saqlash uchun faylning o'zi kerak (vazifalar da — IndexedDB).

3.2 Rasmni yuborishdan oldin kichraytirish

Telefon surati 5–12 MB. Profil rasmi uchun 256×256 piksel yetadi. Brauzerda kichraytirish — <canvas> (Canvas 2D) yoki createImageBitmap + OffscreenCanvas. Tayyor kutubxona — browser-image-compression:

js
import imageCompression from "browser-image-compression";

const small = await imageCompression(file, {
  maxSizeMB: 0.5,
  maxWidthOrHeight: 512,
  useWebWorker: true,
});

Kesish (crop) — react-easy-crop: mehmon rasmning qaysi qismi ko'rinishini tanlaydi, kutubxona croppedAreaPixels qaytaradi, uni canvas bilan kesasiz. Bular vazifalar da yo'q — 1 MB chegarasi bilan foydalanuvchi o'zi kichik rasm tanlaydi.

4. Sudrab tashlash (drag and drop)

Kompyuterda fayl oynasidan ko'ra papkadan sudrab tashlash qulay. Drag and Drop API dagi uch hodisa yetadi: dragover (preventDefault — "bu yerga tashlash mumkin"), dragleave, drop (event.dataTransfer.files):

jsx
import { useState } from "react";

export default function App() {
  const [over, setOver] = useState(false);
  const [names, setNames] = useState([]);

  function onDrop(e) {
    e.preventDefault(); // brauzer faylni ochib yubormasin
    setOver(false);
    setNames([...e.dataTransfer.files].map((f) => f.name));
  }

  return (
    <div>
      <div
        onDragOver={(e) => {
          e.preventDefault();
          setOver(true);
        }}
        onDragLeave={() => setOver(false)}
        onDrop={onDrop}
        style={{
          padding: 24,
          border: `2px dashed ${over ? "#2f7d32" : "#999"}`,
        }}
      >
        <label htmlFor="drop-file">
          Rasmni shu yerga tashlang yoki tanlang
        </label>{" "}
        <input
          id="drop-file"
          type="file"
          accept="image/*"
          onChange={(e) =>
            setNames([...e.target.files].map((f) => f.name))
          }
        />
      </div>
      <p>Tanlandi: {names.join(", ") || "hali yo'q"}</p>
    </div>
  );
}

Ekranda:

text
Rasmni shu yerga tashlang yoki tanlang
Tanlandi: hali yo'q

Kompyuterda «Ishga tushir» ni bosib, papkadan faylni ramkaga tashlab ko'ring — ramka yashil bo'ladi va nomi chiqadi. Tekshiruvchimiz sudray olmaydi, shuning uchun bu yerda faqat boshlang'ich holat.

Muhim qoida: zona ichida oddiy <input type="file"> qoladi. Sudrab tashlash sichqoncha va kompyuter uchun qo'shimcha qulaylik. Telefonda, klaviaturada va ekran o'quvchida u ishlamaydi — ular uchun asosiy yo'l maydonning o'zi. drop dagi fayllar ham accept dan o'tmaydi — tekshiruv baribir kerak.

Tayyor kutubxona — react-dropzone: useDropzone({ accept, maxSize, onDrop }) hook'i zona va yashirin input uchun props beradi, tur va hajmni o'zi tekshiradi (headless naqshi):

jsx
const { getRootProps, getInputProps, isDragActive, fileRejections } =
  useDropzone({ accept: { "image/*": [] }, maxSize: 1_000_000 });

<div {...getRootProps()}>
  <input {...getInputProps()} />
  {isDragActive ? "Tashlang…" : "Rasmni tashlang yoki bosing"}
</div>

5. Yuborish: progress va bekor qilish

5.1 Nega fetch emas

fetch javob yuklanishini (download) oqim bilan kuzatishga imkon beradi, lekin yuborish (upload) jarayoni haqida hodisa bermaydi. Yuborish progressi uchun hali ham XMLHttpRequest kerak: xhr.upload.addEventListener("progress", …) — event.loaded / event.total (Fayl yuklash va progress). axios ichida XHR ishlatadi, shuning uchun unda onUploadProgress bor:

js
await axios.post("/api/rasm", formData, {
  onUploadProgress: (e) =>
    setPercent(Math.round((e.loaded / e.total) * 100)),
  signal: controller.signal,
});

5.2 Progress UI va bekor qilish (soxta yuklash)

UI qismini sinab ko'ramiz. Tarmoq yo'q, shuning uchun "yuborish" — taymer: har 100 ms da 20%. Bu soxta, faqat UI'ni ko'rsatish uchun. Bekor qilish — AbortController (Bekor qilish: AbortController), xuddi haqiqiy so'rovdagidek:

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

// SOXTA yuborish: tarmoq yo'q — har 100 ms da 20%
function fakeUpload(file, { onProgress, signal }) {
  return new Promise((resolve, reject) => {
    let sent = 0;
    const timer = setInterval(() => {
      sent = Math.min(file.size, sent + file.size / 5);
      onProgress(sent, file.size);
      if (sent === file.size) {
        clearInterval(timer);
        resolve();
      }
    }, 100);
    signal.addEventListener("abort", () => {
      clearInterval(timer);
      reject(new DOMException("Bekor qilindi", "AbortError"));
    });
  });
}

export default function App() {
  const [progress, setProgress] = useState(null);
  const [message, setMessage] = useState("");
  const controller = useRef(null);

  async function upload() {
    const file = new File(["x".repeat(50_000)], "osh.png", {
      type: "image/png",
    });
    controller.current = new AbortController();
    setMessage("");
    setProgress({ sent: 0, total: file.size }); // darhol 0%
    try {
      await fakeUpload(file, {
        signal: controller.current.signal,
        onProgress: (sent, total) => setProgress({ sent, total }),
      });
      setMessage("Yuklandi (soxta)");
    } catch (error) {
      const aborted = error.name === "AbortError";
      setMessage(aborted ? "Bekor qilindi" : "Xato");
    } finally {
      setProgress(null);
    }
  }

  return (
    <div>
      {progress === null ? (
        <button type="button" onClick={() => void upload()}>
          Yuklash
        </button>
      ) : (
        <p>
          <label htmlFor="up">Yuborilmoqda</label>{" "}
          <progress
            id="up"
            max={progress.total}
            value={progress.sent}
          />{" "}
          {Math.round((progress.sent / progress.total) * 100)}%{" "}
          <button
            type="button"
            onClick={() => controller.current.abort()}
          >
            Bekor qilish
          </button>
        </p>
      )}
      <p role="status">{message}</p>
    </div>
  );
}

Ekranda:

text
Yuklash
Bekor qilindi

Ikki ssenariy ketma-ket: birinchisida progress oxirigacha borib "Yuklandi (soxta)" chiqdi, ikkinchisida ~150 ms dan keyin «Bekor qilish» — "Bekor qilindi". UI tafsilotlari:

  • <progress max value> — brauzerning o'z elementi: ekran o'quvchi foizni o'qiydi. <label> — "nima yuborilmoqda".
  • Foiz matni ham yonida — rang ko'rmaydigan va kichik ekranda ham tushunarli.
  • role="status" — natija (yuklandi / bekor qilindi) ekran o'quvchiga e'lon qilinadi.
  • Bekor qilish — xato emas. AbortError alohida xabar oladi, "Xato" emas.
  • Yuborish paytida fayl maydoni disabled bo'lishi kerak — ikki yuborish aralashmasin (vazifalar da shunday).

5.3 Katta fayllar: presigned URL va bo'lib yuklash

Video yoki 500 MB arxiv o'z serveringiz orqali o'tsa, server tarmog'i va xotirasi band bo'ladi. Odatiy yechim — presigned URL (oldindan imzolangan manzil). Server bulutdagi fayl saqlash joyiga (S3, MinIO) faqat bitta fayl va qisqa muddat (masalan, 5 daqiqa) uchun ruxsat beradi, brauzer faylni to'g'ridan bulutga yuboradi:

sequenceDiagram
  participant B as Brauzer
  participant S as Sizning server
  participant O as Bulut (S3)
  B->>S: "osh.mp4, 80 MB yuklamoqchiman"
  S->>S: huquq, tur, hajm tekshiruvi
  S-->>B: presigned URL (5 daqiqa)
  B->>O: PUT fayl (progress — XHR)
  O-->>B: 200
  B->>S: "yuklandi" (kalit)
  S->>O: fayl bormi, hajmi to'g'rimi?

Server faylni ko'rmaydi, lekin qarorni u qiladi: kim, qaysi tur, qancha hajm, qaysi kalit nomi bilan. Oxirida yana tekshiradi — brauzer "yukladim" desa ham. Bu backend qismi — Presigned URL'lar va Object storage darslarida.

Yana ikki atama:

  • Bo'lib yuklash (chunked / multipart upload) — katta fayl 5–10 MB bo'laklarga bo'linadi (file.slice(a, b)), har biri alohida yuboriladi. Internet uzilsa, faqat oxirgi bo'lak qayta yuboriladi. Protokollar: S3 multipart, tus (to'xtab-davom etadigan yuklash).
  • Uppy — tayyor yuklash UI'i: sudrab tashlash, preview, progress, kamera, tus/S3, bekor qilish va qayta urinish. Ko'p faylli murakkab yuklash kerak bo'lsa — o'zingiz yozgandan ko'ra tezroq.

6. Hujumchi nigohi

Fayl yuklash — veb-ilovalardagi eng xavfli joylardan biri. Hujumchi shunday o'ylaydi:

  1. "Brauzer tekshiruvini chetlab o'taman." accept, checkFile — hammasi brauzerda. DevTools'da kodni o'zgartirish yoki curl -F "rasm=@virus.exe;type=image/png" bilan so'rovni to'g'ridan yuborish — bir daqiqalik ish. Himoya: server tur va hajmni o'zi tekshiradi: faylning birinchi baytlari ("sehrli raqamlar": PNG — 89 50 4E 47), hajm chegarasi (oshsa — 413 Payload Too Large), rasm haqiqatan ochiladimi.
  2. "Rasm deb skript yuboraman." SVG — rasm, lekin ichida <script> bo'lishi mumkin. Sayt uni o'z domenidan ochsa — XSS (XSS va DOM xavfsizligi). Himoya: ruxsat ro'yxatida SVG yo'q (vazifalar da PNG, JPEG, WebP); serverda fayllar alohida domenda yoki Content-Disposition: attachment bilan beriladi, X-Content-Type-Options: nosniff — brauzer turini "taxmin qilmaydi".
  3. "Fayl nomi bilan o'ynayman." ../../etc/passwd yoki <img onerror=…>.png. Himoya: server fayl nomini o'zi yaratadi (tasodifiy kalit), mehmon bergan nom faqat ko'rsatish uchun va matn sifatida (React buni {name} da avtomatik ekranlaydi).
  4. "Juda katta yoki juda ko'p fayl yuboraman" — disk to'ladi, server qotadi. Himoya: hajm chegarasi serverda va proksi'da (Nginx client_max_body_size), so'rovlar soniga chegara (rate limit).

Bizning checkFile — mehmon uchun tez va tushunarli javob: "bu PDF, rasm kerak" — serverga bormasdan. Bu yaxshi UX, lekin himoya emas. Brauzer tekshiruvi va server tekshiruvi bir-birini almashtirmaydi — ikkalasi ham kerak. Server tomoni — Fayl yuklash xavfsizligi darsida.

7. Vazifalar qadami: profil rasmi (holat 36 = v9)

7.1 Nima quramiz

Sozlamalar sahifasida "Profil rasmi" bo'limi: tanlash yoki sudrab tashlash, tekshiruv, preview, «Saqlash» — progress va «Bekor qilish» bilan, keyin rasm qurilmada saqlanadi va yuqori menyuda ism yonida kichik avatar ko'rinadi. Bu — 19-qismning oxirgi qadami.

bash
git switch -c feat/profil-rasmi
npm install --save-exact idb-keyval@6.3.0

Fayllar — src/xususiyatlar/profil-rasmi/ (katta loyiha arxitekturasi dagi "xususiyat papkasi"): fayl-tekshir.ts, yuklash.ts, profil-rasmi.ts, ProfilRasmi.tsx va ochiq API — index.ts.

7.2 Tekshiruv: fayl-tekshir.ts

Kanon (to'liq; izoh qatorlari telefonga sig'ishi uchun bo'lingan):

ts
// fayl-tekshir.ts — tanlangan rasm (19/#36), sof funksiya
// (node:test). Klientdagi tekshiruv — foydalanuvchiga TEZ javob
// uchun, himoya emas: file.type — brauzer kengaytmadan taxmin
// qiladi, o'zgartirib bo'ladi. Haqiqiy tekshiruv (tarkib, hajm) —
// serverda (29-qism)

export const RUXSAT_TURLAR = [
  "image/png",
  "image/jpeg",
  "image/webp",
] as const;
/**
 * 1 MB — «Bahor» mashq serveri (aks) ham taxminan shunchasini
 * qabul qiladi
 */
const ENG_KATTA = 1_000_000;

type Fayl = {
  readonly name: string;
  readonly type: string;
  readonly size: number;
};

/** Hajm — odam o'qiydigan: 950 B, 12.3 KB, 1.2 MB */
export function hajmMatni(bayt: number): string {
  if (bayt < 1000) {
    return `${bayt} B`;
  }
  if (bayt < 1_000_000) {
    return `${(bayt / 1000).toFixed(1)} KB`;
  }
  return `${(bayt / 1_000_000).toFixed(1)} MB`;
}

/** @returns xato matni yoki null (fayl yaroqli) */
export function faylniTekshir(fayl: Fayl): string | null {
  if (!RUXSAT_TURLAR.some((tur) => tur === fayl.type)) {
    return `«${fayl.name}» — rasm emas (PNG, JPEG yoki WebP kerak).`;
  }
  if (fayl.size === 0) {
    return `«${fayl.name}» bo'sh.`;
  }
  if (fayl.size > ENG_KATTA) {
    return `«${fayl.name}» juda katta (${hajmMatni(fayl.size)}; ko'pi bilan ${hajmMatni(ENG_KATTA)}).`;
  }
  return null;
}

«Tur va hajm tekshiruvi» bo'limidagi checkFile — shuning o'zi. Ikki tafsilot: Fayl turi — File emas, faqat uch maydon. Shuning uchun node:test da (brauzersiz) oddiy obyekt bilan sinaladi: faylniTekshir({ name: "a.svg", type: "image/svg+xml", size: 10 }). Va boshidagi izoh — «Hujumchi nigohi» ning qisqa mazmuni kodning o'zida.

7.3 Yuborish: yuklash.ts

XHR, progress va bekor qilish (kanon, asosiy qismi):

ts
export function faylniYukla(
  manzil: string,
  fayl: Blob,
  sozlama: { onJarayon: (j: Jarayon) => void; signal: AbortSignal },
): Promise<void> {
  return new Promise((hal, rad) => {
    const xhr = new XMLHttpRequest();
    const malumot = new FormData();
    malumot.append("rasm", fayl);
    xhr.upload.addEventListener("progress", (event) => {
      if (event.lengthComputable) {
        sozlama.onJarayon({
          yuborildi: event.loaded,
          jami: event.total,
        });
      }
    });
    xhr.addEventListener("load", () => {
      if (xhr.status < 200 || xhr.status >= 300) {
        rad(new Error(`Server xatosi: ${xhr.status}`));
        return;
      }
      // Server haqiqatan shu hajmdagi faylni olganmi
      const natija = javobSxemasi.safeParse(
        JSON.parse(xhr.responseText),
      );
      const kelgan = natija.success ? natija.data.tana[0] : undefined;
      if (kelgan?.hajm !== fayl.size) {
        rad(new Error("Server faylni to'liq olmadi."));
        return;
      }
      hal();
    });
    // ... "error" — "Internet aloqasini tekshiring.",
    // "abort" — AbortError
    sozlama.signal.addEventListener("abort", () => xhr.abort(), {
      once: true,
    });
    xhr.open("POST", manzil);
    xhr.send(malumot);
  });
}

«Progress UI va bekor qilish» dagi soxta fakeUpload bilan bir xil shartnoma: onJarayon(yuborildi, jami) va signal — shuning uchun UI ikkalasida bir xil. Farqi — bu yerda haqiqiy tarmoq:

  • Server — «Bahor» mashq API'sining /aks manzili. U faylni saqlamaydi, faqat nima kelganini qaytaradi (nom, hajm, tur). Ya'ni "soxta" yuklash, lekin tarmoq haqiqiy: progress haqiqiy baytlarni ko'rsatadi. Kanon uni curl bilan tekshirgan: 1 MB — 200, 2 MB — 413.
  • Javob ham tekshiriladi — zod/mini sxemasi bilan (API qatlami): server haqiqatan shu hajmdagi faylni oldimi.
  • AbortSignal → xhr.abort() — fetch dagi signal bilan bir xil API, faqat qo'lda ulangan.

7.4 Saqlash: IndexedDB va useSyncExternalStore

Rasm qayerda saqlanadi? localStorage faqat matn saqlaydi — rasm base64 bo'lib 33% kattalashadi va 5 MB chegaraning katta qismini oladi. IndexedDB Blob ni o'zini saqlaydi. Kutubxona — idb-keyval (6.3.0): get/set/del (IndexedDB kutubxonalari). Ro'yxat esa localStorage da qoladi — u yerda sinxron o'qish birinchi kadr uchun kerak (o'lchov: 10 000 vazifada localStorage 1.7 ms, idb-keyval 6.5 ms va faqat async).

Saqlangan rasm butun ilovada (sozlamalar va menyu) bitta — tashqi "store", React'ga useSyncExternalStore bilan ulanadi (kanon, profil-rasmi.ts dan):

ts
function obuna(tinglovchi: () => void): () => void {
  tinglovchilar.add(tinglovchi);
  if (!yuklandi) {
    yuklandi = true;
    // Birinchi obunachi — IndexedDB'dan (async; yo'q yoki xato —
    // rasmsiz). Promise.resolve().then: IndexedDB umuman yo'q bo'lsa
    // get() SINXRON tashlaydi (ReferenceError) — u ham rad etilgan
    // Promise bo'lsin, menyu yiqilmasin (Vitest/jsdom topdi:
    // indexedDB is not defined)
    Promise.resolve()
      .then(() => get<Blob>(KALIT))
      .then(almashtir, (xato: unknown) => {
        console.warn(`Profil rasmi o'qilmadi: ${String(xato)}`);
      });
  }
  return () => tinglovchilar.delete(tinglovchi);
}

/** blob: URL yoki null (rasm yo'q) */
export function useProfilRasmi(): string | null {
  return useSyncExternalStore(
    obuna,
    () => joriyUrl,
    () => null,
  );
}

almashtir(blob) — eski blob: URL'ni revokeObjectURL bilan bo'shatadi, yangisini yaratadi va tinglovchilarni chaqiradi. Ekranga — blob: manzil.

Izohdagi topilmaga qarang. Vitest (jsdom) da IndexedDB yo'q, va get() Promise qaytarish o'rniga sinxron xato tashladi — butun foydalanuvchi menyusi yiqildi. Real hayotda bu — eski brauzer yoki IndexedDB o'chirilgan maxfiy rejim. Promise.resolve().then(() => get(…)) — sinxron xatoni ham rad etilgan Promise'ga aylantiradi, menyu esa rasmsiz ishlaydi. Testlar ilovani himoya qildi.

7.5 Komponent va CSP

ProfilRasmi.tsx (kanon, preview qismi):

tsx
  // Obyekt URL bo'shatilmasa — Blob sahifa yopilguncha
  // xotirada qoladi
  function almashtir(yangi: File | null) {
    if (joriyUrl.current !== null) {
      URL.revokeObjectURL(joriyUrl.current);
    }
    const url = yangi === null ? null : URL.createObjectURL(yangi);
    joriyUrl.current = url;
    setTanlangan(
      yangi === null || url === null ? null : { fayl: yangi, url },
    );
  }

«createObjectURL va revokeObjectURL» bo'limidagi show ning o'zi: URL hodisada yaratiladi, eskisi bo'shatiladi, fayl va URL bitta state'da. Qolgani — darsdagi qismlar: sudrab tashlash zonasi ichida <input type="file" accept>, <progress> va foiz, role="status" xabar, yuborish paytida maydon disabled, event.target.value = "" (o'sha faylni qayta tanlash mumkin bo'lsin).

Bitta to'siq bor edi — CSP. vazifalar ning Content Security Policy'si (Brauzer xavfsizlik modeli) rasmlarni faqat o'z domenidan ruxsat berardi: img-src 'self'. blob: manzilli rasm bloklanadi. Kanon diff (index.html, vercel.json da ham):

diff
-                 style-src 'self'; img-src 'self';
+                 style-src 'self'; img-src 'self' blob:;

Nega blob:, data: emas? Chunki blob: manzilni faqat shu sahifaning o'z JavaScript'i yaratadi (createObjectURL). Ammo data: manzilni istalgan matnda yozish mumkin. Sahifaga begona HTML tushib qolsa, <img src="data:…"> orqali kuzatuv yoki chalg'ituvchi rasm qo'shish osonroq bo'lardi. Ruxsat — eng tor kerakli darajada.

Menyudagi avatar — bezak, shuning uchun alt="": ism yonida, ekran o'quvchi ismni o'qiydi, rasmni emas.

7.6 Yakuniy tekshiruvlar va commit

Yangi tekshiruv/fayl-tekshir.test.js — 3 test (turlar, SVG/bo'sh/katta, hajmMatni). Natijalar (haqiqiy, holat 36):

  • npm test — node:test ℹ tests 190 · ℹ pass 190, Vitest Tests 67 passed (67). «Orqaga» tuzatishidan (React Router III) keyin kanonda — 198.
  • Build: index-….js 480.24 kB │ gzip: 152.90 kB — +1.5 KB (idb-keyval va komponent).
  • Brauzer: 159/159, 72/72, 19-qism — 97 (tuzatishlardan keyin 100). Profil rasmi bo'limi:
text
✅ fayl: matn — rad, preview yo'q: ["«matn.txt» — rasm emas (PNG, JPEG yoki WebP kerak).",false]
✅ fayl: 1.2 MB — rad: "«katta.png» juda katta (1.2 MB; ko'pi bilan 1.0 MB)."
✅ fayl: PNG — preview (blob: URL, CSP img-src blob:), Saqlash tugmasi: [[true,1],true]
✅ bekor qilish: xhr.abort — xabar, preview joyida: ["Yuklash bekor qilindi.",true,false]
✅ saqlandi: progress (max = hajm, yorlig'i bor), /aks ga yuborildi, avatar menyuda: [[70,1],2,[true,""]]
✅ qayta ochildi: rasm IndexedDB'dan (avatar va joriy rasm): [1,true]
✅ olib tashlandi — avatar yo'q: false
✅ fayl yuklash: CSP buzilishi yo'q: []

Uchinchi qator — CSP o'zgarishining isboti: blob: rasm ko'rindi, buzilish yo'q. To'rtinchisida bekor qilingandan keyin preview joyida — mehmon qayta urinishi mumkin.

Kichik yakuniy ishlar: Service Worker versiyasi v8- → v9- (yangi keshlar), README, AGENTS.md va TEXNIK-QARZ — v9. Diff: 18 fayl, +500 −16.

bash
git add -A
git commit

Commit (kanon): feat: profil rasmi — tekshiruv, preview, progress, bekor qilish, IndexedDB.

7.7 19-qism yakuni: vazifalar v9

36 darsda vazifalar bitta sahifali ilovadan to'liq SPA'ga aylandi. Unda endi sahifalar va marshrutlar (React Router 8.4.0), server state (TanStack Query 5.104.1) va client state (Zustand 5.0.15) bor. Formalar — React Hook Form 7.89.0 + zod/mini 4.6.5, profil rasmi — idb-keyval 6.3.0. Yakuniy o'lchovlar (kanon, holat 36):

v8 (18-qism) v9 (19-qism)
birinchi yuklash, gzip 84.6 KB ~153.7 KB
testlar (node:test / Vitest) 173 / 49 198 / 67
brauzer tekshiruvlari 159 + 72 159 + 72 + 99
Lighthouse (mobil, Performance) 96 93–96
19-qismda birinchi yuklash qanchaga o'sdi (gzip)
  • React Router 8.4.029,4 KB
  • RHF + resolvers + zod tekshiruvlari14,7 KB
  • TanStack Query 5.104.19,9 KB
  • zod/mini (API sxemasi)5,8 KB
  • Zustand 5.0.151,3 KB

Manba: Kanon o'lchovi: sinov/19/QADAMLAR.md «Yakuniy holat», natijalar/tekshiruv-*.txt (Vite 8.3.3, gzip), 2026-10-13/14

Halol xulosa: hajm deyarli ikki baravar o'sdi, Lighthouse Performance 96 dan 93–96 ga, birinchi chizish (FCP) 1.5 dan 2.0 s ga tushdi. Har kutubxona o'lchab kiritildi va narxi darsda aytildi. Ikki tejash yo'li ham o'lchandi: TanStack Router (−4 KB, TanStack Router) va Valibot (−4 KB, Muqobillar). Forma sahifalarini alohida yuklash (route.lazy) ham sinaldi: tejash atigi ~1.7 KB, chunki ro'yxat sahifasi ham zod ga tayanadi (natijalar/36-forma-lazy.txt). Bu ish — TEXNIK-QARZ 46–47, keyingi qismlarda.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
accept bor, lekin PDF ham o'tdi accept — faqat fayl oynasi uchun maslahat tur va hajmni kodda tekshiring (va serverda)
xotira o'sib boradi revokeObjectURL chaqirilmagan almashtirganda va unmount'da bo'shating
preview har renderda "miltillaydi" createObjectURL render ichida URL'ni hodisada yarating
preview ko'rinmaydi, konsolda CSP xatosi img-src da blob: yo'q img-src 'self' blob:
fayl tashlanganda brauzer uni ochib yubordi dragover/drop da preventDefault yo'q ikkalasida ham preventDefault()
o'sha faylni qayta tanlasa hech narsa bo'lmaydi value o'zgarmagani uchun change yo'q tanlovdan keyin event.target.value = ""
progress ko'rinmaydi fetch bilan yuborilmoqda XMLHttpRequest (xhr.upload) yoki axios
"Bekor qilindi" o'rniga "Xato" AbortError alohida ushlanmagan error.name === "AbortError"
blob: URL serverda ochilmaydi URL faqat shu sahifada yashaydi faylning o'zini yuboring

9. Mashqlar

Mashqlar kurs/mashqlar/19/36-fayl-yuklash/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): to'g'ri yoki noto'g'ri?

  • accept="image/*" bo'lsa, PDF yuklab bo'lmaydi.
  • file.type — faylning ichidagi baytlardan aniqlanadi.
  • createObjectURL bilan yaratilgan URL'ni bo'shatish kerak.
  • fetch yuborish progressini (upload) hodisa bilan beradi.
  • Brauzerdagi hajm tekshiruvi serverdagi tekshiruvni almashtiradi.
Yechim
  • Noto'g'ri — accept faqat fayl oynasini filtrlaydi; "Barcha fayllar" yoki sudrab tashlash uni chetlab o'tadi.
  • Noto'g'ri — brauzer turni kengaytmadan taxmin qiladi.
  • To'g'ri — revokeObjectURL, aks holda Blob xotirada qoladi.
  • Noto'g'ri — yuborish progressi uchun XMLHttpRequest ning upload hodisasi.
  • Noto'g'ri — brauzer tekshiruvi — UX, himoya — server.

2-mashq (o'rta): bir nechta fayl — xabarlar bilan

Menyu suratlari uchun forma: multiple, ko'pi bilan 3 ta fayl, har biri ≤ 500 KB, faqat JPEG va WebP. Natija ikki ro'yxat: "Qabul qilindi: …" va "Rad etildi: …" (sababi bilan). 4-fayl tanlansa — "Ko'pi bilan 3 ta fayl" va ortiqchasi rad etiladi.

Sinov uchun tayyor fayllar va tugma bor — siz check va pick ni yozasiz:

jsx
import { useState } from "react";

const SAMPLE = [
  ["osh.jpg", "image/jpeg", 200_000],
  ["manti.webp", "image/webp", 120_000],
  ["chek.pdf", "application/pdf", 30_000],
  ["lagmon.jpg", "image/jpeg", 900_000],
  ["somsa.jpg", "image/jpeg", 100_000],
];

export default function App() {
  const [result, setResult] = useState({ ok: [], bad: [] });
  function pick(files) {
    // shu yerga
    setResult({ ok: files.map((f) => f.name), bad: [] });
  }
  const sample = () =>
    pick(
      SAMPLE.map(
        ([n, t, s]) => new File(["x".repeat(s)], n, { type: t }),
      ),
    );
  return (
    <div>
      <button type="button" onClick={sample}>
        Sinov to'plami
      </button>
      <p>Qabul qilindi: {result.ok.join(", ")}</p>
      <p>Rad etildi: {result.bad.join("; ")}</p>
    </div>
  );
}

Ekranda:

text
Sinov to'plami
Qabul qilindi: osh.jpg, manti.webp, chek.pdf, lagmon.jpg, somsa.jpg
Rad etildi:
Yechim
jsx
import { useState } from "react";

const SAMPLE = [
  ["osh.jpg", "image/jpeg", 200_000],
  ["manti.webp", "image/webp", 120_000],
  ["chek.pdf", "application/pdf", 30_000],
  ["lagmon.jpg", "image/jpeg", 900_000],
  ["somsa.jpg", "image/jpeg", 100_000],
];
const TYPES = ["image/jpeg", "image/webp"];
const MAX_SIZE = 500_000;
const MAX_COUNT = 3;

function check(file) {
  if (!TYPES.includes(file.type)) return "JPEG yoki WebP emas";
  if (file.size > MAX_SIZE) return "500 KB dan katta";
  return null;
}

export default function App() {
  const [result, setResult] = useState({ ok: [], bad: [] });
  function pick(files) {
    const ok = [];
    const bad = [];
    for (const file of files) {
      const error = check(file);
      if (error !== null) bad.push(`${file.name} (${error})`);
      else if (ok.length >= MAX_COUNT) {
        bad.push(`${file.name} (ko'pi bilan ${MAX_COUNT} ta fayl)`);
      } else ok.push(file.name);
    }
    setResult({ ok, bad });
  }
  const sample = () =>
    pick(
      SAMPLE.map(
        ([n, t, s]) => new File(["x".repeat(s)], n, { type: t }),
      ),
    );
  return (
    <div>
      <button type="button" onClick={sample}>
        Sinov to'plami
      </button>
      <p>Qabul qilindi: {result.ok.join(", ")}</p>
      <p>Rad etildi: {result.bad.join("; ")}</p>
    </div>
  );
}

Ekranda:

text
Sinov to'plami
Qabul qilindi: osh.jpg, manti.webp, somsa.jpg
Rad etildi: chek.pdf (JPEG yoki WebP emas); lagmon.jpg (500 KB dan katta)

Beshta fayldan uchtasi qabul qilindi — chegara aynan to'ldi, shuning uchun "ko'pi bilan 3 ta" xabari bu to'plamda chiqmadi. Avval tur va hajm, keyin son: noto'g'ri fayl "joy egallamasin". To'plamga yana bitta to'g'ri JPEG qo'shib ko'ring — u "ko'pi bilan 3 ta fayl" bilan rad etiladi.

3-mashq (qiyin): yuklash navbati

Bir nechta faylni ketma-ket yuklang (soxta fakeUpload — «Progress UI va bekor qilish» bo'limidagidek):

  1. Har fayl uchun qator: nomi va holati — "kutmoqda", "N%", "yuklandi", "bekor qilindi".
  2. Fayllar navbat bilan, bittadan yuklanadi (parallel emas).
  3. «Hammasini bekor qilish» — joriy yuklash to'xtaydi, qolganlari "bekor qilindi".

Ishora: bitta AbortController butun navbat uchun; for…of ichida await — har fayl oldingisi tugagach boshlanadi. Holat — { [nom]: matn } obyekti.

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

function fakeUpload(file, { onProgress, signal }) {
  return new Promise((resolve, reject) => {
    if (signal.aborted) {
      reject(new DOMException("Bekor qilindi", "AbortError"));
      return;
    }
    let sent = 0;
    const timer = setInterval(() => {
      sent = Math.min(file.size, sent + file.size / 5);
      onProgress(sent / file.size);
      if (sent === file.size) {
        clearInterval(timer);
        resolve();
      }
    }, 100);
    signal.addEventListener("abort", () => {
      clearInterval(timer);
      reject(new DOMException("Bekor qilindi", "AbortError"));
    });
  });
}

const NAMES = ["osh.png", "manti.png", "somsa.png"];

export default function App() {
  const [status, setStatus] = useState({});
  const controller = useRef(null);

  async function uploadAll() {
    controller.current = new AbortController();
    const { signal } = controller.current;
    setStatus(Object.fromEntries(NAMES.map((n) => [n, "kutmoqda"])));
    const set = (name, text) =>
      setStatus((s) => ({ ...s, [name]: text }));
    for (const name of NAMES) {
      const file = new File(["x".repeat(10_000)], name, {
        type: "image/png",
      });
      try {
        await fakeUpload(file, {
          signal,
          onProgress: (p) => set(name, `${Math.round(p * 100)}%`),
        });
        set(name, "yuklandi");
      } catch {
        set(name, "bekor qilindi");
      }
    }
  }

  return (
    <div>
      <button type="button" onClick={() => void uploadAll()}>
        Yuklash
      </button>{" "}
      <button
        type="button"
        onClick={() => controller.current?.abort()}
      >
        Hammasini bekor qilish
      </button>
      <ul>
        {Object.entries(status).map(([name, text]) => (
          <li key={name}>
            {name}: {text}
          </li>
        ))}
      </ul>
    </div>
  );
}

Ekranda:

text
Yuklash Hammasini bekor qilish
osh.png: yuklandi
manti.png: bekor qilindi
somsa.png: bekor qilindi

Birinchi fayl ~500 ms da yuklandi, ikkinchisi yarim yo'lda to'xtadi. Uchinchisi umuman boshlanmadi: fakeUpload boshida signal.aborted ni tekshiradi va darhol rad etadi. Bu tekshiruvsiz navbatdagi fayllar bekor qilingandan keyin ham yuklanaverardi — haqiqiy kodda ham shunday: fetch va axios allaqachon bekor qilingan signal bilan darhol AbortError beradi.

4-mashq: Vazifalar qadami

feat/profil-rasmi branch'ida xususiyatlar/profil-rasmi/ (to'rt fayl va index.ts), testi, sozlamalar sahifasidagi bo'lim, menyudagi avatar, CSP (index.html va vercel.json) va SW versiyasi. Commit.

Yechim

Tekshiruv ro'yxati:

  • npm test — node:test 198 («Orqaga» tuzatishigacha — 190), Vitest 67. Vitest "indexedDB is not defined" bilan yiqilsa — get() Promise.resolve().then(...) ichida emas («Saqlash» bo'limi).
  • npm run dev + mashq serveri → Sozlamalar: .txt tanlang — xabar, preview yo'q; 2 MB rasm — "juda katta"; kichik PNG — preview va «Saqlash».
  • DevTools Network → "Slow 4G": «Saqlash» — <progress> harakatlanadi, «Bekor qilish» — "Yuklash bekor qilindi.", preview joyida.
  • Saqlangach — menyuda avatar; sahifani yangilang — rasm qaytadi (IndexedDB). DevTools Console'da CSP xatosi yo'q.
  • git log --oneline -1 — feat: profil rasmi — tekshiruv, preview, progress, bekor qilish, IndexedDB.

10. Real ishda

  • Qayerda: profil va hujjat yuklash (bank, davlat xizmatlari — pasport surati), e'lon saytlari (OLX kabi — 10 tagacha rasm), maktab va kurs platformalari (uy vazifasi fayli), ichki CRM.
  • Arxitektura: kichik fayllar — o'z server orqali (multipart/form-data, Multer); katta fayllar — presigned URL bilan to'g'ridan S3/MinIO'ga; juda katta — bo'lib yuklash (multipart, tus). Rasmlarni serverda kichraytirish — sharp.
  • UX standarti: sudrab tashlash + oddiy tugma, darhol preview, aniq xato ("2.4 MB; ko'pi bilan 1 MB"), progress va bekor qilish, qayta urinish.
  • Intervyu savollari: "upload progress'ni qanday ko'rsatasiz, nega fetch emas?", "createObjectURL va FileReader farqi?", "brauzerdagi fayl tekshiruvi nega yetarli emas?", "presigned URL nima va nega kerak?".

Xulosa

  • Tanlash — <input type="file" accept multiple> (asosiy yo'l), sudrab tashlash — qo'shimcha; accept — maslahat, tekshiruv emas.
  • Tur va hajm tekshiruvi — mehmonga tez javob; himoya — server (sehrli baytlar, hajm, 413, SVG yo'q, o'z fayl nomi).
  • Preview — createObjectURL hodisada, revokeObjectURL almashtirganda va unmount'da; CSP'da img-src blob:.
  • Progress — XMLHttpRequest upload hodisasi (fetch bermaydi), bekor qilish — AbortController → xhr.abort(); katta fayl — presigned URL va bo'lib yuklash.
  • vazifalar v9 tayyor: router, Query, Zustand, RHF + Zod, profil rasmi — har biri o'lchangan narxi bilan (84.6 → ~153.7 KB).

Keyingi dars: UI kutubxonalar xaritasi: styled, headless va copy-paste — 20-qism boshlanadi: tayyor komponent kutubxonalari, ularning turlari va vazifalar uchun tanlov.

Manbalar

  • MDN: <input type="file">, accept, capture — developer.mozilla.org/docs/Web/HTML/Element/input/file
  • MDN: URL.createObjectURL(), URL.revokeObjectURL() — developer.mozilla.org/docs/Web/API/URL/createObjectURL_static
  • MDN: XMLHttpRequest.upload (progress) — developer.mozilla.org/docs/Web/API/XMLHttpRequest/upload
  • OWASP: File Upload Cheat Sheet — cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
  • react-dropzone — react-dropzone.js.org; Uppy — uppy.io; idb-keyval — github.com/jakearchibald/idb-keyval (6.3.0)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React'da fayl yuklash UI: tanlash, sudrab tashlash, preview, progress va bekor qilish — IlmHamroh