Пошук уроків, статей та іншого контенту
Передаватимете файли й додаткові поля форми через FormData та відстежуватимете типи вмісту.
FormDataFormData — це об’єкт для формування даних форми у форматі multipart/form-data. Його використовують, коли запит має містити:
текстові поля;
один або кілька файлів;
Blob або File;
повторювані поля з однаковим ім’ям;
додаткові метадані разом із файлами.
На відміну від JSON, FormData може передавати бінарні дані без перетворення файлу на Base64.
const formData = new FormData();
formData.append("title", "Звіт за липень");
formData.append("published", "true");
const file = new File(["Вміст звіту"], "report.txt", {
type: "text/plain",
});
formData.append("document", file);Кожен виклик append() додає нове значення. Якщо поле вже існує, його попереднє значення не видаляється.
FormData з HTML-формиНайчастіше FormData створюють на основі форми:
<form id="upload-form">
<label>
Назва документа:
<input name="title" type="text" required>
</label>
<label>
Опис:
<textarea name="description"></textarea>
</label>
<label>
Файли:
<input name="documents" type="file" multiple required>
</label>
<label>
Опубліковано:
<input name="published" type="checkbox" value="yes">
</label>
<button type="submit">Надіслати</button>
</form>
<pre id="status"></pre>
<script>
const form = document.querySelector("#upload-form");
const status = document.querySelector("#status");
form.addEventListener("submit", async (event) => {
event.preventDefault();
const formData = new FormData(form);
// Перевіряємо всі поля перед надсиланням
for (const [name, value] of formData.entries()) {
if (value instanceof File) {
console.log(name, {
fileName: value.name,
size: value.size,
type: value.type,
});
} else {
console.log(name, value);
}
}
try {
const response = await fetch("/upload", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`Помилка HTTP: ${response.status}`);
}
const result = await response.json();
status.textContent = `Завантажено файлів: ${result.count}`;
} catch (error) {
status.textContent = `Не вдалося надіслати форму: ${error.message}`;
}
});
</script>У цьому прикладі браузер:
знаходить усі елементи форми з атрибутом name;
додає їхні значення до FormData;
додає вибрані файли як об’єкти File;
надсилає дані на /upload.
Поле без атрибута name не потрапляє до FormData.
Файли можна отримати з <input type="file"> і додати самостійно:
const input = document.querySelector('input[type="file"]');
const formData = new FormData();
for (const file of input.files) {
formData.append("documents", file);
}Якщо користувач вибрав три файли, у FormData буде три записи з ім’ям documents.
На сервері таке поле має оброблятися як масив, а не як одне значення:
documents = [file1, file2, file3]Для кожного файлу можна передати окреме ім’я поля:
for (const [index, file] of [...input.files].entries()) {
formData.append(`document_${index}`, file);
}Однак повторювані поля зазвичай зручніші для серверної обробки.
append() і set()Методи append() і set() мають різну поведінку:
const formData = new FormData();
formData.append("tag", "javascript");
formData.append("tag", "browser");
console.log(formData.getAll("tag"));
// ["javascript", "browser"]
formData.set("tag", "forms");
console.log(formData.getAll("tag"));
// ["forms"]append()Додає нове значення:
formData.append("role", "admin");
formData.append("role", "editor");Результат:
role = admin
role = editorset()Замінює всі значення з таким іменем одним новим:
formData.set("role", "viewer");Результат:
role = viewerВикористовуйте:
append() для масивів і кількох файлів;
set() для полів, які повинні мати лише одне значення.
FormDataМетод append() приймає рядок, Blob або File:
const formData = new FormData();
formData.append("title", "Резюме");
formData.append("version", 3);
formData.append("enabled", true);
const blob = new Blob(
[JSON.stringify({ source: "browser", version: 1 })],
{ type: "application/json" }
);
formData.append("metadata", blob, "metadata.json");Числа та логічні значення перетворюються на рядки:
console.log(formData.get("version"));
// "3"
console.log(formData.get("enabled"));
// "true"FormData не зберігає типи number і boolean так, як це робить JavaScript-об’єкт. Якщо серверу потрібне число або логічне значення, він має виконати явне перетворення.
Об’єкт або масив не можна передавати безпосередньо, очікуючи JSON-семантики:
const formData = new FormData();
formData.append("settings", { darkMode: true });
// "[object Object]"Правильний варіант:
formData.append(
"settings",
JSON.stringify({ darkMode: true })
);На сервері це значення потрібно розібрати як JSON.
File, Blob і ім’я файлуFile є спеціалізованим різновидом Blob. Він містить:
байти даних;
ім’я файлу;
MIME-тип;
розмір;
дату останньої зміни.
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];
console.log(file.name);
console.log(file.size);
console.log(file.type);
console.log(file.lastModified);Blob не обов’язково має ім’я файлу. Якщо передати його через append() без третього аргументу, браузер може використати стандартне ім’я, наприклад blob.
const content = new Blob(["hello"], {
type: "text/plain",
});
formData.append("file", content, "greeting.txt");Третій параметр задає ім’я файлу, яке буде передано серверу.
Для текстового JSON як файлу:
const jsonBlob = new Blob(
[JSON.stringify({ id: 42, active: true }, null, 2)],
{ type: "application/json" }
);
formData.append("configuration", jsonBlob, "configuration.json");Content-TypeЗапит із FormData має заголовок приблизно такого вигляду:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...boundary — це спеціальний роздільник між частинами запиту. Він потрібен серверу, щоб відокремити одне поле від іншого та визначити межі бінарних даних.
Під час надсилання FormData через fetch() не потрібно вручну встановлювати Content-Type:
const formData = new FormData();
formData.append("name", "document");
formData.append("file", file);
await fetch("/upload", {
method: "POST",
body: formData,
});Не робіть так:
await fetch("/upload", {
method: "POST",
headers: {
"Content-Type": "multipart/form-data",
},
body: formData,
});У такому випадку браузер не додасть коректний boundary до заголовка. Сервер може не змогти розібрати тіло запиту.
Загальне правило:
Якщо
body— цеFormData, дозвольте браузеру самостійно сформуватиContent-Type.
Це відрізняється від JSON-запиту:
await fetch("/settings", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ darkMode: true }),
});Для JSON заголовок потрібно встановити вручну, а для FormData — ні.
FormDataДля перегляду записів використовують entries(), keys(), values() або forEach():
const formData = new FormData();
formData.append("title", "Звіт");
formData.append("tag", "javascript");
formData.append("tag", "upload");
for (const [name, value] of formData.entries()) {
if (value instanceof File) {
console.log(`${name}: ${value.name} (${value.size} байт)`);
} else {
console.log(`${name}: ${value}`);
}
}Методи читання:
formData.get("title");
// перше значення або null
formData.getAll("tag");
// ["javascript", "upload"]
formData.has("title");
// true
formData.delete("tag");
formData.keys();
formData.values();
formData.entries();Важливо: console.log(formData) часто не показує його записи безпосередньо. Для діагностики перебирайте entries().
У браузері MIME-тип доступний через File.type:
function describeFile(file) {
return {
name: file.name,
type: file.type || "тип не визначено",
size: file.size,
};
}Наприклад:
const file = input.files[0];
console.log(describeFile(file));
// {
// name: "photo.jpg",
// type: "image/jpeg",
// size: 184320
// }Однак File.type не можна вважати надійним механізмом безпеки. Значення може бути порожнім або залежати від операційної системи та браузера. Розширення файлу також можна змінити вручну.
Тому перевірки потрібно виконувати на сервері:
обмежувати максимальний розмір;
перевіряти фактичний формат;
перевіряти MIME-тип;
не довіряти імені файлу;
генерувати власне безпечне ім’я для збереження;
не зберігати завантажені файли в каталозі, де вони можуть виконуватися як код.
Клієнтська перевірка корисна для зручності, але не замінює серверну.
function validateFile(file) {
const maxSize = 10 * 1024 * 1024;
const allowedTypes = new Set([
"image/jpeg",
"image/png",
"application/pdf",
]);
if (file.size > maxSize) {
return "Файл перевищує максимальний розмір 10 МБ";
}
if (!allowedTypes.has(file.type)) {
return "Дозволені лише JPEG, PNG або PDF";
}
return null;
}
const files = [...input.files];
for (const file of files) {
const error = validateFile(file);
if (error) {
throw new Error(`${file.name}: ${error}`);
}
}Атрибут accept допомагає користувачу вибрати правильний файл:
<input
name="documents"
type="file"
accept="image/jpeg,image/png,application/pdf"
multiple
>accept не є захистом: користувач або клієнтський код все одно може надіслати інший тип.
multipart/form-dataІноді разом із файлами потрібно передати складний об’єкт. Для цього його серіалізують у JSON і додають як Blob із відповідним типом:
const formData = new FormData();
const options = {
visibility: "private",
labels: ["finance", "monthly"],
};
const optionsBlob = new Blob(
[JSON.stringify(options)],
{ type: "application/json" }
);
formData.append("options", optionsBlob, "options.json");
formData.append("file", input.files[0]);На сервері поле options можна розібрати як JSON-частину multipart-запиту.
Це краще, ніж передавати складний об’єкт через звичайний append(), оскільки вказаний тип явно повідомляє серверу, що частина містить JSON.
fetch() зручно використовувати для надсилання FormData, але стандартний API не надає універсальної події прогресу саме для upload-частини запиту. Для відстеження прогресу в браузері часто використовують XMLHttpRequest.
<form id="progress-form">
<input name="title" value="Великий файл">
<input id="file-input" name="file" type="file" required>
<button type="submit">Завантажити</button>
</form>
<progress id="progress" value="0" max="100"></progress>
<span id="progress-label">0%</span>
<script>
const form = document.querySelector("#progress-form");
const fileInput = document.querySelector("#file-input");
const progress = document.querySelector("#progress");
const progressLabel = document.querySelector("#progress-label");
form.addEventListener("submit", (event) => {
event.preventDefault();
const file = fileInput.files[0];
if (!file) {
return;
}
const formData = new FormData(form);
const request = new XMLHttpRequest();
request.open("POST", "/upload");
request.upload.addEventListener("progress", (event) => {
if (!event.lengthComputable) {
progressLabel.textContent = "Завантаження...";
return;
}
const percent = Math.round((event.loaded / event.total) * 100);
progress.value = percent;
progressLabel.textContent = `${percent}%`;
});
request.addEventListener("load", () => {
if (request.status >= 200 && request.status < 300) {
progressLabel.textContent = "Завантаження завершено";
} else {
progressLabel.textContent = `Помилка HTTP: ${request.status}`;
}
});
request.addEventListener("error", () => {
progressLabel.textContent = "Мережева помилка";
});
request.send(formData);
});
</script>Так само, як і у випадку з fetch(), не потрібно встановлювати Content-Type вручну. Метод send(formData) сам формує коректне multipart-тіло та заголовок.
Для fetch() можна використати AbortController:
const controller = new AbortController();
const formData = new FormData();
formData.append("file", file);
const promise = fetch("/upload", {
method: "POST",
body: formData,
signal: controller.signal,
});
// Наприклад, викликається після натискання кнопки «Скасувати»
controller.abort();
try {
await promise;
} catch (error) {
if (error.name === "AbortError") {
console.log("Надсилання скасовано");
} else {
throw error;
}
}Після скасування сервер уже міг отримати частину даних. Тому серверне API має коректно обробляти перервані завантаження та тимчасові файли.
Клієнт не повинен вважати успішним будь-який HTTP-відповідь, який вдалося отримати:
const response = await fetch("/upload", {
method: "POST",
body: formData,
});
if (!response.ok) {
const message = await response.text();
throw new Error(message || `HTTP ${response.status}`);
}
const result = await response.json();Зазвичай сервер повертає структурований результат:
{
"count": 2,
"files": [
{
"name": "report.pdf",
"size": 245760,
"url": "/files/abc123"
}
]
}Не варто використовувати ім’я, отримане від клієнта, як шлях до файлу. Ім’я потрібно нормалізувати або замінити на ідентифікатор, згенерований сервером.
Великі файли потрібно обробляти обережно:
обмежуйте розмір на клієнті для швидкого зворотного зв’язку;
повторно перевіряйте розмір на сервері;
не перетворюйте великі файли на Base64 без необхідності;
не викликайте arrayBuffer() або text() для великих файлів, якщо потрібне лише їх завантаження;
для дуже великих файлів розглядайте потокове або часткове завантаження;
передбачайте повторне завантаження частин і відновлення після обриву мережі.
FormData зручний для стандартних multipart-запитів, але не є протоколом resumable upload сам по собі.
Content-Typeheaders: {
"Content-Type": "multipart/form-data"
}Так можна втратити boundary. Якщо використовується FormData, не встановлюйте цей заголовок вручну.
JSON.stringify(formData)JSON.stringify(formData);Результат не міститиме файлів і не буде коректним представленням multipart-даних. Надсилайте сам об’єкт:
fetch("/upload", {
method: "POST",
body: formData,
});name<input type="file" id="document">Такий елемент не буде автоматично включений до new FormData(form). Потрібно додати name:
<input type="file" id="document" name="document">get() для кількох файлівformData.get("documents");Це поверне лише перше значення. Для всіх файлів використовуйте:
formData.getAll("documents");formData.append("count", 5);
formData.append("enabled", false);Після читання це будуть рядки:
formData.get("count"); // "5"
formData.get("enabled"); // "false"File.typeFile.type може бути порожнім або некоректним. Він підходить для попередньої перевірки інтерфейсу, але не для серверної авторизації чи захисту від небезпечних файлів.
name у файлу, створеного з BlobformData.append("document", blob);Для передавання зрозумілого імені задайте третій аргумент:
formData.append("document", blob, "document.txt");async function uploadFiles(form, endpoint) {
const formData = new FormData(form);
const files = formData.getAll("documents");
if (files.length === 0) {
throw new Error("Не вибрано жодного файлу");
}
for (const file of files) {
if (!(file instanceof File)) {
continue;
}
if (file.size === 0) {
throw new Error(`Файл ${file.name} порожній`);
}
if (file.size > 20 * 1024 * 1024) {
throw new Error(`Файл ${file.name} перевищує 20 МБ`);
}
}
const response = await fetch(endpoint, {
method: "POST",
body: formData,
credentials: "same-origin",
});
if (!response.ok) {
throw new Error(`Сервер повернув HTTP ${response.status}`);
}
const contentType = response.headers.get("Content-Type") || "";
if (!contentType.includes("application/json")) {
throw new Error("Сервер повернув відповідь не у форматі JSON");
}
return response.json();
}Використання:
const form = document.querySelector("#upload-form");
form.addEventListener("submit", async (event) => {
event.preventDefault();
try {
const result = await uploadFiles(form, "/upload");
console.log("Результат:", result);
} catch (error) {
console.error("Помилка завантаження:", error);
}
});FormData призначений для надсилання полів форми, файлів, Blob і File.
Для кількох значень з одним ім’ям використовуйте append() і getAll().
set() замінює всі попередні значення поля.
Числа, логічні значення та інші прості значення у FormData передаються як рядки.
Складні об’єкти потрібно серіалізувати через JSON.stringify().
Не встановлюйте Content-Type: multipart/form-data вручну: браузер має додати boundary.
File.type і розширення файлу не є надійною серверною перевіркою.
Для прогресу завантаження використовуйте XMLHttpRequest.upload.
Сервер повинен перевіряти розмір, фактичний формат, ім’я та вміст кожного файлу.
Для дуже великих файлів звичайний FormData може бути недостатнім без механізму часткового або відновлюваного завантаження.