FormData

Асинхронно отправлять JSON на сервер может каждый, а файлы — только FormData.

Время чтения: 8 мин

Кратко

Скопировано

FormData — это специальная коллекция данных, которая позволяет передавать данные в виде пар [ключ, значение] на сервер при помощи fetch() или XMLHttpRequest. При этом используется точно такой же формат данных, какой использует тег <form> с типом кодирования 'multipart/form-data'. Поэтому, значения в FormData, как и у обычной HTML формы, могут быть только строками или файлами.

Пример

Скопировано

Предположим, что мы пишем функцию, которая отправляет на сервер два поля name и email. Значения полей она получает через аргументы:

        
          
          async function sendData(name, email) {  const data = new FormData()  data.append('name', name)  data.append('email', email)  return await fetch('/api/subscribe/', {    method: 'POST',    body: data,  })}
          async function sendData(name, email) {
  const data = new FormData()

  data.append('name', name)
  data.append('email', email)

  return await fetch('/api/subscribe/', {
    method: 'POST',
    body: data,
  })
}

        
        
          
        
      

Данные отправляются на сервер с помощью объекта FormData. Мы используем метод append() чтобы добавить значения, а затем передаём полученный объект функции fetch().

Как пишется

Скопировано

Для работы с FormData сначала с помощью конструктора new создаётся объект этого типа: const form = new FormData(). Затем у полученного объекта можно вызывать методы.

Основные методы для работы с FormData:

  • append(ключ, значение) — добавляет значение для ключа с сохранением предыдущих значений;
  • set(ключ, значение) — устанавливает значение для ключа, перезаписывая предыдущие значения;
  • get(ключ) — возвращает первое значение ключа;
  • getAll(ключ) — возвращает все значения ключа;
  • has(ключ) — проверяет наличие переданного ключа;
  • entries() — возвращает итератор пар [ключ, значение];
  • values() — возвращает итератор всех значений коллекции;
  • keys() — возвращает итератор всех ключей коллекции;
  • delete(ключ) — удаляет конкретное значение;

Как понять

Скопировано

При отправке данных на сервер и сервер и клиент должны понимать друг друга, то есть использовали понятные обоим способы кодирования и декодирования данных. Таких способов существует большое количество и FormData позволяет работать с одним из них — 'multipart/form-data'.

FormData похожа на коллекцию Map — предоставляет удобные методы для добавления и удаления данных. Но если передать её в качестве тела запроса при вызове fetch() (как в примере выше), данные «под капотом» будут преобразованы в нужный формат, а HTTP-заголовку Content-Type будет присвоено значение 'multipart/form-data', чтобы сервер знал, что именно с этим форматом ему предстоит работать.

FormData является отражением данных обычной HTML-формы с атрибутом enctype="multipart/form-data", поэтому пример выше можно представить следующим образом без JavaScript:

        
          
          <form method="post" action="/api/subscribe/" enctype="multipart/form-data">  <input type="text" name="name" value="" />  <input type="email" name="email" value="" />  <button type="submit">Отправить</button></form>
          <form method="post" action="/api/subscribe/" enctype="multipart/form-data">
  <input type="text" name="name" value="" />
  <input type="email" name="email" value="" />
  <button type="submit">Отправить</button>
</form>

        
        
          
        
      

Когда выбирать

Скопировано

Существует несколько самых популярных способов кодирования данных для отправки на сервер: 'application/x-www-form-urlencoded', 'multipart/form-data' и 'application/json'. Иногда бывает так, что сервер поддерживает только какой-то определённый способ. Тогда выбирать не приходится. Но чаще всего современные решения на бэкенде поддерживают несколько способов, поэтому выбирать нужно в зависимости от задачи.

  • 'application/x-www-form-urlencoded' — способ, который используют HTML-формы по умолчанию. Из-за особенностей преобразования, этот способ плохо подходит для больших объёмов данных, в особенности, файлов или строк с большим количеством символов не из ASCII таблицы (например, символы русского алфавита).
  • 'application/json' — достаточно популярный формат из-за широкого распространения JSON как формата обмена данными. Из плюсов - поддерживает вложенные структуры, поэтому можно в одном запросе отправить, например, целый объект с данными. Однако, чтобы отправить файл при помощи этого формата, необходимо файл дополнительно закодировать в строку каким-нибудь алгоритмом, например Base64. Причём на сервере нужно декодировать эти данные обратно.
  • 'multipart/form-data' — удобный способ для загрузки файлов, оптимален с точки зрения размера закодированных данных, но в качестве значений может хранить только строки или файлы.

Поэтому, лучше всего использовать FormData для отправки файлов на сервер или когда поддержка только строковых данных не является проблемой. Дополнительно, при создании FormData можно передать DOM-элемент формы (будет рассмотрено ниже), и коллекция вытащит из этой формы все данные. Поэтому, если стоит задача отравить данные какой-либо формы, FormData позволит сделать это с минимумом кода.

Создание FormData

Скопировано

Создать новый пустой объект FormData можно с помощью конструктора:

        
          
          const data = new FormData()
          const data = new FormData()

        
        
          
        
      

Также, конструктор может принимать в качестве аргумента DOM-элемент формы, в этом случае FormData запишет текущие значения полей формы:

        
          
          <form id="user-form">  <input type="text" name="name" value="Аня" />  <input type="text" name="language" value="JavaScript" /></form>
          <form id="user-form">
  <input type="text" name="name" value="Аня" />
  <input type="text" name="language" value="JavaScript" />
</form>

        
        
          
        
      
        
          
          const form = document.querySelector('#user-form')const data = new FormData(form)for (let [key, value] of data) {  console.log(`${key} - ${value}`)}// 'name - Аня'// 'language - JavaScript'
          const form = document.querySelector('#user-form')
const data = new FormData(form)

for (let [key, value] of data) {
  console.log(`${key} - ${value}`)
}
// 'name - Аня'
// 'language - JavaScript'

        
        
          
        
      

Работа с коллекцией

Скопировано

Для добавления данных в коллекцию есть метод append():

        
          
          const data = new FormData()data.append('name', 'Вася')
          const data = new FormData()
data.append('name', 'Вася')

        
        
          
        
      

Теперь в коллекции появилось одно значение с ключом name и значением 'Вася'.

После выполнения этого кода, в коллекции будет два значения ('Вася' и 'Лена') для одного ключа name:

        
          
          const data = new FormData()data.append('name', 'Вася')data.append('name', 'Лена')
          const data = new FormData()

data.append('name', 'Вася')
data.append('name', 'Лена')

        
        
          
        
      

FormData поддерживает ещё один метод для записи данных: set(). В отличие от append(), он перезапишет старые данные для переданного ключа, если они были:

        
          
          const data = new FormData()data.set('name', 'Вася')data.set('name', 'Лена')
          const data = new FormData()

data.set('name', 'Вася')
data.set('name', 'Лена')

        
        
          
        
      

В коллекции у ключа name будет одно значение 'Лена', потому что прошлое значение было перезаписано.

Вот пример такого поведения. Записываем число 30, но фактически записывается строка '30':

        
          
          const data = new FormData()data.append('age', 30)console.log(data.get('age') === 30)// falseconsole.log(data.get('age') === '30')// trueconsole.log(typeof data.get('age'))// 'string'
          const data = new FormData()

data.append('age', 30)

console.log(data.get('age') === 30)
// false

console.log(data.get('age') === '30')
// true

console.log(typeof data.get('age'))
// 'string'

        
        
          
        
      

Для получения записанных значений есть два метода: get() и getAll(). get() вернёт первое значение для ключа или null, если для указанного ключа значений не было:

        
          
          const data = new FormData()console.log(data.get('name'))// nulldata.append('name', 'Вася')data.append('name', 'Лена')console.log(data.get('name'))// 'Вася'
          const data = new FormData()

console.log(data.get('name'))
// null

data.append('name', 'Вася')
data.append('name', 'Лена')
console.log(data.get('name'))
// 'Вася'

        
        
          
        
      

В примере выше второе значение 'Лена' при помощи метода get() недоступно, потому что он всегда возвращает только первое значение. Поэтому, чтобы получить все значения, на помощь приходит getAll(). Он всегда возвращает массив значений для указанного ключа. Если значений не было, возвращаемый массив будет пустой:

        
          
          const data = new FormData()console.log(data.getAll('name'))// []data.append('name', 'Вася')data.append('name', 'Лена')console.log(data.getAll('name'))// ['Вася', 'Лена']
          const data = new FormData()

console.log(data.getAll('name'))
// []

data.append('name', 'Вася')
data.append('name', 'Лена')
console.log(data.getAll('name'))
// ['Вася', 'Лена']

        
        
          
        
      

Чтобы проверить, есть ли в коллекции данные для определённого ключа, есть метод has(), он вернёт true или false:

        
          
          const data = new FormData()console.log(data.has('name'))// falsedata.append('name', 'Вася')console.log(data.has('name'))// true
          const data = new FormData()

console.log(data.has('name'))
// false

data.append('name', 'Вася')

console.log(data.has('name'))
// true

        
        
          
        
      

Для удаления значений для определённого ключа можно воспользоваться методом delete(). Важно помнить, что если у указанного ключа несколько значений, то удалятся все значения:

        
          
          const data = new FormData()data.append('name', 'Вася')data.append('name', 'Лена')data.delete('name')
          const data = new FormData()

data.append('name', 'Вася')
data.append('name', 'Лена')

data.delete('name')

        
        
          
        
      

После выполнения кода, коллекция снова будет пустая. Мы удалили ключ целиком, поэтому оба значения по этому ключу исчезли.

Обход значений

Скопировано

FormData предоставляет встроенный итератор для обхода значений:

        
          
          const data = new FormData()data.append('name', 'Вася')data.append('name', 'Лена')data.append('language', 'JavaScript')for (let [key, value] of data) {  console.log(`${key} - ${value}`)}// name - Вася// name - Лена// language - JavaScript
          const data = new FormData()

data.append('name', 'Вася')
data.append('name', 'Лена')
data.append('language', 'JavaScript')

for (let [key, value] of data) {
  console.log(`${key} - ${value}`)
}
// name - Вася
// name - Лена
// language - JavaScript

        
        
          
        
      

Тот же итератор доступен при помощи метода entries(). Обратите внимание, что каждый элемент итератора — массив из двух элементов. Первый элемент — ключ, а второй — значение.

В дополнение к этому, FormData предоставляет два других итератора: только ключей при помощи метода keys() и только значений при помощи values(). Каждый ключ при перечислении ключей будет появляться ровно столько раз, сколько значений он содержит:

        
          
          const data = new FormData()data.append('name', 'Вася')data.append('name', 'Лена')data.append('language', 'JavaScript')console.log('Проходимся по значениям:')for (let value of data.values()) {  console.log(value)}// 'Вася'// 'Лена'// 'JavaScript'console.log('Проходимся по ключам:')for (let key of data.keys()) {  console.log(key)}// 'name'// 'name' <-- ключ появился второй раз, потому что содержит два значения// 'language'
          const data = new FormData()

data.append('name', 'Вася')
data.append('name', 'Лена')
data.append('language', 'JavaScript')

console.log('Проходимся по значениям:')
for (let value of data.values()) {
  console.log(value)
}
// 'Вася'
// 'Лена'
// 'JavaScript'

console.log('Проходимся по ключам:')
for (let key of data.keys()) {
  console.log(key)
}
// 'name'
// 'name' <-- ключ появился второй раз, потому что содержит два значения
// 'language'

        
        
          
        
      

На практике

Скопировано

Виталий Баев советует

Скопировано

Сильной стороной FormData является загрузка файлов на сервер. Если при использовании 'application/json' файлы необходимо дополнительно кодировать каким-то способом, чтобы привести к строке (и точно так-же декодировать на сервере), то FormData умеет это делать «из коробки».

Например, если мы хотим после выбора файла сразу же загрузить его на сервер, то нам понадобится следующий HTML:

        
          
          <input id="file-input" type="file" />
          <input id="file-input" type="file" />

        
        
          
        
      

А для отправки на сервер просто добавим файл в объект FormData и отправим его на сервер:

        
          
          // Объявляем функцию загрузки файлаfunction sendFile(file) {  const data = new FormData()  // Добавляем файл  data.append('document', file)  return fetch('/api/upload/', {    method: 'POST',    body: data,  })}const fileInput = document.querySelector('#file-input')fileInput.addEventListener('change', (event) => {  // Получаем файл. Обратите внимание, что файлов может быть несколько если у инпута стоит атрибут `multiple`  const file = event.target.files[0]  // Отправляем файл на сервер при помощи созданной функции  sendFile(file)  // Очищаем текущее значение инпута. Если этого не делать, то при ошибке загрузки, повторный выбор того же файла не вызовет событие _change_  event.target.value = null})
          // Объявляем функцию загрузки файла
function sendFile(file) {
  const data = new FormData()

  // Добавляем файл
  data.append('document', file)

  return fetch('/api/upload/', {
    method: 'POST',
    body: data,
  })
}

const fileInput = document.querySelector('#file-input')
fileInput.addEventListener('change', (event) => {
  // Получаем файл. Обратите внимание, что файлов может быть несколько если у инпута стоит атрибут `multiple`
  const file = event.target.files[0]

  // Отправляем файл на сервер при помощи созданной функции
  sendFile(file)

  // Очищаем текущее значение инпута. Если этого не делать, то при ошибке загрузки, повторный выбор того же файла не вызовет событие _change_
  event.target.value = null
})