Использование объекта FormData для загрузки файлов

Кому нужны лицензии на CS Yazzle — на сайте webmasters.ru проводится CS Yazzle — раздача лицензий.

Ранее я писал про отправку форм с использованием ajax’a. Для отправки формы способом, описанным в той статье, необходимо было слепить тело запроса из пар ключ/значение (имя поля / значение) самостоятельно. Кроме того, тем способом нельзя было загрузить файл на сервер, если в форме было расположено поле c type=”file”.

Всего этого можно избежать, если использовать объект FormData. К сожалению, он пока поддерживается далеко не всеми современными браузерами. Его поддерживают только последние, на момент написания статьи, версии Google Сhrome и FireFox.

Посмотреть демо

Объект FormData позволяет составить набор пар ключ/значение для отправки при помощи XMLHttpRequest. Это, в первую очередь, предназначено для отправки данных форм, но вы можете использовать этот объект независимо от форм, тогда передаваемые данные будут в том же формате, что и при обычной отправке формы с enctype=»multipart/form-data».

Создание объекта FormData при помощи HTML формы

Для создания объекта FormData, который содержит данные существующей формы необходимо передать конструктору в качестве параметра эту форму:

var fd = new FormData(form);

Для отправки полученных данных нужно использовать объект XMLHttpRequest.

1
2
3
4
var form = document.getElementById("form"),
  xhr = new XMLHttpRequest();
xhr.open("POST", "submitform.php");
xhr.send(new FormData(form));

Вы также можете добавить дополнительные данные к объекту FormData перед их отправкой на сервер.

1
2
3
4
var form = document.getElementById("form"),
  fd = new FormData(form);
fd.append("serialnumber", serialNumber++);
xhr.send(fd);

Отправка форм с помощью объекта FormData

В форме, данные которой отправляются на сервер с иcпользованием объекта FormData также может содержаться и input type=”file”.

Давайте попробуем отправить такую форму, используя FormData. Ограничим максимальный размер загружаемого файла одним мегабайтом.

Сама форма будет выглядеть вот так:

1
2
3
4
5
6
7
8
9
<form action="upload.php" method="post" enctype="multipart/form-data"
	onsubmit="return sendForm(this, ge('content'))">
 
  <input type="text" placeholder="Ваше имя" name="name" id="name" />
  <progress class="pBar" min="0" max="100" value="0">0% complete</progress>
  <input type="file" name="file" id="file" />
  <div align="right"><div id="status"></div>
  <input type="submit" name="go" id="go" value="Загрузить" /></div>
</form>

Про атрибут placeholder текстового поля я писал ранее.

Содержимое файла style.css:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
body {
  background-color: #ececec;
  font: 62.5% "Trebuchet MS", Tahoma, Arial, sans-serif;
  line-height: 1.6em;
  color: #444;
  font-size: 14px;
}
#content {
  background-color: #fff;
  padding: 30px;
  margin: 200px auto 0;
  width: 250px;
}
input[type=text] {
  width: 240px;
}
input[type=file] {
  width: 250px;
}
input[type=text], input[type=file] {
  padding: 5px;
  font-size: 16px;
  border: 1px solid #ccc;
  color: #999;
  margin-bottom: 20px;
}
input[type=submit] {
  background-color: #ccc;
  border: none;
  padding: 5px;
  font-size: 16px;
  cursor: pointer;
}
input[type=submit]:hover {
  background-color: #89bd51;
  color: #fff;
}
.pBar {
  width: 250px;
  height: 30px;
  margin-bottom: 20px;
}
#status {
  float: left;
  color: red;
}
.notSupport {
  color: red;
}

Содержимое файла script.js в котором расположена функция sendForm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//Избавляем себя от лишнего стука по клаве и сокращаем код
function ge(id) {
  return document.getElementById(id);
}
 
// Как только страничка загрузилась
window.onload = function () {
  // проверяем поддерживает ли браузер FormData
  if(!window.FormData) {
 
    /*
     * если не поддерживает, то выводим
     * то выводим предупреждение вместо формы
     */
 
    var div = ge('content');
    div.innerHTML = "Ваш браузер не поддерживает объект FormData";
    div.className = 'notSupport';
  }
}
 
function sendForm(form, output) {
  var data = new FormData(form),
 
    /*
     * Использовать кроссбраузерный способ создания
     * не имеет смысла, т.к. браузеры для, для которых
     * XMLHttpRequest (xhr) создаётся по-другому, не поддерживают FormData
     */
 
    xhr = new XMLHttpRequest(),
 
    progressBar = document.querySelector('progress'),
    goBtn = ge('go'),
    fileInp = ge('file'),
    nameInp = ge('name');  
  if(nameInp.value == '' && fileInp.value == '') {
    ge('status').innerHTML = 'Заполните поля!';
    return false;
  } else if(nameInp.value == '') {
    ge('status').innerHTML = 'Введите имя!';
    return false
  } else if(fileInp.value == '') {
    ge('status').innerHTML = 'Выберите файл!';
    return false;
  }
 
  if(fileInp.files[0].size > 1024 * 1024) { // 1 мб
    ge('status').innerHTML = 'Максимум 1 мб!';
    return false;
  }
 
  ge('status').innerHTML = '';
 
  xhr.open('POST', form.action);
 
  xhr.onload = function (e) {
    output.innerHTML = e.currentTarget.responseText;
  }
 
  xhr.upload.onprogress = function (e) {
    progressBar.value = e.loaded / e.total * 100;
  }
 
  xhr.send(data);
  return false;
}

Скрипт на сервере, который обрабатывает полученные данные, расположен в файле upload.php. Вот его содержимое:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
  $error = $_FILES['file']['error'];
  switch($error) {
    case 0 :
      $error = 'нет';
      break;    
    case 1 : case 2 :
      $error = 'слишком большой файл';
      break;
    case 3 :
      $error = 'файл загружен частично';
      break;
    case 4 :
      $error = 'файл не был загружен';
  }  
?>
 
Ваше имя: <?=$_POST['name']; ?><br />
Имя файла: <?=$_FILES['file']['name']; ?><br />
Mime type: <?=$_FILES['file']['type']; ?><br />
Ошибки: <?=$error; ?><br />
Размер файла: <?=$_FILES['file']['size']; ?> байт

Этот скрипт просто выводит информацию о залитом файле и текстовое поле (имя пользователя) в доказательство того, что файл действительно был загружен. Вы можете сохранять файл на сервере и возвращать ссылку на его скачивание пользователю, создав, таким образом, файлообменник или прикреплять картинку к сообщению пользователя. Сами думайте, как это использовать. Чтобы избежать проблем с кодировкой используйте UTF-8.

Надеюсь, вскоре этот способ заменит нам костыли с загрузкой файлов с использованием скрытого айфрейма.

Посмотреть демо | Скачать исходники

  • Анатолий

    Если ограничения на длину Post-запроса через FormData?
    Снимается ли это через настройки сервера или надо руками разбивать длинный POST-запрос на части? Если разбивать то как происходит синхронизация на стороне back-end?

  • Анатолий, насколько я знаю, размер загружаемого файла ограничен только настройками php и Apache.

  • Maxim

    А как быть с браузерами, которые не поддерживают FormData?

    • Maxim, для старых браузеров есть трюк со скрытым айфреймом. Выглядит это примерно вот так:

      <form action="upload.php" method="post" enctype="multipart/form-data" target="hidden_upload">
      <input type="file" name="file" />
      </form>
       
      <iframe name="hidden_upload" style="display: none"></iframe>

      Скрипт upload.php в этом случае обычно возвращает в айфрейм javascript, который выводит сообщение о результате заливки файла где-нибудь на страничке. Можно сделать айфрейм видимым и возвращать сообщение туда. Подобный механизм, наверняка, реализует какой-нибудь jquery-плагин.

  • Maxim

    По своей натуре привык свое писать, вот и спрашиваю) Т.е. на странице создается скрытый фрэйм, в который JS вносит input с type=file и значением — адрес из видимого и отправка уже идет из фрэйма?

  • Maxim, нет. Форма находится вне фрейма, на странице. Отправка происходит оттуда же в айфрейм возвращается ответ сервера.

  • Maxim

    Не пойму тогда. Веде перезагрузка страницы произойдет.

  • Maxim, нет. Изменится лишь содержимое фрейма.

  • Maxim

    Не понимаю тогда. Можете пример привести?

  • Сча кину работающий искходник.

    • Maxim

      Доброго дня еще раз. Вы не подскажете, как в JS отловить результат загрузки?
      Пробовал так:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      
      function ResUpload() {
      		if ($('#upload_frame').has('div.result')) {
      			alert($('div.result').html());
      		} else {
      			setTimeout(function() {
      				if ($('#upload_frame').has('div.result')) {
      					alert($('#upload_frame').find('div.result').html());
      				} else {
      					setTimeout(function() {
      						ResUpload();
      					}, 1000)
      				}
      			}, 1000);
      		}
      	}

      Но не помогло.

      • Maxim, достучаться к содержимому айфрейма со странички где он находится невозможно — это продиктовано требованиями безопасности.

        То, что файл загрузился, можно узнать лишь потому что js-код, который отдаёт сервер в айфрейм, выполнился. Просто отдавайте js-код, который вы хотите выполнить после окончания загрузки файла, в айфрейм. В моём примере это код в файле upload.php:

        22
        23
        24
        25
        
        var name = "<?=$name; ?>";
        var mimeType = "<?=$mimeType; ?>";
        var size = <?=$size; ?>;
        alert(["Имя файла: ", name, "Тип: ", mimeType, "Размер: ", size].join('n'));

        P.S. Если нужен результат и самому писать не хочется, то можно воспользоваться плагином jQuery Form. Он сделает, по-сути, то же самое, только уже автоматически и без вашего ведома. К тому же, с jQuery уже, как вижу, вы знакомы.

        • Maxim

          В том то и проблема, что в фрейм вывести какой-либо JS-код не выйдет — только проблемы будут. Дело в том, что пишу плагин текстового редактора под jquery с загрузкой графических файлов. Поэтому Ваше предложение не подходит. Хотя, мне кажется, что я могу передавать ссылку на документ фрейма?

          • Не вижу никаких проблем с js-кодом в айфрейме. Это решения придумал не я и оно успешно используется, хотя бы в плагине jQuery Form. JavaScript-код в айфрейме выполняется абсолютно так же как и вне его. Единственные ограничения продиктованы политикой безопасности, ну и к переменным, который объявлены на родительской, по отношению к фрейму страничке, из фрейма нужно обращаться вот так:

            var variable = parent.variable;

            Хотите отдавать ссылку на документ в айфрейме — отдавайте. Это довольно оригинальное решение. Вот только пользователю открывать ещё одну вкладку для просмотра картинки не совсем удобно, по-моему.

            • Maxim

              Фрейм нужен только для загрузки файла на сервер. Никаких окон больше не будет. Все происходит на текущей странице, без перезагрузки.

              • А как пользователь узнает, что загрузка окончена?

                • Maxim

                  Вот для этого и надо считать ответ в этом фрейме. Если загрузка прошла успешно, то в редакторе появится загруженная картинка.

                  • О окончании загрузки в этом случае НИКАК нельзя узнать с родительской странички фрейма.

  • Maxim

    Ага, спасибо! Попробую.

  • Сергей

    КТ, слово раздача пишется через букву «зы»

  • Константин

    Как добится того что бы progressBar.value = e.loaded / e.total * 100; доходил до 100, а не до 95 как у Вас ???