IlmHamroh
JavaScript Full-stack/13-qism. Brauzer API'lari, performans va xavfsizlik13/32-dars23 daqiqa
Mundarija (40)

JavaScript'da fayl yuklash (upload): FormData, fetch va XHR progress

Qisqacha: Faylni serverga yuborishning oddiy yo'li — FormData ga qo'shib, fetch bilan POST qilish; Content-Type ni brauzer o'zi (boundary bilan) qo'yadi. fetch esa yuborish progressini bermaydi — foiz kerak bo'lsa, XMLHttpRequest va uning xhr.upload dagi progress hodisasi ishlatiladi. Katta fayllar slice() bilan bo'laklab yuboriladi, real loyihalarda esa fayl ko'pincha server orqali emas, presigned URL bilan to'g'ridan-to'g'ri omborga yuklanadi.

Bu darsda

  • Faylni (va bir nechta faylni) FormData + fetch bilan serverga yubora olasiz.
  • XMLHttpRequest bilan yuklash foizini ko'rsatasiz va yuklashni bekor qilasiz.
  • XHR'ni Promise'ga o'rab, await bilan ishlatiladigan funksiya yozasiz.
  • Bo'laklab (chunked) yuklash va presigned URL g'oyasini tushuntira olasiz.

Oldin bilishingiz kerak: Faylni o'qish va oldindan ko'rish, fetch bilan ma'lumot yuborish, FormData, CORS mijoz tomondan, Bekor qilish: AbortController.

1. Nega bu kerak?

O'tgan darsda «Bahor» bron formasidagi chek rasmini tekshirdik va ko'rsatdik. U hali brauzerda. Endi uni serverga yuborish kerak.

fetch bilan ma'lumot yuborish darsida FormData ni fetch bilan yuborgan edik — faylni ham shunday yuborsa bo'ladi. Lekin Jasur aka bitta shikoyatni aytdi. Mehmon qishloqdan, sekin internet bilan 4 MB lik chek yubordi. Tugmani bosdi — hech narsa o'zgarmadi. 10 soniya, 20 soniya... U "ishlamayapti" deb yana bosdi. Keyin yana. Server uchta bir xil bron oldi.

Muammo — jarayon ko'rinmaydi. Foydalanuvchi "yuklanmoqda, 45%" ni ko'rsa, kutadi. Bo'sh ekranni ko'rsa — yana bosadi yoki sahifani yopadi.

Bu darsda uchta narsa bor. Faylni to'g'ri yuborish, foizni ko'rsatish va katta fayllarni boshqacha yuborish yo'llari. Streams API darsida yuklab olish (download) progressini ko'rgan edik, o'shanda "yuborish (upload) progressi — boshqa mavzu" degan edik. Mana u.

2. FormData + fetch: oddiy yuklash

2.1 Bitta fayl

FormData darsini eslang: u formadagi maydonlarni nom-qiymat juftlari qilib yig'adi va fayllar bilan birga yuborishga tayyor. Fayl qo'shish — append(nom, fayl).

Kurs mashq serveridagi /aks manzili kelgan so'rovni qaytarib aytib beradi: qaysi maydonlar, qaysi fayllar, qancha bayt. Xuddi tog'dagi aks-sado kabi — inglizchada echo. Kodda bu manzilni saqlaydigan o'zgarmasni shuning uchun ECHO_URL deb ataymiz. Node 24 da File, FormData va fetch bor — shu yerning o'zida yuboramiz:

js
const receipt = new File(["chek rasmi o'rniga matn"], "chek.png", {
  type: "image/png",
});

const form = new FormData();
form.append("mehmon", "Malika");
form.append("chek", receipt);

const response = await fetch("https://ilmhamroh.uz/api/mashq/aks", {
  method: "POST",
  body: form,
});
const data = await response.json();

console.log(data.sarlavhalar["content-type"].split(";")[0]);
console.log(data.tana);

Konsolda:

text
multipart/form-data
[
  { nom: 'mehmon', qiymat: 'Malika' },
  { nom: 'chek', fayl: 'chek.png', hajm: 23, tur: 'image/png' }
]

Ikki oxirgi qator:

  • data.sarlavhalar — server ko'rgan so'rov sarlavhalari. ["content-type"] — so'rov turi. Uning to'liq qiymati uzun (multipart/form-data; boundary=…), split(";")[0] esa faqat nuqtali vergulgacha bo'lgan qismini oladi.
  • data.tana — server so'rov tanasida topgan qismlar ro'yxati.

Server ikki qismni ko'rdi: oddiy maydon mehmon va fayl chek — nomi, hajmi va turi bilan. So'rov turi — multipart/form-data, Fayl yuklash maydoni darsidagi enctype ning o'zi. Brauzerda ham kod aynan shunday, faqat File input.files dan keladi.

2.2 Content-Type ni qo'lda yozmang

Sardor "aniq bo'lsin" deb sarlavhani o'zi qo'shdi:

js
const form = new FormData();
form.append("chek", new File(["..."], "chek.png"));

const response = await fetch("https://ilmhamroh.uz/api/mashq/aks", {
  method: "POST",
  body: form,
  headers: { "Content-Type": "multipart/form-data" }, // ❌
});
console.log(response.status);
console.log(await response.text());

Konsolda:

text
400
{"xato":"multipart/form-data buzilgan (boundary yo'q)"}

Server 400 qaytardi — "so'rov buzilgan".

Nega? multipart so'rovning qismlari maxsus ajratgich — boundary bilan bo'linadi (Fayl yuklash maydoni darsida so'rov ichini ko'rgan edik). Brauzer boundary'ni o'zi o'ylab topadi va sarlavhaga yozadi: multipart/form-data; boundary=----WebKitFormBoundary…. Siz sarlavhani qo'lda yozsangiz, u almashtiriladi — boundary'siz. Server qismlar qayerda bo'linishini bilmaydi. Chrome 154 da ham, Node 24 da ham natija bir xil — 400.

Qoida: body — FormData bo'lsa, Content-Type ni yozmang. Brauzer o'zi to'g'ri qo'yadi (fetch bilan ma'lumot yuborish darsidagi "Content-Type qoidalari").

2.3 Bir nechta fayl

<input type="file" multiple> dan bir nechta fayl kelsa, har birini bir xil nom bilan qo'shamiz:

js
const photos = [
  new File(["a"], "osh.jpg", { type: "image/jpeg" }),
  new File(["bb"], "manti.jpg", { type: "image/jpeg" }),
];

const form = new FormData();
for (const photo of photos) {
  form.append("rasmlar", photo);
}

const response = await fetch("https://ilmhamroh.uz/api/mashq/aks", {
  method: "POST",
  body: form,
});
const { tana } = await response.json();
console.log(tana.map((part) => `${part.fayl}: ${part.hajm} bayt`));
// [ 'osh.jpg: 1 bayt', 'manti.jpg: 2 bayt' ]

Server bitta nom ostida ikkita fayl oldi. Backend'da ular ro'yxat bo'lib keladi (Fayl yuklash: multipart va Multer darsida upload.array("rasmlar") bilan). Brauzerda — input.files dan for...of bilan, xuddi shunday.

Ikkinchi yo'l — har faylni alohida so'rov bilan yuborish. Bittasi yiqilsa, faqat o'shani qayta yuborasiz. Har biriga o'z progress chizig'i. Ko'p rasmli galereyalar odatda shunday qiladi.

2.4 Xom tana: faylning o'zi

FormData shart emas. body ga faylning o'zini bersa ham bo'ladi:

js
await fetch(uploadUrl, { method: "PUT", body: file });

Bunda so'rov tanasi — faqat fayl baytlari, multipart o'rami yo'q. Content-Type — file.type dan (bo'sh bo'lsa — sarlavha yo'q). Fayl nomi bormaydi — kerak bo'lsa, URL'da yoki sarlavhada beriladi. Bu usul pastdagi "Presigned URL" bo'limida kerak bo'ladi.

Tekshirib ko'ring: form.append("chek", file) dan keyin fetch ga headers: { "Content-Type": "multipart/form-data; boundary=abc" } berildi. Server faylni o'qiy oladimi?

Javob

Yo'q. Sarlavhada abc deyilgan, lekin brauzer tanani o'zining boundary'si bilan yasaydi — ular mos kelmaydi. Server tanada abc ni topa olmaydi. Boundary'ni faqat brauzer biladi, shuning uchun sarlavhani ham u yozishi kerak.

3. Progress: fetch nima uchun bermaydi

3.1 Ikki yo'nalish

Fayl yuklashda ma'lumot ikki tomonga oqadi:

  1. Yuborish (upload) — fayl brauzerdan serverga. Katta qism, sekin.
  2. Yuklab olish (download) — server javobi brauzerga. Odatda kichik JSON.

Streams API darsida response.body bilan ikkinchisini o'lchadik. Birinchisi uchun fetch da hodisa yo'q. fetch qaytargan Promise faqat javob kelganda bajariladi — oradagi 30 soniyada nima bo'layotganini bilmaysiz.

fetch ga so'rov tanasini oqim (ReadableStream) qilib berish va o'tgan baytlarni o'zingiz sanash yo'li bor (duplex: "half" sozlamasi bilan). Biz Chrome 154 da sinadik: kurs serveriga shunday so'rov ishladi. Lekin bu imkoniyat hozircha asosan Chromium brauzerlarida, HTTP/2 talab qiladi va sanalgan baytlar "tarmoqqa berildi" degani — "serverga yetdi" emas. Shuning uchun amalda hamon boshqa vosita ishlatiladi.

3.2 XMLHttpRequest

XMLHttpRequest (qisqasi XHR) — fetch dan oldingi so'rov yuborish usuli. Axios darsida "uning brauzerdagi asosi — eski XMLHttpRequest, shuning uchun upload progressi bor" degan edik. Nomida "XML" bor, lekin istalgan ma'lumot bilan ishlaydi — bu tarixiy nom.

XHR Promise emas, hodisalar bilan ishlaydi — o'tgan darsdagi FileReader kabi. Va eng muhimi: unda xhr.upload obyekti bor. U yuborish jarayonining hodisalarini beradi.

Asosiy qadamlar:

js
const xhr = new XMLHttpRequest();
xhr.open("POST", "https://ilmhamroh.uz/api/mashq/aks");
xhr.responseType = "json"; // javobni JSON deb o'qi

xhr.upload.addEventListener("progress", (event) => {
  console.log(`${event.loaded} / ${event.total} bayt`);
});
xhr.addEventListener("load", () => {
  console.log(xhr.status, xhr.response);
});
xhr.addEventListener("error", () => console.log("Tarmoq xatosi"));

xhr.send(form);

Qatorma-qator:

  • open(metod, url) — so'rovni tayyorlaydi, lekin yubormaydi.
  • responseType = "json" — javob tayyor bo'lganda xhr.response da obyekt bo'ladi. fetch dagi response.json() ning o'rnida.
  • xhr.upload.addEventListener("progress", …) — yuborish progressi. event.loaded — yuborilgan baytlar, event.total — jami. event.lengthComputable — jami ma'lummi (FormData va File da doim true).
  • load — javob keldi. Diqqat: istalgan status bilan — 200 ham, 413 ham, 500 ham. xhr.status ni o'zingiz tekshirasiz (fetch dagi response.ok kabi).
  • error — faqat tarmoq xatosi: internet yo'q, server topilmadi, CORS rad etdi.
  • send(body) — yuborish. Tinglovchilar shundan oldin ulanadi.

3.3 Hodisalar xaritasi

XHR'da ikki to'plam hodisa bor: xhr.upload dagisi — yuborish uchun, xhr ning o'zidagisi — javob uchun:

sequenceDiagram
  participant B as Brauzer (xhr)
  participant S as Server
  Note over B: xhr.send(form)
  B->>S: baytlar oqimi
  Note over B: upload: loadstart, progress × N
  B->>S: oxirgi bayt
  Note over B: upload: load, loadend
  S-->>B: javob (status + JSON)
  Note over B: xhr: progress, load, loadend

Nimaga qarang: xhr.upload dagi load — "fayl to'liq yuborildi", lekin server hali javob bermagan. Server faylni saqlayotgan bo'lishi mumkin. Haqiqiy natija — xhr ning load hodisasida. xhr ning o'z progress hodisasi esa javobni yuklab olish progressi — uni yuborish bilan adashtirmang.

Tekshirib ko'ring: 2 MB lik faylda xhr.addEventListener("progress", …) yozildi (xhr.upload emas). Konsolda nima ko'rinadi?

Javob

Javobning yuklab olinishi — bir necha yuz bayt, odatda bitta hodisa va u ham fayl to'liq yuborilgandan keyin. Biz Chrome 154 da sinadik: 900 KB fayl yuborilganda xhr ning progress i bir marta keldi — 268 / 268 (server javobining hajmi). Yuborish jarayoni uchun — xhr.upload.

4. «Bahor»: chekni progress bilan yuklash

4.1 Jonli misol

Endi hammasi birga: fayl tanlash, hajm tekshiruvi (mashq serveri formani 1 MB gacha qabul qiladi), progress chizig'i, foiz va bekor qilish:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  .row { display: flex; align-items: center; gap: 0.5rem;
    margin: 0.75rem 0; }
  progress { flex: 1; height: 1rem; }
  button { font: inherit; padding: 0.4rem 0.8rem; }
</style>
<form id="form">
  <label for="receipt">To'lov cheki (1 MB gacha):</label>
  <input type="file" id="receipt" required>
  <button id="send">Yuborish</button>
  <button id="cancel" type="button" hidden>Bekor qilish</button>
</form>
<div class="row">
  <label for="bar">Yuklanmoqda:</label>
  <progress id="bar" max="100" value="0"></progress>
  <output id="percent">0%</output>
</div>
<p id="status" aria-live="polite"></p>
<script>
  const MAX_SIZE = 1_000_000;
  const form = document.getElementById("form");
  const input = document.getElementById("receipt");
  const send = document.getElementById("send");
  const cancel = document.getElementById("cancel");
  const bar = document.getElementById("bar");
  const percent = document.getElementById("percent");
  const status = document.getElementById("status");

  function setBusy(busy) {
    send.disabled = busy;
    cancel.hidden = !busy;
  }

  form.addEventListener("submit", (event) => {
    event.preventDefault();
    const [file] = input.files;
    if (file.size > MAX_SIZE) {
      status.textContent = "Fayl 1 MB dan katta — boshqasini tanlang";
      return;
    }
    const body = new FormData();
    body.append("chek", file);

    const xhr = new XMLHttpRequest();
    xhr.open("POST", "https://ilmhamroh.uz/api/mashq/aks");
    xhr.responseType = "json";

    let lastLogged = 0;
    xhr.upload.addEventListener("progress", (event) => {
      if (!event.lengthComputable) return;
      const value = Math.round((event.loaded / event.total) * 100);
      bar.value = value;
      percent.textContent = `${value}%`;
      const step = Math.floor(value / 25) * 25; // 0, 25, 50, 75, 100
      if (step > lastLogged) {
        lastLogged = step;
        console.log(`Yuborildi: ${step}%`);
      }
    });
    xhr.addEventListener("load", () => {
      setBusy(false);
      if (xhr.status !== 200) {
        status.textContent = `Server rad etdi: ${xhr.response.xato}`;
        return;
      }
      const [part] = xhr.response.tana;
      status.textContent = `Chek qabul qilindi: ${part.hajm} bayt.`;
      console.log(status.textContent);
    });
    xhr.addEventListener("error", () => {
      setBusy(false);
      status.textContent = "Aloqa uzildi. Qayta urinib ko'ring.";
    });
    xhr.addEventListener("abort", () => {
      setBusy(false);
      status.textContent = "Yuklash bekor qilindi.";
      console.log(status.textContent);
    });
    cancel.onclick = () => xhr.abort();

    setBusy(true);
    status.textContent = "Yuklanmoqda…";
    xhr.send(body);
  });
</script>

Kompyuteringizda internet tez bo'lsa, chiziq bir zumda to'ladi. Sekin internetni taqlid qiling: DevTools → Network → "No throttling" o'rniga "3G". Natija oynasidagi so'rovlar ham shu sozlamaga bo'ysunadi. Endi 900 KB lik fayl bir necha soniya yuklanadi va siz har foizni ko'rasiz.

Biz Chrome 154 da, tarmoqni soniyasiga 50 KB ga sekinlatib, 900 000 baytli fayl yubordik. Konsolda:

text
Yuborildi: 25%
Yuborildi: 50%
Yuborildi: 75%
Yuborildi: 100%
Chek qabul qilindi: 900000 bayt.

Ikkinchi sinovda yuklash o'rtasida «Bekor qilish» bosildi — konsolda Yuklash bekor qilindi., server javob bermadi. 1,2 MB lik fayl esa yuborilmadi: Fayl 1 MB dan katta — boshqasini tanlang

4.2 Kod qanday ishlaydi

  • Ikki marta bosishdan himoya. setBusy(true) «Yuborish» ni o'chiradi. Jasur akaning muammosi — uchta bir xil bron — shu bilan yo'qoladi.
  • <progress> — HTML'ning tayyor progress elementi. Ekran o'quvchi uni "progress bar, 45 foiz" deb o'qiydi. max="100" va value — foiz.
  • aria-live="polite" — holat matni ("Yuklanmoqda…", "qabul qilindi") Malikaga ovoz bilan aytiladi. Har foizni aytish shart emas — bu bezovta qiladi. Shuning uchun <output> da aria-live yo'q.
  • Konsolga faqat 25% qadamlar. progress hodisasi ko'p keladi: spetsifikatsiya uni har ~50 ms da yuborishga ruxsat beradi. Sinovda (50 KB/s) 900 KB fayl uchun 50 dan ortiq hodisa keldi. Har birini yozsak, konsol to'lib ketadi.
  • abort() — so'rovni to'xtatadi. Server yarim faylni oladi va tashlab yuboradi. abort hodisasi keladi, load esa — yo'q.

4.3 Server 413 qaytarsa

Hajm tekshiruvi brauzerda bo'lmasa nima bo'ladi? Biz sinab ko'rdik: tekshiruvni olib tashlab, 1,2 MB lik faylni yubordik. Progress 100% gacha bordi — butun fayl yuborildi. Keyin server javob berdi: status 413, tanasi {"xato":"Forma 1 MB dan katta","chegara":1048576}. Ya'ni foydalanuvchi butun faylni sekin internetda yubordi — behuda.

Xulosa: hajmni avval brauzerda tekshiring (o'tgan darsdagi tartib). Server tekshiruvi baribir qoladi — bu himoya. Brauzerdagisi — foydalanuvchining vaqti va trafigini asrash.

413 — error emas, load. Shuning uchun kodda xhr.status !== 200 tekshiruvi bor.

4.4 CORS: nega so'rov ikkita

DevTools Network panelida qarasangiz, har yuklashda ikkita so'rov ko'rinadi: avval OPTIONS, keyin POST. Birinchisi — CORS mijoz tomondan darsidagi preflight (uchishdan oldingi tekshiruv).

FormData ni POST qilish odatda "oddiy so'rov" — preflight'siz. Lekin spetsifikatsiyada bitta qoida bor: xhr.upload ga tinglovchi ulangan bo'lsa, so'rov preflight bilan yuboriladi. Biz Chrome 154 da tekshirdik: tinglovchisiz XHR — bitta POST; upload.onprogress bilan — OPTIONS + POST. Mashq serveri OPTIONS ga ruxsat beradi (CORS *), shuning uchun hammasi ishlaydi. O'z serveringizda CORS sozlanmagan bo'lsa — progress qo'shgan zahoti yuklash "buziladi".

5. XHR'ni Promise'ga o'rash

5.1 uploadWithProgress

Hodisali kodni har joyda yozish noqulay. Uni bir marta Promise'ga o'raymiz (Promise asoslari darsidagi "callback'ni Promise'ga aylantirish" naqshi):

js
function uploadWithProgress(url, body, { onProgress, signal } = {}) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("POST", url);
    xhr.responseType = "json";

    xhr.upload.addEventListener("progress", (event) => {
      if (event.lengthComputable) {
        onProgress?.(event.loaded / event.total);
      }
    });
    xhr.addEventListener("load", () => {
      resolve({ status: xhr.status, data: xhr.response });
    });
    xhr.addEventListener("error", () => {
      reject(new TypeError("Tarmoq xatosi"));
    });
    xhr.addEventListener("abort", () => {
      reject(new DOMException("Yuklash bekor qilindi", "AbortError"));
    });
    signal?.addEventListener("abort", () => xhr.abort(), {
      once: true,
    });

    xhr.send(body);
  });
}

Endi ishlatish fetch ga o'xshaydi:

js
const controller = new AbortController();
cancelButton.onclick = () => controller.abort();

try {
  const { status, data } = await uploadWithProgress(url, form, {
    onProgress: (ratio) => { bar.value = ratio * 100; },
    signal: controller.signal,
  });
  if (status !== 200) throw new Error(data.xato);
} catch (error) {
  if (error.name === "AbortError") return; // foydalanuvchi to'xtatdi
  showError(error.message);
}

Uch yaxshi tomoni:

  • signal — AbortController darsidagi standart bekor qilish usuli. fetch bilan bir xil interfeys: bitta tugma ham fetch ni, ham yuklashni to'xtata oladi.
  • AbortError — bekor qilinganini boshqa xatolardan ajratish mumkin (fetch ham aynan shu nomni beradi).
  • onProgress?.(…) — ?. (optional chaining): progress kerak bo'lmasa, funksiya berilmaydi va xato chiqmaydi.

Axios'ning onUploadProgress sozlamasi ichkarida xuddi shunday qurilgan.

6. Katta fayllar: bo'laklab yuklash

6.1 Muammo

Jasur aka oshxona tanishtiruv videosini — 800 MB — yuklamoqchi. Bitta so'rov bilan:

  • 20 daqiqa davomida internet bir soniya uzilsa — hammasi boshidan.
  • Ko'p serverlar bitta so'rovga chegara qo'yadi: Nginx sukut bo'yicha 1 MB, kurs mashq serveri — forma uchun 1 MB.
  • Server butun faylni xotirada yoki vaqtinchalik diskda ushlab turadi.

6.2 G'oya: slice() + ko'p so'rov

Bo'laklab yuklash (chunked upload) — faylni teng bo'laklarga kesib, har birini alohida so'rov bilan yuborish. File va Blob darsidagi slice() aynan shu uchun: u nusxasiz, bir zumda kesadi.

Mashq serveri xom tanani (FormData siz) 10 KB gacha qabul qiladi. Shuning uchun bo'lak — 8 000 bayt. Har bo'lak bilan URL'da uning raqami va jami soni ketadi:

js
const CHUNK_SIZE = 8_000;
const rows = "Bron;19:00;4\n".repeat(1_500);
const report = new File([rows], "bronlar.csv");
const total = Math.ceil(report.size / CHUNK_SIZE);
console.log(`${report.size} bayt → ${total} bo'lak`);

for (let index = 0; index < total; index++) {
  const start = index * CHUNK_SIZE;
  const chunk = report.slice(start, start + CHUNK_SIZE);
  const params = new URLSearchParams({
    fayl: report.name,
    bolak: index + 1,
    jami: total,
  });
  const response = await fetch(
    `https://ilmhamroh.uz/api/mashq/aks?${params}`,
    { method: "PUT", body: chunk },
  );
  const { sorov, tana } = await response.json();
  console.log(`${sorov.bolak}/${sorov.jami}: ${tana.length} bayt`);
}

Konsolda:

text
19500 bayt → 3 bo'lak
1/3: 8000 bayt
2/3: 8000 bayt
3/3: 3500 bayt

Oxirgi bo'lak kichikroq — slice fayl oxiridan oshib ketmaydi. Math.ceil — kasrni yuqoriga yaxlitlaydi: 19 500 ÷ 8 000 = 2,4, lekin 2 bo'lakka sig'maydi, shuning uchun 3.

Bo'sh joyni to'ldiring: 20 000 baytli fayl 8 000 baytli bo'laklarga bo'linsa, bo'laklar soni URLSearchParams — URL va URLSearchParams darsidagi usul: qiymatlarni URL uchun to'g'ri kodlaydi.

Haqiqiy server bo'laklarni yig'adi va oxirgisi kelganda faylni birlashtiradi. Bizning /aks shunchaki "ko'rdim" deydi — uning vazifasi so'rovni ko'rsatish.

6.3 Nima yutamiz

  • Davom ettirish (resume). Internet 37-bo'lakda uzildi — 1–36 serverda bor. Server "qaysi bo'laklar keldi?" degan savolga javob beradi va yuklash 37-dan davom etadi. Bunday protokollardan biri — tus (ochiq standart).
  • Qayta urinish. Yiqilgan bo'lakni qayta urinish naqshi bilan 2–3 marta yuborish — butun faylni emas.
  • Progress bepul. fetch bilan ham: "37 / 100 bo'lak" — tayyor foiz. XHR shart emas.
  • Parallel. 3–4 bo'lakni bir vaqtda yuborish tezlashtiradi (parallellik cheklovi).

Server tomoni (bo'laklarni yig'ish, oqim bilan diskka yozish) — Katta fayllarni stream bilan yuklash darsida, backend qismida.

7. Presigned URL: server orqali emas, to'g'ri omborga

7.1 Muammo

Real loyihalarda fayllar API serverining diskida saqlanmaydi. Ular obyekt omboriga (object storage) boradi: Amazon S3, Cloudflare R2 yoki o'z serverdagi MinIO. Bu — fayllar uchun maxsus, cheksiz kengayadigan "disk".

Oddiy yo'l: brauzer → API server → ombor. 800 MB video API server orqali ikki marta o'tadi: kirishda va chiqishda. Server band, trafik ikki baravar.

7.2 Yechim

Presigned URL (oldindan imzolangan manzil) — server omborga vaqtinchalik ruxsat beradi: "shu manzilga, 5 daqiqa ichida, faqat shu turdagi, 10 MB gacha fayl qo'yish mumkin". Manzilda imzo bor — server faqat o'zi biladigan sir kalit bilan hisoblagan maxsus satr. Manzildagi bitta harfni (masalan, muddat yoki fayl nomini) o'zgartirsangiz, imzo mos kelmay qoladi va ombor so'rovni rad etadi. Xuddi muhrlangan ruxsatnoma kabi: muhrsiz yoki tuzatilgan qog'oz o'tmaydi. Brauzer faylni shu manzilga to'g'ridan-to'g'ri yuboradi:

sequenceDiagram
  participant B as Brauzer
  participant A as API server
  participant O as Ombor (S3, R2)
  B->>A: chek.png, 2 MB, image/png yuklamoqchiman
  A->>A: login, hajm va turni tekshiradi
  A-->>B: imzolangan URL (5 daqiqa)
  B->>O: PUT fayl (XHR progress bilan)
  O-->>B: 200
  B->>A: yuklandi, kalit: cheklar/7f3a.png
  A->>O: fayl bormi, hajmi qancha?
  A-->>B: bron saqlandi

Brauzer tomonida bu uch qadam:

js
// 1) serverdan ruxsat so'rash
const { uploadUrl, key } = await apiPost("/api/yuklash", {
  name: file.name,
  size: file.size,
  type: file.type,
});
// uploadUrl: https://ombor.example.com/cheklar/7f3a.png
//   ?X-Amz-Expires=300&X-Amz-Signature=...

// 2) faylni to'g'ri omborga — xom tana, FormData'siz
await fetch(uploadUrl, {
  method: "PUT",
  body: file,
  headers: { "Content-Type": file.type },
});

// 3) serverga xabar berish
await apiPost("/api/bronlar", { chek: key });

apiPost — API klient qatlami darsidagi kabi o'ram. Ikkinchi qadamda fetch o'rniga uploadWithProgress (PUT bilan) ishlatsangiz — progress ham bo'ladi. Content-Type — imzolashda ko'rsatilgan tur bilan bir xil bo'lishi shart, aks holda ombor 403 qaytaradi.

Uchinchi qadam muhim: server brauzerning "yuklandi" degan gapiga ishonmaydi, omborda faylni o'zi tekshiradi. Imzolash va omborni sozlash — Presigned URL'lar darsida.

Tekshirib ko'ring: Mehmon imzolangan URL'ni oldi, lekin faylni 10 daqiqadan keyin yubordi (muddat — 5 daqiqa). Nima bo'ladi?

Javob

Ombor so'rovni rad etadi (403). Muddat imzoning ichida, uni uzaytirib bo'lmaydi — bitta raqam o'zgarsa, imzo buziladi. Brauzer serverdan yangi imzolangan URL so'raydi. Qisqa muddat — himoya: URL tasodifan tarqalib ketsa ham, tez orada yaroqsiz bo'ladi.

8. Hujumchi nigohi

Yuklash — saytga eng ko'p hujum qilinadigan joylardan biri. Brauzer tomonida nima hujumga uchraydi:

  • Brauzerdagi tekshiruvni chetlab o'tish. MAX_SIZE va accept ni DevTools'da o'chirish bir soniyalik ish. «Server 413 qaytarsa» bo'limidagi sinovda ko'rdik: server baribir 413 qaytardi. Chegaralar serverda bo'lishi shart.
  • Presigned URL'ni suiiste'mol qilish. Agar server "istalgan hajm, istalgan tur, 7 kun" deb imzolasa, hujumchi ombornizga 5 GB fayl yoki HTML sahifa qo'yadi. Himoya: qisqa muddat (daqiqalar), hajm va tur shartlari imzoning ichida, har yuklash uchun yangi tasodifiy kalit.
  • Sir kalitlar brauzerda. "Omborga to'g'ri yuklaymiz" deb S3 kalitlarini JavaScript'ga yozish — eng og'ir xato. Kodni har kim o'qiydi. Brauzer faqat imzolangan, vaqtinchalik URL'ni ko'radi; kalit serverda qoladi.
  • Bir xil so'rovni takrorlash. Ikki marta bosish — ikki bron. Tugmani o'chirish yordam beradi, lekin server ham takrorni aniqlashi kerak (backend qismida).

Fayl tarkibi xavfi (soxta tur, SVG ichida skript) — o'tgan darsda va Fayl yuklash xavfsizligi darsida.

9. Ko'p uchraydigan xatolar

9.1 Content-Type ni qo'lda yozish

FormData bilan — 400, "boundary yo'q". Tuzatish: sarlavhani yozmang.

9.2 xhr.progress va xhr.upload.progress ni adashtirish

xhr dagisi — javob yuklab olinishi. Tuzatish: yuborish uchun xhr.upload.addEventListener("progress", …).

9.3 Tinglovchini send() dan keyin ulash

js
xhr.open("POST", url);
xhr.send(form);
xhr.upload.addEventListener("progress", showProgress); // ❌ kech

Xato chiqmaydi, lekin progress hodisalari kelmaydi. Sinovda bunday XHR 900 KB faylni yubordi, progress esa 0 marta keldi. Brauzer send() paytida "upload'ni kimdir tinglayaptimi?" deb qaraydi va shunga qarab hodisalarni yoqadi. Tuzatish: hamma tinglovchilar — send() dan oldin.

9.4 load ni muvaffaqiyat deb hisoblash

load 413 va 500 da ham keladi. Tuzatish: xhr.status ni tekshiring.

9.5 Tugmani yoqiq qoldirish

Sekin internetda foydalanuvchi yana bosadi — takroriy yuklash. Tuzatish: yuklash paytida tugmani o'chiring, «Bekor qilish» ni ko'rsating.

10. Mashqlar

1-mashq (oson): Ikki maydonli forma

Node'da (shu yerda) FormData ga uchta qism qo'shing: ism = "Dilshod aka", stol = "7" va menyu.txt fayli (tarkibi "Osh, choy"). /aks ga POST qiling va server ko'rgan qismlar nomini chiqaring.

Yechim
js
const form = new FormData();
form.append("ism", "Dilshod aka");
form.append("stol", "7");
form.append("menyu", new File(["Osh, choy"], "menyu.txt"));

const response = await fetch("https://ilmhamroh.uz/api/mashq/aks", {
  method: "POST",
  body: form,
});
const { tana } = await response.json();
console.log(tana.map((part) => part.nom));
// [ 'ism', 'stol', 'menyu' ]
console.log(tana[2].fayl, tana[2].hajm); // menyu.txt 9

stol ga son emas, satr berildi — FormData faqat satr va fayl saqlaydi (son bersangiz ham satrga aylanadi).

2-mashq (o'rta): Har fayl — alohida progress

Mehmon bir nechta rasm tanlaydi. Har biri alohida XHR bilan yuborilsin va har biriga o'z <progress> i bo'lsin (nomi bilan). Hammasi tugagach konsolga Yuklandi: N ta fayl. Faqat 1 MB gacha fayllar yuborilsin.

Ishora: «Promise'ga o'rash» bo'limidagi uploadWithProgress ni ko'chiring; Promise.all (Promise kombinatorlari) bilan hammasini kuting.

Yechim
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  li { margin: 0.4rem 0; }
  progress { width: 100%; }
</style>
<label for="photos">Rasmlar (har biri 1 MB gacha):</label>
<input type="file" id="photos" multiple>
<ul id="list"></ul>
<script>
  const ECHO_URL = "https://ilmhamroh.uz/api/mashq/aks";
  const photos = document.getElementById("photos");
  const list = document.getElementById("list");

  function uploadWithProgress(url, body, { onProgress } = {}) {
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open("POST", url);
      xhr.responseType = "json";
      xhr.upload.addEventListener("progress", (event) => {
        if (event.lengthComputable) {
          onProgress?.(event.loaded / event.total);
        }
      });
      xhr.addEventListener("load", () => {
        resolve({ status: xhr.status, data: xhr.response });
      });
      xhr.addEventListener("error", () => {
        reject(new TypeError("Tarmoq xatosi"));
      });
      xhr.send(body);
    });
  }

  photos.addEventListener("change", async () => {
    list.replaceChildren();
    const files = [...photos.files]
      .filter((file) => file.size <= 1_000_000);
    const uploads = files.map((file) => {
      const item = document.createElement("li");
      const label = document.createElement("span");
      const bar = document.createElement("progress");
      label.textContent = file.name;
      bar.max = 1;
      bar.value = 0;
      bar.setAttribute("aria-label", file.name);
      item.append(label, bar);
      list.append(item);

      const body = new FormData();
      body.append("rasm", file);
      return uploadWithProgress(ECHO_URL, body, {
        onProgress: (ratio) => { bar.value = ratio; },
      });
    });
    const results = await Promise.all(uploads);
    const ok = results.filter((result) => result.status === 200);
    console.log(`Yuklandi: ${ok.length} ta fayl`);
  });
</script>

ECHO_URL — mashq serverining aks (echo) manzili, bir marta yozilib, hamma yuklashda ishlatiladi. bar.max = 1 — shunda ratio (0 dan 1 gacha) to'g'ridan-to'g'ri value bo'ladi. Fayllar parallel ketadi, har biri o'z chizig'ini to'ldiradi. Biz osh.png va manti.jpg bilan sinadik: konsolda Yuklandi: 2 ta fayl. Fayl nomi textContent bilan — File va Blob darsidagi XSS qoidasi.

3-mashq (qiyin): Bo'laklab yuklash progressi bilan

"Bo'laklab yuklash" misolini funksiyaga aylantiring: uploadInChunks(file, onProgress). Har bo'lak yuborilgach onProgress(yuborilgan / jami) chaqirilsin. Biror bo'lak 200 dan boshqa status qaytarsa — u bir marta qayta yuborilsin, yana xato bo'lsa — Error otilsin. Node'da 19 500 baytli fayl bilan sinang va progressni foizda chiqaring.

Yechim
js
const CHUNK_SIZE = 8_000;
const ECHO_URL = "https://ilmhamroh.uz/api/mashq/aks";

async function sendChunk(file, index, total) {
  const start = index * CHUNK_SIZE;
  const params = new URLSearchParams({
    fayl: file.name,
    bolak: index + 1,
    jami: total,
  });
  const response = await fetch(`${ECHO_URL}?${params}`, {
    method: "PUT",
    body: file.slice(start, start + CHUNK_SIZE),
  });
  return response.status;
}

async function uploadInChunks(file, onProgress) {
  const total = Math.ceil(file.size / CHUNK_SIZE);
  for (let index = 0; index < total; index++) {
    let status = await sendChunk(file, index, total);
    if (status !== 200) {
      status = await sendChunk(file, index, total); // bir marta qayta
    }
    if (status !== 200) {
      throw new Error(`${index + 1}-bo'lak yuborilmadi: ${status}`);
    }
    onProgress((index + 1) / total);
  }
}

const rows = "Bron;19:00;4\n".repeat(1_500);
const report = new File([rows], "bronlar.csv");
await uploadInChunks(report, (ratio) => {
  console.log(`${Math.round(ratio * 100)}%`);
});

Konsolda:

text
33%
67%
100%

Bu yerda progress "nechta bo'lak tugadi" bo'yicha — XHR'siz, fetch bilan. Bo'lak ichidagi progress kerak bo'lsa, sendChunk ichida uploadWithProgress (PUT bilan) ishlatiladi. Haqiqiy kutubxonalar (masalan, tus-js-client) yana pauza va keyingi kun davom ettirishni qo'shadi.

4-mashq: Amaliy tajriba — vazifalar eksportini serverga yuborish

vazifalar kanon kodiga tegmasdan tajriba qiling: kurs/mashqlar/13/13-yuklash/index.html. O'tgan darslarda eksport fayli yasaldi. Endi uni "zaxira nusxa" sifatida serverga yuboring: tugma bosilganda kichik JSON ({"versiya":1,"vazifalar":[…]}) Blob qilinadi, FormData ga zaxira.json nomi bilan qo'shiladi va /aks ga XHR bilan, <progress> bilan yuboriladi. Javobdagi hajm ekranga chiqsin. Live Server bilan oching, DevTools'da "3G" ni yoqib progressni ko'ring.

Yechim
html
<button id="backup" type="button">Zaxira nusxani yuborish</button>
<progress id="bar" max="1" value="0"></progress>
<p id="status" aria-live="polite"></p>
<script>
  const tasks = [
    { id: 1, matn: "Non olish", bajarildi: false },
    { id: 2, matn: "Choy damlash", bajarildi: true },
  ];
  const bar = document.getElementById("bar");
  const status = document.getElementById("status");

  document.getElementById("backup").addEventListener("click", () => {
    const json = JSON.stringify({ versiya: 1, vazifalar: tasks });
    const body = new FormData();
    const blob = new Blob([json], { type: "application/json" });
    body.append("zaxira", blob, "zaxira.json");

    const xhr = new XMLHttpRequest();
    xhr.open("POST", "https://ilmhamroh.uz/api/mashq/aks");
    xhr.responseType = "json";
    xhr.upload.addEventListener("progress", (event) => {
      bar.value = event.loaded / event.total;
    });
    xhr.addEventListener("load", () => {
      const [part] = xhr.response.tana;
      status.textContent = `${part.fayl}: ${part.hajm} bayt keldi.`;
    });
    xhr.send(body);
  });
</script>

append ning uchinchi argumenti — fayl nomi. Blob da nom yo'q (File va Blob), shuning uchun nomni shu yerda beramiz; bermasangiz, server blob degan nomni ko'radi.

bash
git add 13/13-yuklash/index.html
git commit -m "13/13: vazifalar zaxirasi XHR progress bilan"

11. Real ishda

  • Har yuklash formasida progress. Google Drive, Telegram, GitHub'ga fayl tashlash — foiz va bekor qilish tugmasi. Ichkarida — XHR yoki bo'laklab fetch.
  • Presigned URL — standart. Avatar, hujjat, video saqlaydigan deyarli har zamonaviy ilova faylni S3/R2'ga to'g'ridan-to'g'ri yuklaydi. Backend qismida buni o'zingiz qurasiz.
  • Kutubxonalar. Uppy, FilePond, tus-js-client — bo'laklab yuklash, davom ettirish, rasmni kichraytirish tayyor. React'da — react-dropzone (React — 17-qismdan, hozir bilish shart emas).
  • Intervyu. "Fetch bilan upload progress olsa bo'ladimi?", "Katta faylni qanday yuklaysiz?", "Presigned URL nima va nega kerak?" — o'rta darajadagi frontend intervyularida tez-tez.

Xulosa

  • Fayl FormData + fetch bilan yuboriladi; Content-Type ni brauzer o'zi qo'yadi — qo'lda yozilsa boundary yo'qoladi.
  • fetch yuborish progressini bermaydi; XMLHttpRequest ning xhr.upload progress hodisasi — beradi.
  • Tinglovchilar send() dan oldin; load istalgan statusda keladi — xhr.status ni tekshiring; abort() — bekor qilish.
  • Upload tinglovchisi so'rovni preflight'li qiladi — server CORS'ni sozlagan bo'lishi kerak.
  • Katta fayl — slice() bilan bo'laklab; real loyihada — presigned URL bilan to'g'ri omborga.
  • Hajm chegarasi brauzerda (qulaylik) va serverda (himoya).

Keyingi dars: Drag and Drop API — faylni sahifaga sudrab tashlash, elementlarni sichqoncha bilan joyidan ko'chirish va buning telefon va klaviatura uchun muqobili.

Manbalar

  • WHATWG: XMLHttpRequest Standard ("upload listener flag", progress hodisalari) — xhr.spec.whatwg.org
  • MDN: XMLHttpRequest.upload, FormData, <progress> — developer.mozilla.org
  • Chrome for Developers: "Streaming requests with the fetch API" — developer.chrome.com
  • tus — ochiq resumable upload protokoli — tus.io
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript'da fayl yuklash (upload): FormData, fetch va XHR progress — IlmHamroh