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));

Связанные темы

Ресурсы