Ajax на практике.

Загрузка файлов.

В прошлой статье, мы пробежались по нескольким основным методам для получения данных и их дальнейшей передаче AJAX-запросом. Теперь пришло время поговорить о том, как же можно загружать файлы с помощью AJAX. Еще до недавнего времени, способов загружать файлы без перезагрузки самой страницы, было не так уж и много (скрытый iframe, Flash). Они и сейчас используются по причине того, что еще остаются пользователи со старыми версиями браузеров, которых не коснулся прогресс. Но оглядываться назад не будем, посему шагаем в ногу со временем.

Рассмотрим, на мой взгляд, один из самых удобных способов для работы с файлами (и не только) - объект FormData. Пусть будет такая простенькая форма, для загрузки аватара пользователя:

HTML (файл index.html)

<form action="handler.php" method="post" id="my_form" enctype="multipart/form-data">
  <label for="fio">Ф.И.О:</label>
    <input type="text" name="fio" id="fio"><br>
  <label for="avatar">Аватар:</label>
    <input type="file" name="avatar" id="avatar"><br>
  <input type="submit" id="submit" value="Отправить">
</form>

Перейдем к JS-части. С полем "Ф.И.О" сложностей не будет и его используем только для наглядности того, что вместе с файлом, мы можем отправлять любые другие данные.

jQuery (файл script.js)

$(function(){
  $('#my_form').on('submit', function(e){
    e.preventDefault();
    var $that = $(this),
    formData = new FormData($that.get(0)); // создаем новый экземпляр объекта и передаем ему нашу форму (*)
    $.ajax({
      url: $that.attr('action'),
      type: $that.attr('method'),
      contentType: false, // важно - убираем форматирование данных по умолчанию
      processData: false, // важно - убираем преобразование строк по умолчанию
      data: formData,
      dataType: 'json',
      success: function(json){
      	if(json){
          $that.replaceWith(json);
        }
      }
    });
  });
});

(*)Обратите внимание на то, что передаем форму не объектом jQuery, а DOM-элемент

PHP-обработчик (файл handler.php)

<?php
if(isset($_POST['fio'],$_FILES['avatar'])){
  $req = false; // изначально переменная для "ответа" - false
  // Приведём полученную информацию в удобочитаемый вид
  ob_start();
  echo '<pre>';
  echo 'Имя пользователя: <strong>' , $_POST['fio'] , '</strong><br>Данные загруженного файла:<br>';	
  print_r($_FILES['avatar']);
  echo '</pre>';
  $req = ob_get_contents();
  ob_end_clean();
  echo json_encode($req); // вернем полученное в ответе
  exit;
}

Если всё было сделано правильно, то на экране нам выведится информация в таком виде:

Имя пользователя: Alex
Данные загруженного файла:
Array
(
  [name] => avatar.jpg
  [type] => image/jpeg
  [tmp_name] => E:\OpenServer\userdata\temp\php8627.tmp
  [error] => 0
  [size] => 6697
)

Ясное дело, что выводить эту информацию нам не потребуется, а нужно будет сохранять файл на сервере, указав ему место "постоянной прописки" ;). Как загружать/сохранять файлы средствами php, я описывать тут не буду, т.к. тема не маленькая и ей можно посветить отдельную статью. Для самых нетерпеливых, могу дать пару намёков - используем: move_uploaded_file(), getimagesize(), Fileinfo и другие полезные функции. И не забывайте, что очень желательно задавать файлам уникальные имена, т.к. при совпадении имён и расширений, старый файл будет попросту перезаписан новым.

"Замечтательно! - скажите вы. - А как же быть, если нужно загрузить несколько файлов одновременно?" Ничего сверхъестественного и существует несколько способов реализации:

  1. Используем атрибут multiple (в нашем случае, позволяет для одного поля input указывать несколько файлов). Добавляем этот атрибут в наше поле и изменяем его имя, добавив квадратные скобки "[]". Это укажет, что мы передаем массив данных из этого поля:
    <input type="file" name="avatar[]" id="avatar" multiple>
    Способ хороший, но к сожалению не все браузеры с ним дружат.
  2. Заранее подготовить несколько полей, которым так же можно указать одинаковые имена массивом:
    <input type="file" name="avatar[]">
    <input type="file" name="avatar[]">
    Это уже кроссбраузерный вариант, но и он имеет маленькие недостатки при определённых обстоятельствах. К примеру, мы создали два поля, а пользователю необходимо загрузить три и более файлов.
  3. Динамическое добавление полей. Тут мы создаем одно поле и добавляем какую-нибуть кнопку, по нажатию на которую, пользователь сможет добавлять необходимое кол-во дополнительный полей. Каким способом добавлять поля - дело вашего вкуса и фантазии. Можно заранее подготовить код поля в JS, присвоив его переменной, можно клонировать уже существующий элемент - clone() и т.д.

Во всех трёх вариантах, на стороне сервера, мы получим массив файлов и, как с любым другим массивом, обрабатываем его в цикле.

Ещё одним моментом, который сто́ит затронуть - это добавление файлов (и других данных) в FormData, если формы, как таковой, нету или данные берутся из других источников. Для этой задачи, будем использовать метод append(), который похож на jQuery-метод append(), но выполняет немного другой функционал. Синтаксис метода:

void append(DOMString name, File value, optional DOMString filename);
void append(DOMString name, Blob value, optional DOMString filename);
void append(DOMString name, DOMString value);
Где параметры:
name
Имя поля, данные которого передаются в параметре value. По сути, если мы взяли данные не из поля формы, у которого есть атрибут name, то мы этот name задаём сами.
value
Значение поля. Может быть типа Blob, File или string
filename
Необязательный параметр. Для типов Blob и File - имя файла, сообщаемое серверу

Возьмем нашу форму загрузки аватара без изменений и предположим, что мы хотим добавим к ней текущую дату. Теперь наш код должен выглядеть следующим образом:

$(function(){
  $('#my_form').on('submit', function(e){
    e.preventDefault();
    var $that = $(this),
    formData = new FormData($that.get(0));
    formData.append('date_upl', new Date()); // добавляем данные, не относящиеся к форме. У нас - это дата
    $.ajax({
      url: $that.attr('action'),
      type: $that.attr('method'),
      contentType: false,
      processData: false,
      data: formData,
      dataType: 'json',
      success: function(json){
      	if(json){
          $that.replaceWith(json);
        }
      }
    });
  });
});

Вуаля, собственно новые данные добавлены к набору передавемых на сервер. Наша дата будет в переменной $_POST['date_upl']. Для добавления еще одной и более пары "ключ - значение", используем ту же конструкцию. С файлами дело обстоит так же, но нужно учитывать, что значением является DOM-элемент, а не объект jQuery. И напомню, что абсолютно все данные, приходящие от клиента, должны в обязательном порядке подвергаться проверке, фильтрации, валидации и т.д.!


На этом можно было бы поставить многоточие, но не точку, т.к. тема обширна и всегда будут обстоятельства, которые потребуют нестандартных путей решения. Осталось еще пара небольших вопросов, которые относятся к данной теме, но я решил их вынести в отдельные статьи. Непосредственно по теме загрузки файлов - создание прогрессбара (индикатора выполнения загрузки на сервер) и Ajax на чистом (нативном) JS, без использования фреймворков.

Incode Pro logo

51 комментарий

Страница 2 из 3  
Incode 07.01.2015 21:36
А у меня просто без [] получается
Если использовать FormData, то не получится, т.к. данные из формы собираются "как есть", а значит, если даже установить атрибут multiple, но не указать, массив, то на серевр доедет только один файл. Вы, скорее всего, подготавливаете файлы поотдельности перед запросом, поэтому указание массива - не обязательно.
Гость 01.04.2015 14:01
Большое спасибо за Вашу статью! Очень пригодилась. Как всё просто, оказывается.
sash 21.04.2015 16:36
"Замечтательно! - скажите вы
Не знал что можно без фреймворков типа ajaxupload загружать файлы
Incode 21.04.2015 17:59
@sash, плагин "типа ajaxupload", просто добавляет кроссбраузерности (что можно реализовать и тут) и может иметь какие-то дополнительные "плюшки", которыми чаще всего не пользуются, а вес они прибавляют.
sash 21.04.2015 18:21
а вес они прибавляют.

Вот по этому поводу как раз хочу поговорить. Неужели эти несчастные килобайты реально могут затормозить сайтик? Ну на сколько дольше он будет грузиться? На миллисекунду? Сейчас у большинства нормальные машины, вот мой сайтик на компе грузиться моментально, хотя там девять картинок по 300-1000 кб каждая.
Incode 21.04.2015 19:14
@sash, из несчастных килобайтов, порой образуются несчастные мегабайты. Можно в квартире хранить и стопку кирпичей. Да, поместятся в любой квартире, да займут не много места, если их правильно сложить, но есть ли от этого хоть малейшая польза? И даже не в размере файла может быть проблема. Всего пара строк кода, может повесить браузер напрочь.
sash 21.04.2015 19:25
@Incode, категорически согласен, но по слухам, есть такие люди что тратят дни на написание яваскрипт кода, лишь бы джейквери не подключать ))
Incode 21.04.2015 19:40
@sash, если используется всего пара методов из jQuery, которые к тому же элементарно написать на чистом JS, то подключать из-за этого библиотеку - преступление )) Но чаще всего, в проекте используются десятки методов и тут уже "игра сто́ит свеч". К тому же, не факт, что программер среднего, а тем более ниже уровня, сможет написать на чистом JS лучше, чем это реализовано в библиотеке.
sash 22.04.2015 17:23
@Incode, Как сказал один великий человек - я не умею и не буду:) Мы ещё и UI подключим.. мы преступники, мы вне закона)))
Такое только тут может быть, что сначала изучается jQuery, а потом чистый яваскрипт
Гость 30.04.2015 13:43
При отправке формы на электронный адрес, сообщение дублируется, при отправке без jaxa, сообщение доставляется только в 1 экземпляре, в чем может быть причина?
Incode 30.04.2015 15:27
При отправке формы на электронный адрес, сообщение дублируется
Первое, что можно предположить - это то, что не отменено событие формы (submit). В этом случае, отправляются данные ajax-запросом и обычным методом передачи данных из формы. Убедитесь, что страница не перезагружается и отменено действие по умолчанию - event.preventDefault(). Если у вас всё верно, то дайте ссылку на страницу этой формы или покажите код.
Гость 10.06.2015 11:53
Хорошая статья, спасибо!
Единственное, что бросилось в глаза - у функции print_r есть второй параметр

bool $return = false
. Установив его в true можно отказаться от ob_* функций и код станет лучше :)

Я недавно написал похожую статью про ajax загрузку файлов - ajax загрузка файлов на сервер. Отличается тем, что файлы мы вручную скармливаем formData, но зато обходимся без <form>.
Incode 14.06.2015 18:31
Единственное, что бросилось в глаза...
По сути, статья не посвещена серверной части, но всё равно, народу будет полезно. Спасибо ;)
Отличается тем, что файлы мы вручную скармливаем formData
Отлично. Я всегда считал, что зная какой-то метод, можно придумать массу способов его использования, но если кто-то и не сможет додуматься, то ссылка на вашу статью - будет хорошим дополнением.
Гость 14.06.2015 23:01
Я вроде бы вставлял ссылку, но что-то пошло не так :D

Попытка №2: ajax загрузка файлов на сервер
Гость 14.06.2015 23:09
Не вставляется(
Может быть потому, что в ней есть кириллические символы?
Incode 15.06.2015 00:26
Не вставляется
0_o Как это не вставляется? Уже две ссылки есть: четырмя ответами выше и двумя :D Текстовое описание "ajax загрузка файлов на сервер" и ведут ссылки на вашу статью :)
Гость 15.06.2015 00:46
Я понял. При включенном AdBlock не работает. Выключил - появилась ссылка.

Но при этом с сервера приходит html-код без ссылки, она потом вставляется js'ом. Занятный seo-метод, что скажешь.. )
Гость 16.07.2015 00:28
echo json_encode($req);

Это не обязательно в json кодировать. Вы же строку получаете и на клиенте эту же строку вместо формы вставляете. Т.е. echo $req; вполне сойдёт. Ну и на клиенте вместо dataType: 'json' поставить dataType: 'text'
Incode 16.07.2015 02:12
Это не обязательно в json кодировать.
Я и не говорил, что обязательно. Однако, передача данных в виде json-строки - достаточно неплохая практика. К примеру, в проекте вам чаще всего может понадобится возвращать ответом как строки, так и массивы данных. Последние, по понятным причинам, придется передавать в json-закодированной строке. Поэтому удобней указать один раз в $.ajaxSetup() тип данный ожидаемых в ответе, как "json", что подходит для большинства форматов, ну и в исключительных случая, как, например, с xml, указывать dataType отдельно к соответствующему запросу.
Гость 14.08.2015 21:49
Отличная статья

только хотелось бы информацию как formData заменить IE 8
Incode 14.08.2015 23:06
как formData заменить IE 8
FormData поддерживается только начиная с IE10. Для старых ослов используют загрузку через Flash или скрытый iframe, о чём, собственно, я и писал в начале статьи. В принципе, последнего вполне хватает, если не нужны какие-то "плюшки" в виде прогрессбара и т.п. Проверяем браузер на соответствие технологиям.
if (window.FormData !== undefined) {
    // используем FormData для нормальных браузеров.
}
Для остальных - обычная загрузка через iframe.
Гость 13.01.2016 14:45
при загрузке файла свыше ~500-600 кб
появляется ошибка [error] => 3 (файл частично закачан)
в логах апача (2.4) чисто, в js консоли браузера тоже.
php 5.6.17
в папку права есть.
Array
(
[name] => 1.txt
[type] =>
[tmp_name] =>
[error] => 3
[size] => 0
)
на диске место есть, также квоты в php.ini точно не ограничивают закачку 600 кб
Incode 13.01.2016 16:12
появляется ошибка [error] => 3 (файл частично закачан)
А вы уверены, что загрузка не была прервана? Ну, например, нажатием Esc?
Гость 16.03.2016 15:38
Привет! Спасибо тебе за твой пример. Я долго сам разбирался с ajax и за прошлые пару дней, хорошо продвинулся в понимании этой технологии. Есть пару вопросов, которые пока остались не ясны.

Я раньше всегда использовал для сбора данных с формы функцию .serialize(); и потом уже отправлял данные на сервер.


$(".clock").click(function(){
   formData= $(this).serialize();
   
   $.ajax({
      dataType: 'json',
      method: 'POST',
      data: formData,
      ...
   });
})


А тут, когда дело дошло до передачи файлов, получилось так, что в $_POST данные приходят, а массив $_FILES остается пустым. Почему так получается не особо понимаю.

1. Объясните вкратце, почему используется именно такой подход - создание объекта и прикрепление к нему данных? Почему объект создается с DOM элементом внутри вместо серриализованных данных?

2. Что происходит, когда данные передаются ajax в php? Понятно что данные типа ("key" : "value") приходят, как часть массива $_POST, а как воспринимается DOM элемент, который мы туда отправляем? Как данные о файле оказываются в $_FILES ?

Дополнительно скажу, что можно не писать $that = $(this), formData = new formData ($that.get(0)); А можно просто написать formData = new formData(this). Если не писать $ - то this и будет DOM объект по которому вы кликнули.
Incode 16.03.2016 18:49
почему используется именно такой подход
Вопрос "почему" - это скорее не ко мне, а к разработчикам ))
как воспринимается DOM элемент, который мы туда отправляем?
Вопрос как-то некорректно поставлен. Если мы и отправим DOM-элемент на сервер, то только в виде строки.
Как данные о файле оказываются в $_FILES ?

Ajax-запрос практически ничем не отличается от обычного запроса, только тем, что передача данных происходит в "фоновом режиме". У вас же не возникает вопросов, почему при обычном запросе данные файлов оказываются в глобальной переменной $_FILES? Использование объекта FormData позволяет получить на выходе данные в таком формате, как если бы мы отправляли обыкновенную форму с encoding установленным в "multipart/form-data". Если вы помните, то именно такой способ кодирования (атрибут формы enctype="multipart/form-data") и требуется указывать, если собираетесь загружать файлы.
Дополнительно скажу, что можно не писать $that = $(this)
Абсолютно правильно, но таким способом, я акцентировал внимание на том, что передается не объект jQuery, а элемент страницы (DOM-элемент). Кроме того, не всегда обработчик устанавливают на событие "submit", а какое-нибудь другое. И в этом случае, ссылка на объект (this), уже будет совершенно не на форму.
Страница 2 из 3  
Ваш комментарий:
X