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

50 комментар

Страница 1 из 2  
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), уже будет совершенно не на форму.
Гость 01.06.2016 00:01
а слабо без перезагрузки страницы?
Incode 01.06.2016 01:37
а слабо без перезагрузки страницы?
Крайне непонятно к чему этот вопрос и кому он адресован. Это всё равно, что под статьёй "Как разжечь костер без спичек", спросить: "А слабо без спичек разжечь?". Вы наверно удивитесь, но ajax таки и предназначен для передачи данных между клиентом и сервером без перезагрузки страницы (Ваш К.О.)
Гость 17.07.2016 00:07
e.preventDefault();

Что это значит?
Incode 17.07.2016 11:04
Что это значит?
Отмена действия события по умолчанию. Читаем
Гость 31.07.2016 22:02
Работает только в utf-8 и перезагружает страницу, вернее, открывает другую - handler.php
Для работы не подходит, в модальном окне вывод результата не сделать.
Гость 17.10.2016 17:19
Спасибо за статью, очень помогла!
Гость 24.11.2016 14:46
Работает только в utf-8 и перезагружает страницу, вернее, открывает другую - handler.php
Для работы не подходит, в модальном окне вывод результата не сделать.


Ты делаешь что то не то)
Гость 26.12.2016 11:29
Спасибо. Все работает. Подошло к моей задаче
Гость 08.02.2017 12:01
Наконец то нашел то что искал
Гость 20.03.2017 14:42
Интересная статья, Также полезно будет почитать, https://learn.javascript.ru/xhr-forms
Гость 11.05.2017 09:20
Спасибо большое! Все работает!
Гость 26.08.2017 13:55
Как понять что это $that.get(0) ?
Гость 12.01.2018 21:40
Как понять что это $that.get(0) ?
Изучаю недавно, но понял так, что метод get возвращает элемент по интексу. Формдата не работает с объектами jQuery, а тут get возвращает, как в обычном js this, т.е. ссылку на элемент формы.
Гость 17.03.2018 15:51
Я правильно понимаю, что загрузить файл на сервер с помощью Ajax можно только через объект FormData, а использование сериализации значений полей здесь не поможет? Или нет?
Гость 17.03.2018 15:52
имел ввиду как в примере из предыдущей статьи
Incode 17.03.2018 15:59
Я правильно понимаю
Правильно
Гость 17.03.2018 17:04
Спасибо!
и еще, я так понял, что проблема с использования FormData в IE это поддержка метода get().
перебрав поля формы вручную в цикле и добавив из объекту FormData можно использовать и в IE? И загрузка файлов через FormData в IE поддерживается?
Incode 17.03.2018 17:13
в IE поддерживается?
Поддержку браузерами различных технологий, методов, свойств и т.п., можно проверять на ресурсе caniuse.com. Все современные браузеры не имеют проблем с FormData, а что касается ослов старых версий, то тут вариантов не много: или скрытый iframe, или отправка данных стандартным способом. Впрочем, для "деревянных" браузеров - деревянные методы.
Гость 19.03.2018 09:08
О чём вы вообще...
Данный пример банально не работает , открывается hendler.php причём в нём отображается php код и всё... Браузер firefox-54 файлы скачаны с сайта. Что может быть "не так" ???
Incode 19.03.2018 11:13
О чём вы вообще...
О технологиях, которые используются во всём цивилизованном мире ;)
Данный пример банально не работает
Нужно добавлять: "лично у меня не работает".
Что может быть "не так" ?
С этого нужно и начинать. Причин не так много, но нужна дополнительная информация от вас: на каком сервере тестировали, какой браузер используете, есть ли какие-то ошибки в консоли или серверные ошибки и т.д.
Гость 19.03.2018 12:58
Я не хотел сказать, что это вообще нерабочий код... Пример "банально заработал", когда папки были загружены в сайт на локальном сервере.
Он не работал (и не работает) почему-то когда скачанные файлы в папке "загрузки" на компе. Кстати ранее кто-то тоже писал, что у него открывается hendler.php и всё... Так что не я один такой тупой. Не пойму почему....(в смысле не работает - а не почему тупой.. с этим то как раз всё ясно...)
Гость 23.03.2018 17:48
Спасибо. Буду знать такой способ. Но зачем прописывать все эти атрибуты для формы? Можно даже не юзать сабмит.
Такая давняя статья, свежего в принципе давно здесь ничего не появляется, но всё ещё отвечаете на вопросы. Это здорово
Гость 15.11.2018 11:41
Доброго времени суток!
Пример, действительно, то что нужно! Все работает...
Работает передача файла. Он падает в /tmp а дальше он должен быть перенесен в положенное место, а вот этого не происходит. А куда в handler.php вставить move_upload_file я ни как не соображу.
Incode 15.11.2018 21:00
А куда в handler.php вставить move_upload_file я ни как не соображу.
Вы уверены, что проблема не в функции? Функции "move_upload_file" нет, но есть move_uploaded_file. Использовать её можно в любой части кода, лишь бы до этого момента переменная $_FILES['some_file_key'] была "жива".
if(move_uploaded_file($_FILES['some_file_key']['tmp_name'], 'path/to/destination/img.jpg')) {
    // Ok
}
Вместо some_file_key, естественно, что ваше реальное значение.
Страница 1 из 2  
Ваш комментарий:
X