IlmHamroh
JavaScript Full-stack/21-qism. React: media, integratsiyalar, PWA17/32-dars19 daqiqa
Mundarija (27)

Fayl yuklab berish React'da: Blob, a[download] va showSaveFilePicker

Qisqacha: Brauzerda yasalgan faylni berishning asosiy yo'li — Blob → URL.createObjectURL → vaqtinchalik <a download> → click() → revokeObjectURL. U hamma brauzerda ishlaydi, fayl odatda "Yuklanmalar" papkasiga tushadi. Chrome va Edge'da yana bir yo'l bor — showSaveFilePicker: foydalanuvchi faylni qayerga va qanday nom bilan saqlashni o'zi tanlaydi. U progressiv qo'shimcha bo'lishi kerak: API bo'lmasa tugma ko'rinmaydi, oyna yopilsa (AbortError) — "bekor qilindi", boshqa xato bo'lsa — oddiy yuklab olishga tushadi. Fayl nomi foydalanuvchi ma'lumotidan yasalsa — uni tozalang.

Bu darsda

  • Yuklab berish retseptini React komponentiga va hook'iga joylaysiz, revoke vaqtini to'g'ri tanlaysiz.
  • Fayl nomini xavfsiz qilasiz: taqiqlangan va ko'rinmas belgilar, yashirin kengaytma hujumi.
  • Serverdan kelgan faylni fetch bilan olib, nomini Content-Disposition dan o'qiysiz.
  • showSaveFilePicker ni progressiv qo'shimcha sifatida yozasiz: AbortError, SecurityError, zaxira yo'l.
  • file-saver va Web Share bilan fayl ulashish qachon kerakligini bilasiz.

Oldin bilishingiz kerak: File va Blob (<a download> asoslari), Clipboard va Web Share API, Excel va CSV, Fayl yuklash UI, useEffect I.

1. Nega bu kerak?

O'tgan darsda havolani QR kod qilib telefonga uzatdik. Endi faylni kompyuterga. vazifalar da allaqachon uchta "fayl berish" bor: JSON eksport (17-qism), statistika hisoboti (18-qism) va CSV (Excel va CSV). Hammasi bitta funksiyadan — faylniYuklabBer. Jasur aka esa so'radi: "Fayl doim Yuklanmalarga tushadi. Men uni to'g'ridan-to'g'ri «Bahor/Hisobotlar» papkasiga, o'z nomim bilan saqlamoqchiman."

Bu darsning ikki qismi bor:

  1. Oddiy yuklab berish — File va Blob darsidagi retsept React'da: qayerga yoziladi, qachon bo'shatiladi, nomi qanday tozalanadi.
  2. Joyini tanlab saqlash — showSaveFilePicker: kuchli, lekin faqat ba'zi brauzerlarda. Uni qanday qilib "bor joyda — bor, yo'q joyda — zarar yo'q" qilish.

Hayotiy o'xshatish: kitob do'koni. Oddiy yuklab olish — kitobni sotuvchi sizning sumkangizga soladi. Joyini tanlab saqlash — siz o'zingiz "shu javonga qo'ying" deysiz.

2. Oddiy yuklab berish React'da

2.1 Retsept va qayerga yoziladi

Retseptni File va Blob darsidan bilasiz. vazifalar kanonida u fayl.ts da yashaydi (kanon kodi):

ts
export function faylniYuklabBer(
  matn: string,
  nomi: string,
  tur = "application/json",
): void {
  const blob = new Blob([matn], { type: tur });
  const url = URL.createObjectURL(blob);
  const havola = document.createElement("a");
  havola.href = url;
  havola.download = nomi;
  havola.click();
  // blob: URL Blob'ni xotirada ushlab turadi — bo'shatamiz. Keyingi
  // vazifada: yuklash shu paytgacha boshlangan bo'ladi
  setTimeout(() => URL.revokeObjectURL(url), 0);
}

React nuqtai nazaridan uchta qoida:

  • Hodisada, renderda emas. Fayl tugma bosilganda yasaladi. Render ichida createObjectURL — har chizishda yangi URL va xotira sizishi (Fayl yuklash UI darsidagi qoida).
  • Oddiy funksiya — komponent emas. faylniYuklabBer React'ni bilmaydi: DOM'ga havola qo'shmaydi, state o'zgartirmaydi. Shuning uchun uni istalgan joydan (menyu bandi, klaviatura buyrug'i, Ctrl+K oynasi) chaqirish mumkin.
  • Natijani e'lon qiling. Yuklab olish ekranda hech narsani o'zgartirmaydi. Malika fayl tushganini bilishi uchun kanon har eksportdan keyin elonQil(\${nomi} yuklab olindi.`)chaqiradi —aria-live` hududiga matn.

2.2 Havola sahifada qolsa

Ba'zan fayl oldindan tayyorlanadi va foydalanuvchiga havola ko'rsatiladi: "Hisobot tayyor — yuklab oling (12 KB)". Bunda URL bir muddat yashaydi va uni komponent yopilganda bo'shatish kerak. Bu — effekt tozalashining klassik ishi:

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

function DownloadLink({ text, name }) {
  const url = useMemo(() => {
    const blob = new Blob([text], { type: "text/plain" });
    return URL.createObjectURL(blob);
  }, [text]);
  useEffect(() => {
    console.log("yaratildi:", url.startsWith("blob:"));
    return () => {
      URL.revokeObjectURL(url);
      console.log("bo'shatildi");
    };
  }, [url]);
  const bytes = new Blob([text]).size;
  return (
    <a href={url} download={name}>
      {name} ({bytes} bayt)
    </a>
  );
}

export default function App() {
  const text = "Osh — 35 000 so'm\n";
  return <DownloadLink text={text} name="narx.txt" />;
}

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

text
yaratildi: true

Ekranda — «narx.txt (20 bayt)»: matnda 18 ta belgi, lekin — UTF-8 da 3 bayt (File va Blob darsidagi size farqi). Havolani bossangiz, narx.txt yuklab olinadi (natija oynasida allow-downloads ruxsati bor). text o'zgarsa — useMemo yangi URL yasaydi, effekt eskisini bo'shatadi. Komponent yopilsa — oxirgisi ham bo'shaydi. Bu blok faqat saytda: tekshiruvchimizda (jsdom) createObjectURL yo'q.

Tekshirib ko'ring: Nega faylniYuklabBer URL'ni darhol emas, setTimeout(…, 0) bilan bo'shatadi?

Javob

click() yuklashni boshlaydi, lekin brauzer manzilni o'qishga ulgurishi kerak. Darhol revokeObjectURL chaqirilsa, ba'zi brauzerlarda yuklash "fayl topilmadi" bilan to'xtaydi. setTimeout(…, 0) bo'shatishni keyingi vazifaga suradi — shu paytgacha yuklash boshlangan bo'ladi (Event loop navbati). Havolali variantda esa URL foydalanuvchi bosguncha kerak — u faqat komponent yopilganda bo'shatiladi.

3. Fayl nomi

3.1 Nom qayerdan keladi

Kanonda fayl nomi sanadan: vazifalar-2026-10-17.json. Lekin ko'pincha nom ma'lumotdan yasaladi: "Bahor: oktabr/hisobot.csv", mijoz ismi, vazifa matni. Fayl tizimlari ba'zi belgilarni taqiqlaydi: Windows'da < > : " / \ | ? *, CON, PRN, NUL kabi maxsus nomlar, oxirida nuqta yoki bo'shliq. Brauzer bunday nomni o'zicha o'zgartiradi — har biri boshqacha. Yaxshisi — o'zingiz tozalang.

3.2 Yashirin kengaytma hujumi

Undan xavfliroq — ko'rinmas belgilar. Unicode'da matn yo'nalishini o'zgartiradigan boshqaruv belgilari bor. Ulardan biri — RLO (U+202E, "o'ngdan chapga"): undan keyingi matn teskari ko'rsatiladi. Hujumchi faylini shunday nomlaydi: chek + RLO + fdp.exe. Ko'p dasturlarda u chekexe.pdf bo'lib ko'rinadi — PDF'ga o'xshaydi, aslida esa dastur (.exe). Fayl nomini foydalanuvchi ma'lumotidan yasasangiz, bunday belgi o'z saytingiz bergan faylga tushadi.

Tozalovchi funksiya:

js
const RESERVED = /^(con|prn|aux|nul|com\d|lpt\d)$/i;

function safeFileName(name, fallback = "fayl") {
  const clean = name
    .normalize("NFC")
    .replace(/[\p{Cc}\p{Bidi_Control}]/gu, "") // ko'rinmas belgilar
    .replace(/[<>:"/\\|?*]/g, "_") // Windows taqiqlagan belgilar
    .replace(/[. ]+$/, "") // oxiridagi nuqta va bo'shliq
    .slice(0, 100);
  const base = clean.split(".")[0];
  return clean === "" || RESERVED.test(base) ? fallback : clean;
}

const rlo = String.fromCodePoint(0x202e);
console.log(safeFileName("Bahor: oktabr/hisobot?.csv"));
console.log(safeFileName(`chek${rlo}fdp.exe`));
console.log(safeFileName("CON.txt"), safeFileName("...  "));

Konsolda:

text
Bahor_ oktabr_hisobot_.csv
chekfdp.exe
fayl fayl
  • \p{Cc} — nazorat belgilari (yangi qator, tab, nol bayt), \p{Bidi_Control} — yo'nalish belgilari, RLO ham shu yerda. u bayrog'i — Unicode xususiyatlari bilan ishlash uchun (Unicode regex: u va v flag).
  • normalize("NFC") — bir xil ko'rinadigan harflar bitta shaklda bo'lsin (masalan, macOS'dan kelgan nom boshqacha kodlangan bo'lishi mumkin).
  • Ikkinchi qator — RLO o'chdi, endi haqiqiy kengaytma ko'rinadi: .exe. Endi foydalanuvchi aldanmaydi.
  • .slice(0, 100) — juda uzun nomni qisqartirish. Kirill va o'zbek harflari tegilmaydi — ular ruxsat etilgan.

3.3 Serverdan kelgan fayl: Content-Disposition

<a download> faqat shu saytdagi, blob: yoki data: manzillar uchun ishlaydi (File va Blob darsidagi xato). Serverdagi faylni yuklab berishning to'g'ri yo'li — server o'zi aytadi:

text
Content-Disposition: attachment; filename="hisobot.csv"; filename*=UTF-8''%D0%BE%D1%82%D1%87%D1%91%D1%82.csv

attachment — "ochma, saqla". filename — oddiy nom (faqat lotin), filename* — UTF-8 dagi nom (kirill, o'zbek harflari). Brauzer avval filename* ni oladi.

Ba'zan faylni fetch bilan olish kerak bo'ladi — masalan, so'rovga Authorization sarlavhasi qo'shish uchun (oddiy havola uni yubora olmaydi). Unda fayl Blob bo'lib keladi, nomini esa sarlavhadan o'zingiz o'qiysiz:

js
const res = await fetch("/api/hisobot.csv", {
  headers: { Authorization: `Bearer ${token}` },
});
const header = res.headers.get("Content-Disposition");
const fromHeader = fileNameFromHeader(header);
const name = safeFileName(fromHeader ?? "hisobot.csv");
faylniYuklabBer(await res.text(), name, "text/csv;charset=utf-8");

fileNameFromHeader — mashqda yozasiz. Ikki eslatma. Boshqa domendagi API bo'lsa, server Access-Control-Expose-Headers: Content-Disposition ham yuborishi kerak — aks holda CORS bu sarlavhani JavaScript'dan yashiradi. Va bu yo'l butun faylni xotiraga yuklaydi: 500 MB lik arxiv uchun yaramaydi (pastda — oqim bilan).

4. showSaveFilePicker: joyini tanlab saqlash

4.1 API qanday ishlaydi

File System Access API — brauzerga foydalanuvchi ruxsati bilan haqiqiy fayllarga yozish imkonini beradi. Uning window.showSaveFilePicker() funksiyasi tizimning "Saqlash" oynasini ochadi:

sequenceDiagram
  participant U as Foydalanuvchi
  participant A as Ilova
  participant B as Brauzer
  U->>A: «Joyini tanlab saqlash…» bosdi
  A->>B: showSaveFilePicker({ suggestedName, types })
  B->>U: Saqlash oynasi
  alt Joy va nom tanlandi
    U->>B: «Bahor/Hisobotlar/oktabr.json»
    B-->>A: FileSystemFileHandle
    A->>B: createWritable → write(blob) → close
    A->>U: «oktabr.json saqlandi.»
  else Oyna yopildi
    B-->>A: AbortError
    A->>U: «Saqlash bekor qilindi.»
  end

Diagrammaga qarang: ilova faqat tutqich (handle) oladi — faylning o'zi emas, unga yozish huquqi. Foydalanuvchi qaysi papkani tanlaganini ilova bilmaydi, faqat nomini (handle.name).

Uch muhim qoida:

  • Qo'llovi. Chrome va Edge'da (86-versiyadan) bor, Firefox va Safari'da yo'q. Bu standart hali Web Incubator (WICG) bosqichida — shuning uchun TypeScript'ning lib.dom ida ham yo'q.
  • Foydalanuvchi harakati. Oyna faqat bosish hodisasidan darhol ochiladi. Oldin await fetch(…) qilib, keyin chaqirsangiz — brauzer "foydalanuvchi so'ramadi" deb rad etadi (SecurityError). Avval oynani oching, keyin ma'lumotni tayyorlang.
  • Iframe. Begona origin'dagi iframe va sandbox hujjatlarida oyna ochilmaydi.

Oxirgi qoidani biz Chrome 155 da tekshirdik — saytdagi natija oynasi aynan shunday sandbox iframe. Undagi chaqiruv shu xatoni berdi:

text
SecurityError | Failed to execute 'showSaveFilePicker' on 'Window': Sandboxed documents aren't allowed to show a file picker.

Tarjimasi: "Sandbox'dagi hujjatlarga fayl tanlash oynasini ko'rsatish ruxsat etilmaydi". Ya'ni bu darsdagi showSaveFilePicker misollari saytda oyna ochmaydi — va bu kanonning zaxira yo'lini jonli ko'rish imkoniyati.

4.2 Uch natija — bitta funksiya

Kanonning joyTanlabSaqla funksiyasi uch holatni ajratadi: saqlandi, bekor qilindi, ishlamadi (→ oddiy yuklab olish). Uni natija oynasida sinash uchun kanonning brauzer testi qilgan ishni qilamiz — soxta picker beramiz. U haqiqiy oynani ochmaydi, lekin uchala javobni aniq qaytaradi:

jsx
import { useState } from "react";

async function saveWithPicker(text, name, picker, fallback) {
  try {
    if (picker === undefined) {
      throw new TypeError("showSaveFilePicker yo'q");
    }
    const handle = await picker({
      suggestedName: name,
      types: [{ accept: { "application/json": [".json"] } }],
    });
    const writable = await handle.createWritable();
    const blob = new Blob([text], { type: "application/json" });
    await writable.write(blob);
    await writable.close();
    return `${handle.name} saqlandi.`;
  } catch (error) {
    const aborted =
      error instanceof DOMException && error.name === "AbortError";
    if (aborted) {
      return "Saqlash bekor qilindi.";
    }
    fallback(text, name); // oddiy yuklab olish
    return `${name} yuklab olindi.`;
  }
}

// Soxta picker'lar — kanon e2e testi ham shunday sinaydi
const PICKERS = {
  tanlandi: async () => ({
    name: "mening-royxatim.json",
    createWritable: async () => ({
      write: async (blob) => {
        console.log("yozildi:", await blob.text());
      },
      close: async () => {},
    }),
  }),
  bekor: async () => {
    throw new DOMException("Foydalanuvchi yopdi", "AbortError");
  },
  "API yo'q": undefined,
};

export default function App() {
  const [message, setMessage] = useState("");
  const text = '{"vazifalar":[]}';
  const fallback = (t, n) => console.log("zaxira yo'l:", n);
  return (
    <>
      {Object.entries(PICKERS).map(([label, picker]) => (
        <button
          key={label}
          onClick={async () => {
            const name = "vazifalar.json";
            const result = await saveWithPicker(
              text,
              name,
              picker,
              fallback,
            );
            setMessage(result);
          }}
        >
          {label}
        </button>
      ))}
      <p role="status">{message}</p>
    </>
  );
}

Konsolda:

text
yozildi: {"vazifalar":[]}
zaxira yo'l: vazifalar.json

Ekranda:

text
tanlandibekorAPI yo'q
vazifalar.json yuklab olindi.

Uch bosish — uch yo'l, ekran() qadamlari oraliq xabarlarni tekshirdi:

Qadam Picker javobi Ekrandagi xabar Konsolda
«tanlandi» tutqich «mening-royxatim.json saqlandi.» yozildi: …
«bekor» AbortError «Saqlash bekor qilindi.» —
«API yo'q» TypeError «vazifalar.json yuklab olindi.» zaxira yo'l: …

Jadval bo'yicha har birini ko'raylik:

  • «tanlandi» — tutqich keldi, createWritable → write → close. Xabar foydalanuvchi tanlagan nom bilan (mening-royxatim.json), biz taklif qilgan nom bilan emas.
  • «bekor» — AbortError: foydalanuvchi o'zi yopdi. Hech narsa yuklanmaydi — u fikridan qaytdi, unga qarshi borib faylni baribir tushirish noto'g'ri.
  • «API yo'q» — TypeError → zaxira yo'l: oddiy yuklab olish. Saytdagi SecurityError ham shu yo'lga tushadi.

error instanceof DOMException && error.name === "AbortError" — xatoni turi bo'yicha ajratish. Xabar matni brauzerga qarab farq qiladi, name esa standart. Ekrandagi tanlandibekorAPI yo'q — tugmalar orasida bo'shliq yo'q; haqiqiy ilovada ular orasida {" "} yoki CSS gap bo'ladi.

4.3 Progressiv: tugma faqat bor joyda

Firefox foydalanuvchisiga ishlamaydigan «Joyini tanlab saqlash…» tugmasini ko'rsatish — chalkashlik. Kanon tugmani faqat API bor bo'lsa chizadi:

tsx
/** "Joyini tanlab saqlash" bormi: Chrome/Edge
 *  (Firefox, Safari — yo'q) */
export const joyTanlashBor = (): boolean =>
  typeof window.showSaveFilePicker === "function";
tsx
      {/* 21/#17: progressiv — API yo'q brauzerda
          tugma umuman yo'q */}
      {onJoyTanlab !== undefined && joyTanlashBor() && (
        <Tugma id="eksport-joy" onClick={onJoyTanlab}>
          Joyini tanlab saqlash…
        </Tugma>
      )}

Bu — progressive enhancement: asosiy yo'l («Faylga yuklab olish») hammada ishlaydi, kuchli brauzerda qo'shimcha imkoniyat. Tugma matnidagi … — interfeys an'anasi: "bosganda yana bir oyna ochiladi".

4.4 Katta fayl: to'g'ri diskka oqim

showSaveFilePicker ning yana bir kuchi — katta faylni xotiraga yuklamasdan saqlash. Serverdan kelgan javob tanasi oqim — uni to'g'ridan-to'g'ri faylga ulash mumkin:

js
const handle = await window.showSaveFilePicker({
  suggestedName: name,
});
const res = await fetch("/api/arxiv.zip");
await res.body.pipeTo(await handle.createWritable());

pipeTo bo'laklarni kelishi bilan diskka yozadi — 2 GB lik arxiv ham brauzer xotirasini to'ldirmaydi. Diqqat: oyna birinchi qatorda — fetch dan oldin (foydalanuvchi harakati qoidasi). <a download> bilan bunday qilib bo'lmaydi: Blob butun fayl bo'lishi kerak.

Tekshirib ko'ring: Kodda tartib shunday: avval const text = await buildReport(); (2 soniya), keyin showSaveFilePicker(…). Chrome'da nima bo'ladi?

Javob

Oyna ochilmaydi — SecurityError. Foydalanuvchi bosishi beradigan "faollashuv" qisqa muddatli: 2 soniyalik await dan keyin u tugagan. Kanon funksiyasida bu xato AbortError emas, shuning uchun oddiy yuklab olishga tushadi — foydalanuvchi faylni baribir oladi, lekin joyini tanlay olmaydi. Tuzatish — avval showSaveFilePicker, keyin hisobotni yasash va yozish.

5. file-saver va Web Share

file-saver — saveAs(blob, "chek.pdf") bitta funksiyasi bilan mashhur kutubxona. U eski brauzerlarning (eski Internet Explorer, eski Safari) download atributi yo'qligini yashirardi. Uning oxirgi versiyasi — 2.0.5, 2020-yil noyabrdagi (npm view file-saver, 2026-10-18). Bugun <a download> hamma asosiy brauzerda bor — yangi loyihaga file-saver shart emas. Eski kodda saveAs ni ko'rsangiz, uni o'nta qatorli faylniYuklabBer bilan almashtirish mumkin. Biz uni o'lchamadik.

Telefonda "yuklab olish" ko'pincha noqulay: fayl qayerdadir yo'qolib qoladi. Mobil brauzerlarda tizimning "Ulashish" oynasi orqali faylni Telegram'ga yoki "Fayllar" ilovasiga yuborish qulayroq (Clipboard va Web Share API):

js
const file = new File([csv], "vazifalar.csv", { type: "text/csv" });
if (navigator.canShare?.({ files: [file] })) {
  await navigator.share({ files: [file], title: "Vazifalar" });
} else {
  faylniYuklabBer(csv, file.name, file.type);
}

canShare({ files }) — "fayl ulasha olamanmi?" degan savol: ulashish bor, lekin fayl ulashib bo'lmaydigan brauzerlar ham bor. Rad etilsa (AbortError — foydalanuvchi yopdi) — xuddi picker kabi, jim qoldiramiz.

6. Vazifalar qadami: «Joyini tanlab saqlash» (holat 17)

6.1 O'zgarishlar

bash
git switch -c feat/joy-tanlab-saqlash
Fayl Nima
src/fayl.ts SaqlashNatijasi turi, joyTanlashBor, joyTanlabSaqla
src/fayl-tizimi.d.ts (yangi) showSaveFilePicker turi (lib.dom'da yo'q)
komponentlar/Eksport.tsx tugma — faqat API bor bo'lsa
App.tsx eksportJoyTanlab() va e'lon
tsconfig.test.json testlar uchun .d.ts fayllar

Turlar fayli — kanon kodi to'liq:

ts
// fayl-tizimi.d.ts — File System Access API'ning bitta
// funksiyasi (21/#17). showSaveFilePicker hali standart emas
// (WICG; Chrome/Edge 86+, Firefox va Safari'da yo'q) —
// TypeScript'ning lib.dom'ida ham yo'q. Faqat bizga kerak qismi;
// FileSystemFileHandle (createWritable) — lib.dom'da bor
declare global {
  type SaqlashTuri = {
    description?: string;
    accept: Record<string, string[]>;
  };

  interface Window {
    showSaveFilePicker?: (sozlama?: {
      suggestedName?: string;
      types?: SaqlashTuri[];
    }) => Promise<FileSystemFileHandle>;
  }
}
export {};

declare global + interface Window — global window turiga xususiyat qo'shish (Kutubxona turlari va module augmentation). ?: — ixtiyoriy: TypeScript har safar window.showSaveFilePicker?.(…) yoki typeof … === "function" tekshiruvini talab qiladi. Bu tur "API har doim bor" deb yolg'on gapirmaydi. export {} — fayl modul bo'lsin, aks holda declare global ishlamaydi.

Asosiy funksiya — kanon kodi:

ts
export async function joyTanlabSaqla(
  matn: string,
  nomi: string,
  tur = "application/json",
): Promise<SaqlashNatijasi> {
  const kengaytma = nomi.slice(nomi.lastIndexOf("."));
  const asosiyTur = tur.split(";")[0] ?? tur;
  try {
    const qayer = await window.showSaveFilePicker?.({
      suggestedName: nomi,
      types: [{ accept: { [asosiyTur]: [kengaytma] } }],
    });
    if (qayer === undefined) {
      throw new TypeError("showSaveFilePicker yo'q");
    }
    const yozuvchi = await qayer.createWritable();
    await yozuvchi.write(new Blob([matn], { type: tur }));
    await yozuvchi.close();
    return { tur: "saqlandi", nomi: qayer.name };
  } catch (xato) {
    if (xato instanceof DOMException && xato.name === "AbortError") {
      return { tur: "bekor" };
    }
    faylniYuklabBer(matn, nomi, tur);
    return { tur: "saqlandi", nomi };
  }
}

Bu — yuqoridagi saveWithPicker ning o'zi, ikki farq bilan. Tur va kengaytma argumentdan olinadi: text/csv;charset=utf-8 → text/csv, chunki accept da parametrli tur bo'lmaydi. Natija — birlashma turi ({ tur: "saqlandi" } | { tur: "bekor" }): App.tsx natija.tur bo'yicha e'lon tanlaydi.

6.2 Natija

e2e-21 — 51 ta. Headless Chrome 155 da typeof showSaveFilePicker — function, lekin test haqiqiy oynani ocholmaydi (headless'da oyna o'zi yopiladi). Shuning uchun kanon testi yuqoridagi kabi soxta picker bilan uch holatni sinadi:

text
   headless Chrome: typeof showSaveFilePicker = function
✅ tanlangan joyga: nom taklifi, tur, yozilgan JSON, e'lon (tanlangan nom): [true,"[{\"accept\":{\"application/json\":[\".json\"]}}]",3,"mening-royxatim.json saqlandi."]
✅ oyna yopildi (AbortError): e'lon, yuklab olinmadi: ["Saqlash bekor qilindi.",0]
✅ API yo'q (Firefox/Safari kabi): tugma yo'q, oddiy yuklab olish joyida: [false,true]
✅ joy tanlash: CSP buzilishi yo'q: []

Ikkinchi qatordagi 0 — bekor qilinganda yuklanmalar papkasi haqiqatan bo'sh qoldi (test brauzerning yuklash papkasini tekshirdi). Haqiqiy oyna bilan qo'lda tekshirish — sizning mashqingiz.

Birinchi yuklash JS — 222.5 → 222.8 KB (+0.3). Testlar soni (node:test 233, Vitest 65) o'zgarmadi. tsconfig.test.json ga turlar fayllari qo'shildi (avval i18n kalit turlari ham u yerda yo'q edi — testlar turlarsiz tekshirilardi; kanon shu yerda tuzatdi).

bash
git add -A
git commit

Commit (kanon): feat: «Joyini tanlab saqlash» — showSaveFilePicker (progressiv).

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
fayl yuklanmaydi, blob: ochildi download atributi yo'q a.download = nomi
boshqa saytdagi fayl ochiladi download faqat o'z origin uchun server Content-Disposition: attachment
kirill nomli fayl ____.csv sarlavhada faqat filename filename*=UTF-8''…
SecurityError — oyna ochilmaydi oldin await, faollashuv tugadi avval picker, keyin ma'lumot
SecurityError iframe'da sandbox yoki begona origin zaxira — oddiy yuklab olish
bekor qilinsa ham fayl tushdi AbortError alohida ushlanmagan error.name === "AbortError"
TS: showSaveFilePicker does not exist lib.dom'da yo'q declare global turi
fayl kengaytmasi «aylanib» ko'rinadi foydalanuvchi matni tozalanmagan safeFileName

8. Mashqlar

Mashqlar kurs/mashqlar/21/17-fayl-yuklab-berish/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

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

  • Yuklab olishni boshlaydigan havola atributi:
  • Foydalanuvchi saqlash oynasini yopsa keladigan xato nomi:
  • Bosishdan keyin uzoq await qilib picker ochilsa keladigan xato:
  • Server "ochma, saqla" deydigan sarlavha qiymati: Content-Disposition:
  • Ko'rinadigan kengaytmani "aylantiradigan" belgi:
Yechim
  • download — qiymati — taklif qilinadigan nom.
  • AbortError — foydalanuvchi qarori, hech narsa yuklanmaydi.
  • SecurityError — foydalanuvchi faollashuvi tugagan; zaxira yo'lga tushadi.
  • attachment — inline esa brauzerda ochadi.
  • RLO (U+202E) — \p{Bidi_Control} bilan tozalanadi.

2-mashq (o'rta): «Saqlanmoqda…» holati

«Uch natija — bitta funksiya» bo'limidagi namunani o'zgartiring: bitta «Saqlash» tugmasi va 300 ms kechikadigan soxta picker. Saqlash davomida tugma matni «Saqlanmoqda…» va disabled bo'lsin — ikki marta bosib, ikki oyna ochib bo'lmasin. Tugagach — «Saqlash» qaytadi va xabar chiqadi.

Ishora: const [busy, setBusy] = useState(false); try { … } finally { setBusy(false); } (try / catch / finally); kechikish — new Promise((r) => setTimeout(r, 300)).

Yechim
jsx
import { useState } from "react";

const wait = (ms) => new Promise((r) => setTimeout(r, ms));

async function slowPicker() {
  await wait(300);
  return {
    name: "oktabr.json",
    createWritable: async () => ({
      write: async () => {},
      close: async () => {},
    }),
  };
}

export default function App() {
  const [busy, setBusy] = useState(false);
  const [message, setMessage] = useState("");

  async function save() {
    setBusy(true);
    try {
      const handle = await slowPicker();
      const writable = await handle.createWritable();
      await writable.write(new Blob(["{}"]));
      await writable.close();
      setMessage(`${handle.name} saqlandi.`);
    } finally {
      setBusy(false);
    }
  }

  return (
    <>
      <button onClick={save} disabled={busy}>
        {busy ? "Saqlanmoqda…" : "Saqlash"}
      </button>
      <p role="status">{message}</p>
    </>
  );
}

Ekranda:

text
Saqlash
oktabr.json saqlandi.

finally — xato bo'lsa ham tugma qaytadi; usiz bitta xato tugmani abadiy "Saqlanmoqda…" holatida qoldirardi. disabled — ikkinchi bosish hodisasi umuman kelmaydi. Haqiqiy showSaveFilePicker ham shu tartibda: oyna ochiq turganda tugma band.

3-mashq (qiyin): Content-Disposition dan nom

fileNameFromHeader(header) funksiyasini yozing:

  1. filename*=UTF-8''… bo'lsa — uni decodeURIComponent bilan ochib qaytarsin (u ustun).
  2. Bo'lmasa — filename="…" yoki filename=… (qo'shtirnoqsiz).
  3. Sarlavha yo'q yoki nom yo'q — null.

Sinov: kirill nomli hisobot (filename* bilan), attachment; filename=chek-12.pdf, inline, null.

Ishora: muntazam ifodada i bayrog'i — katta-kichik harf farqsiz (Flaglar); filename\* — yulduzcha oldida \, chunki u ifodada maxsus belgi.

Yechim
js
function fileNameFromHeader(header) {
  if (!header) return null;
  const star = header.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
  if (star) return decodeURIComponent(star[1].trim());
  const plain = header.match(/filename\s*=\s*"?([^";]+)"?/i);
  return plain ? plain[1].trim() : null;
}

const headers = [
  "attachment; filename=\"hisobot.csv\"; " +
    "filename*=UTF-8''%D0%BE%D1%82%D1%87%D1%91%D1%82.csv",
  "attachment; filename=chek-12.pdf",
  "inline",
  null,
];
for (const h of headers) console.log(fileNameFromHeader(h));

Konsolda:

text
отчёт.csv
chek-12.pdf
null
null

Birinchi sarlavhada ikkala nom bor — filename* ustun, chunki u to'liq UTF-8. filename\s*= ifodasi filename*= ga mos kelmaydi: filename dan keyin * turibdi, \s*= esa faqat bo'shliq va = ni kutadi. Natijani baribir safeFileName dan o'tkazing — sarlavhani server yozadi, lekin u nomni foydalanuvchi ma'lumotidan yasagan bo'lishi mumkin.

4-mashq: Vazifalar qadami

feat/joy-tanlab-saqlash branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling: fayl-tizimi.d.ts, fayl.ts dagi joyTanlashBor va joyTanlabSaqla, Eksport.tsx dagi shartli tugma, App.tsx dagi e'lon, tsconfig.test.json.

Yechim

Tekshiruv ro'yxati:

  • npm run tip — 0 xato; window.showSaveFilePicker ni tekshiruvsiz chaqirsangiz, TypeScript "ehtimol undefined" deydi.
  • Chrome yoki Edge: «Joyini tanlab saqlash…» bor. Bosing → tizim oynasi → papka va nom tanlang → e'lon « saqlandi.» Faylni ochib, JSON'ni tekshiring.
  • Oynani «Bekor qilish» bilan yoping → «Saqlash bekor qilindi.», Yuklanmalar papkasida yangi fayl yo'q.
  • Firefox: tugma umuman yo'q, «Faylga yuklab olish» ishlaydi.
  • npm run build — birinchi yuklash ~222.8 KB.

9. Real ishda

  • Muharrirlar va dizayn vositalari (rasm, diagramma, jadval veb-ilovalari) — showSaveFilePicker va showOpenFilePicker bilan "haqiqiy" fayllar, Chrome'da "Saqlash" bir bosishda.
  • Admin panellar — hisobot fetch + Blob bilan (token kerak) yoki server havolasi Content-Disposition bilan.
  • Mobil — Web Share orqali faylni Telegram'ga yuborish yuklab olishdan qulayroq.
  • Intervyu savollari: "revokeObjectURL qachon chaqiriladi?", "nega download boshqa domendagi faylga ishlamaydi?", "progressive enhancement'ga misol?", "katta faylni xotirasiz qanday saqlaysiz?".

Xulosa

  • Yuklab berish: Blob → obyekt URL → <a download> → click() → setTimeout bilan revoke; hodisada, renderda emas; e'lon bilan.
  • Havola sahifada qolsa — URL'ni effekt tozalashida bo'shating.
  • Fayl nomi: taqiqlangan belgilar, CON kabi nomlar, ko'rinmas va yo'nalish belgilari (RLO) — tozalang.
  • Server fayli — Content-Disposition (filename* — UTF-8); fetch bilan olinsa, nomni sarlavhadan o'qing.
  • showSaveFilePicker — Chrome/Edge: faollashuvdan darhol, AbortError — bekor, boshqa xato — zaxira; tugma faqat API bor joyda.
  • file-saver bugun shart emas; telefonda — Web Share bilan fayl.

Keyingi dars: Analitika: foydalanuvchi xatti-harakatini o'lchash — sahifa ko'rishlari va hodisalarni shaxsiy ma'lumotsiz yig'ish, GA4, Yandex Metrika, PostHog, rozilik va qonunchilik.

Manbalar

  • MDN: HTMLAnchorElement.download, URL.createObjectURL(), Window.showSaveFilePicker(), FileSystemWritableFileStream, Navigator.canShare()
  • WICG: File System Access — wicg.github.io/file-system-access
  • RFC 6266 (Content-Disposition), RFC 5987 (filename*)
  • Unicode: Bidi_Control xususiyati; "Unicode Security Considerations" (UTR #36)
  • file-saver — npm (2.0.5, 2020-11-19)
  • Kanon: vazifalar holat 17, natijalar/tekshiruv-17.txt, brauzer-tekshiruv/natija-21-17.txt, e2e-21.mjs (soxta picker) (2026-10-17); sandbox iframe'dagi SecurityError — dars tayyorlashda Chrome 155 headless (2026-10-18)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Fayl yuklab berish React'da: Blob, a[download] va showSaveFilePicker — IlmHamroh