Mundarija (31)
- Bu darsda
- 1. Nega bu kerak?
- 2. Fayl tanlash
- 2.1 <input type="file">
- 2.2 Tur va hajm tekshiruvi
- 3. Oldindan ko'rish (preview)
- 3.1 createObjectURL va revokeObjectURL
- 3.2 Rasmni yuborishdan oldin kichraytirish
- 4. Sudrab tashlash (drag and drop)
- 5. Yuborish: progress va bekor qilish
- 5.1 Nega fetch emas
- 5.2 Progress UI va bekor qilish (soxta yuklash)
- 5.3 Katta fayllar: presigned URL va bo'lib yuklash
- 6. Hujumchi nigohi
- 7. Vazifalar qadami: profil rasmi (holat 36 = v9)
- 7.1 Nima quramiz
- 7.2 Tekshiruv: fayl-tekshir.ts
- 7.3 Yuborish: yuklash.ts
- 7.4 Saqlash: IndexedDB va useSyncExternalStore
- 7.5 Komponent va CSP
- 7.6 Yakuniy tekshiruvlar va commit
- 7.7 19-qism yakuni: vazifalar v9
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): to'g'ri yoki noto'g'ri?
- 2-mashq (o'rta): bir nechta fayl — xabarlar bilan
- 3-mashq (qiyin): yuklash navbati
- 4-mashq: Vazifalar qadami
- 10. Real ishda
- Xulosa
- Manbalar
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 tugagachrevokeObjectURL. Yuborish jarayoni —fetchyuborish progressini bermaydi, shuning uchunXMLHttpRequestningupload"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
acceptvamultiplebilan yozasiz,Fileobyektidan nom, tur va hajmni o'qiysiz. - Tur va hajmni tekshirib, o'zbekcha xabar berasiz — va nega bu himoya emasligini tushunasiz.
- Rasmni
createObjectURLbilan oldindan ko'rsatasiz va xotiranirevokeObjectURLbilan bo'shatasiz. - Sudrab tashlash zonasini, progress va «Bekor qilish» ni quraysiz; presigned URL va bo'lib yuklash g'oyasini bilasiz.
vazifalarga 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 —
vazifalarqadamida, «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):
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:
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):
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:
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» — yaroqliBirinchi 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.exeniosh.pngdeb qayta nomladi va tanladi.checkFilenima 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):
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):
yangi: true
bo'shatildi
yangi: trueIkkinchi tanlovda birinchisi bo'shatildi. Uch nuqta:
- URL hodisada yaratiladi, render yoki effektda emas. Render paytida
createObjectURLchaqirilsa, 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 valocalStoragega yozmang — saqlash uchun faylning o'zi kerak (vazifalarda — 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:
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):
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:
Rasmni shu yerga tashlang yoki tanlang
Tanlandi: hali yo'qKompyuterda «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):
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:
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:
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:
Yuklash
Bekor qilindiIkki 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.
AbortErroralohida xabar oladi, "Xato" emas. - Yuborish paytida fayl maydoni
disabledbo'lishi kerak — ikki yuborish aralashmasin (vazifalarda 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:
- "Brauzer tekshiruvini chetlab o'taman."
accept,checkFile— hammasi brauzerda. DevTools'da kodni o'zgartirish yokicurl -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. - "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 (vazifalarda PNG, JPEG, WebP); serverda fayllar alohida domenda yokiContent-Disposition: attachmentbilan beriladi,X-Content-Type-Options: nosniff— brauzer turini "taxmin qilmaydi". - "Fayl nomi bilan o'ynayman."
../../etc/passwdyoki<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). - "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.
git switch -c feat/profil-rasmi
npm install --save-exact idb-keyval@6.3.0Fayllar — 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):
// 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):
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
/aksmanzili. U faylni saqlamaydi, faqat nima kelganini qaytaradi (nom, hajm, tur). Ya'ni "soxta" yuklash, lekin tarmoq haqiqiy: progress haqiqiy baytlarni ko'rsatadi. Kanon unicurlbilan tekshirgan: 1 MB —200, 2 MB —413. - Javob ham tekshiriladi —
zod/minisxemasi bilan (API qatlami): server haqiqatan shu hajmdagi faylni oldimi. AbortSignal→xhr.abort()—fetchdagisignalbilan 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):
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):
// 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):
- 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, VitestTests 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:
✅ 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.
git add -A
git commitCommit (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 |
- 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.createObjectURLbilan yaratilgan URL'ni bo'shatish kerak.fetchyuborish progressini (upload) hodisa bilan beradi.- Brauzerdagi hajm tekshiruvi serverdagi tekshiruvni almashtiradi.
Yechim
- Noto'g'ri —
acceptfaqat 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
XMLHttpRequestninguploadhodisasi. - 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:
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:
Sinov to'plami
Qabul qilindi: osh.jpg, manti.webp, chek.pdf, lagmon.jpg, somsa.jpg
Rad etildi:Yechim
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:
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):
- Har fayl uchun qator: nomi va holati — "kutmoqda", "N%", "yuklandi", "bekor qilindi".
- Fayllar navbat bilan, bittadan yuklanadi (parallel emas).
- «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
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:
Yuklash Hammasini bekor qilish
osh.png: yuklandi
manti.png: bekor qilindi
somsa.png: bekor qilindiBirinchi 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:.txttanlang — 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
fetchemas?", "createObjectURLvaFileReaderfarqi?", "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 —
createObjectURLhodisada,revokeObjectURLalmashtirganda va unmount'da; CSP'daimg-src blob:. - Progress —
XMLHttpRequestuploadhodisasi (fetchbermaydi), bekor qilish —AbortController→xhr.abort(); katta fayl — presigned URL va bo'lib yuklash. vazifalarv9 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)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!