Mundarija (27)
- Bu darsda
- 1. Nega bu kerak?
- 2. Oddiy yuklab berish React'da
- 2.1 Retsept va qayerga yoziladi
- 2.2 Havola sahifada qolsa
- 3. Fayl nomi
- 3.1 Nom qayerdan keladi
- 3.2 Yashirin kengaytma hujumi
- 3.3 Serverdan kelgan fayl: Content-Disposition
- 4. showSaveFilePicker: joyini tanlab saqlash
- 4.1 API qanday ishlaydi
- 4.2 Uch natija — bitta funksiya
- 4.3 Progressiv: tugma faqat bor joyda
- 4.4 Katta fayl: to'g'ri diskka oqim
- 5. file-saver va Web Share
- 6. Vazifalar qadami: «Joyini tanlab saqlash» (holat 17)
- 6.1 O'zgarishlar
- 6.2 Natija
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): bo'sh joylarni to'ldiring
- 2-mashq (o'rta): «Saqlanmoqda…» holati
- 3-mashq (qiyin): Content-Disposition dan nom
- 4-mashq: Vazifalar qadami
- 9. Real ishda
- Xulosa
- Manbalar
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,
revokevaqtini to'g'ri tanlaysiz. - Fayl nomini xavfsiz qilasiz: taqiqlangan va ko'rinmas belgilar, yashirin kengaytma hujumi.
- Serverdan kelgan faylni
fetchbilan olib, nominiContent-Dispositiondan o'qiysiz. showSaveFilePickerni 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:
- Oddiy yuklab berish —
FilevaBlobdarsidagi retsept React'da: qayerga yoziladi, qachon bo'shatiladi, nomi qanday tozalanadi. - 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):
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.
faylniYuklabBerReact'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:
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:
yaratildi: trueEkranda — «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
faylniYuklabBerURL'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:
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:
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.ubayrog'i — Unicode xususiyatlari bilan ishlash uchun (Unicode regex:uvavflag).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:
Content-Disposition: attachment; filename="hisobot.csv"; filename*=UTF-8''%D0%BE%D1%82%D1%87%D1%91%D1%82.csvattachment — "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:
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.»
endDiagrammaga 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.domida 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:
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:
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:
yozildi: {"vazifalar":[]}
zaxira yo'l: vazifalar.jsonEkranda:
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. SaytdagiSecurityErrorham 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:
/** "Joyini tanlab saqlash" bormi: Chrome/Edge
* (Firefox, Safari — yo'q) */
export const joyTanlashBor = (): boolean =>
typeof window.showSaveFilePicker === "function"; {/* 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:
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), keyinshowSaveFilePicker(…). 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):
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
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:
// 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:
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:
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).
git add -A
git commitCommit (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
awaitqilib 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 —
inlineesa 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
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:
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:
filename*=UTF-8''…bo'lsa — unidecodeURIComponentbilan ochib qaytarsin (u ustun).- Bo'lmasa —
filename="…"yokifilename=…(qo'shtirnoqsiz). - 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
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:
отчёт.csv
chek-12.pdf
null
nullBirinchi 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.showSaveFilePickerni tekshiruvsiz chaqirsangiz, TypeScript "ehtimolundefined" 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) —
showSaveFilePickervashowOpenFilePickerbilan "haqiqiy" fayllar, Chrome'da "Saqlash" bir bosishda. - Admin panellar — hisobot
fetch+ Blob bilan (token kerak) yoki server havolasiContent-Dispositionbilan. - Mobil — Web Share orqali faylni Telegram'ga yuborish yuklab olishdan qulayroq.
- Intervyu savollari: "
revokeObjectURLqachon chaqiriladi?", "negadownloadboshqa domendagi faylga ishlamaydi?", "progressive enhancement'ga misol?", "katta faylni xotirasiz qanday saqlaysiz?".
Xulosa
- Yuklab berish: Blob → obyekt URL →
<a download>→click()→setTimeoutbilanrevoke; hodisada, renderda emas; e'lon bilan. - Havola sahifada qolsa — URL'ni effekt tozalashida bo'shating.
- Fayl nomi: taqiqlangan belgilar,
CONkabi nomlar, ko'rinmas va yo'nalish belgilari (RLO) — tozalang. - Server fayli —
Content-Disposition(filename*— UTF-8);fetchbilan 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:
vazifalarholat 17,natijalar/tekshiruv-17.txt,brauzer-tekshiruv/natija-21-17.txt,e2e-21.mjs(soxta picker) (2026-10-17); sandbox iframe'dagiSecurityError— dars tayyorlashda Chrome 155 headless (2026-10-18)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!