FormData и multipart
FormData— Web API для построения набора пар ключ/значение, включая файлы, для отправки черезfetchилиXMLHttpRequestв форматеmultipart/form-data, который поддерживает бинарные данные и загрузку файлов.
Зачем нужно
Стандартный JSON.stringify не умеет сериализовать файлы (File, Blob). FormData решает эту проблему: он автоматически устанавливает правильный Content-Type: multipart/form-data с boundary и сериализует данные — как текст, так и бинарные файлы — в нужном формате.
Где используется
- Загрузка файлов (аватар, документы, изображения)
- Форма регистрации с вложениями
- Массовая загрузка файлов (drag & drop)
- API, требующие
multipart/form-data(большинство REST-backend'ов для файлов)
Создание FormData
// 1. Из HTML-формы (автоматически собирает все поля)
const form = document.querySelector('#upload-form');
const formData = new FormData(form);
// 2. Вручную
const formData = new FormData();
formData.append('name', 'Иван Петров');
formData.append('age', 25); // числа конвертируются в строку
formData.append('avatar', fileInput.files[0]); // File объект
Методы FormData
const fd = new FormData();
// Добавить поле
fd.append('key', 'value');
fd.append('file', blob, 'filename.txt'); // Blob с именем файла
// Установить (перезаписать)
fd.set('key', 'new-value');
// Получить
fd.get('key'); // первое значение
fd.getAll('key'); // все значения (для массивов)
// Проверить существование
fd.has('key'); // true/false
// Удалить
fd.delete('key');
// Итерация
for (const [key, value] of fd) {
console.log(key, value);
}
fd.forEach((value, key) => console.log(key, value));
[...fd.keys()]; // ['name', 'file', ...]
[...fd.values()]; // ['Иван', File {}, ...]
[...fd.entries()]; // [['name', 'Иван'], ...]
Отправка файла через fetch
async function uploadAvatar(file) {
const formData = new FormData();
formData.append('avatar', file, file.name);
formData.append('userId', '42');
// НЕ устанавливай Content-Type вручную!
// fetch сам установит multipart/form-data с правильным boundary
const response = await fetch('/api/upload/avatar', {
method: 'POST',
body: formData
// headers: { 'Content-Type': 'multipart/form-data' } — НЕ ДОБАВЛЯЙ!
});
if (!response.ok) throw new Error('Ошибка загрузки');
return response.json();
}
// Использование
fileInput.addEventListener('change', async () => {
const file = fileInput.files[0];
if (!file) return;
const result = await uploadAvatar(file);
console.log('Загружено:', result.url);
});
Множественная загрузка файлов
async function uploadMultiple(files) {
const formData = new FormData();
Array.from(files).forEach((file, index) => {
formData.append(`file_${index}`, file, file.name);
});
// Или с одним ключом для массива:
Array.from(files).forEach(file => {
formData.append('files', file, file.name); // PHP/Laravel стиль
// или просто:
formData.append('files', file, file.name); // зависит от backend
});
formData.append('uploadedAt', new Date.toISOString());
return fetch('/api/upload/multiple', {
method: 'POST',
body: formData
});
}
Прогресс загрузки (XMLHttpRequest)
fetch пока не поддерживает прогресс загрузки (upload progress). Используй XMLHttpRequest:
function uploadWithProgress(file, onProgress) {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload');
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percent = Math.round((event.loaded / event.total) * 100);
onProgress(percent);
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) resolve(JSON.parse(xhr.responseText));
else reject(new Error(`HTTP ${xhr.status}`));
});
xhr.addEventListener('error', () => reject(new Error('Сетевая ошибка')));
xhr.send(formData);
});
}
await uploadWithProgress(file, (percent) => {
progressBar.style.width = `${percent}%`;
});
FormData из объекта (утилита)
function objectToFormData(obj, prefix = '') {
const fd = new FormData();
function appendValue(key, value) {
if (value instanceof File || value instanceof Blob) {
fd.append(key, value, value.name || 'blob');
} else if (Array.isArray(value)) {
value.forEach(v => fd.append(`${key}`, v));
} else if (typeof value === 'object' && value !== null) {
Object.entries(value).forEach(([k, v]) => appendValue(`${key}[${k}]`, v));
} else {
fd.append(key, value ?? '');
}
}
Object.entries(obj).forEach(([k, v]) => appendValue(k, v));
return fd;
}
const data = objectToFormData({
name: 'Иван',
tags: ['js', 'web'],
avatar: fileInput.files[0]
});
Частые ошибки
1. Ручная установка Content-Type
// Плохо: boundary потеряется — сервер не сможет разобрать
fetch('/api/upload', {
method: 'POST',
body: formData,
headers: {
'Content-Type': 'multipart/form-data' // не делай это!
}
});
// Правильно: не указывай Content-Type при FormData — браузер сделает сам
fetch('/api/upload', { method: 'POST', body: formData });
2. Числа не сохраняют тип
formData.append('count', 42);
formData.get('count'); // '42' — строка, не число!
// На backend всегда преобразуй к нужному типу
3. FormData нельзя JSON.stringify
JSON.stringify(formData); // '{}' — пустой объект, FormData непрозрачен для JSON
// Для просмотра используй:
[...formData().entries()].forEach(([k, v]) => console.log(k, v));