Загрузка файлов на сервер с помощью Ajax

С реализацией браузерами спецификации HTML5 многие вещи в веб стало делать намного проще и приятнее. Одна из таких вещей - ajax-запросы в общем и загрузка файлов на сервер с помощью ajax в частности.
Итак, загружаем файл на сервер без перезагрузки страницы - версия с минимальным количеством кода. Используем jQuery и рассматриваем только клиентскую часть (без серверной пока).
Тема:

JavaScript

Дата публикации: 02.06.2015 19:06:47
10728 просмотров

На работе мы сейчас разрабатываем свою CRM-систему. Backend пишем на Yii. Особенностью работы сайта является то, что при переходе на другую страницу перехватывается событие клика по ссылке, отправляется запрос на сервер и, получив в ответ только нужный блок html-кода, перерисовывает DOM-структуру. В итоге все довольны: клиенты - быстрой работой сайта, админы - уменьшенной нагрузкой на сервер, программисты же просто радуются красоте решения.

Решили мы сделать загрузку файлов на сервер - как же без этого? Аватарки пользователей, прикрепленные документы, вложения в почте и т.д. Но в связи с тем, что сайт у нас полностью на Ajax, пришлось провести небольшой research по запросу "ajax загрузка файлов".

Выяснилось, что с этим все хорошо, спасибо спецификации html5. Главный недостаток для production-проектов - поддержка IE10+. Но мы решили пока не обращать внимание на пользователей с IE8,9 - если по статистике после запуска проекта увидим, что таких пользователей не так уж и мало, тогда сделаем решение и под IE8,9.

Сначала наткнулся на плагин Jquery Ajax Upload. Вроде бы хороший, красивенький, но, как и все фреймворки, тянет с собой много js и css-файлов, а так же заставляет, вместо написания кода, писать правильные конфиги, что ничуть не приятнее.

Я же решил реализовать все на jQuery (в проекте он все равно используется).

Для начала будем делать все как можно проще. Html-разметка будет очень простая

<input class="fileinput" data-url="'.$url_to_upload.'" name="'.$this->attribute.'" type="file" />

Это код с backend'а, формируем обычный input с type=file, в data-url указываем URL, по которому будем загружать картинку, ну и name содержит название input'a.

На клиенте у нас будет сформировано следующее

<input class="fileinput" data-url="/lead/ajaxFileUpload?id=12345" name="userpic" type="file" />

Как видно, данные input используется для загрузки аватарки для lead'а в CRM. В URL, на который мы будем отсылать нашу картинку, присутствует id лида.

Поехали дальше - js.

$('.fileinput').change(function(){
    var send_url = $(this).attr('data-url');
    var fd = new FormData();

    console.log(this.files);

    fd.append("userpic", this.files[0]);

    $.ajax({
        url: send_url,
        type: "POST",
        data: fd,
        processData: false,
        contentType: false
    });
});

Тут все максимально просто:

  • Ловим событие onchange для элементов с классом fileinput
  • Формируем данные для отсылки с помощью специального объекта JavaScript (введенного с появлением HTML5) FormData
  • С помощью jQuery.ajax отправляем данные посредством ajax (xhr-запроса).

Так же, для лучшего понимания работы скрипта, я специально оставил console.log(this.files); - можно посмотреть, какая структура данных по-умолчанию создается для html-элемента c type=file.

Но и тут я успел столкнуться с нюансом. Не зря я указал опцию processData: false. Без неё ничего не получилось, в консоли браузера выдавалась ошибка: Uncaught TypeError: Illegal invocation в Chrome и более осмысленная TypeError: 'append' called on an object that does not implement interface FormData. в Firefox (последняя киллер-фича Firebug'а как раз и заключается в том, что ошибки он выводит намного более осмысленные. Зачастую спасает.).

А все потому, что при proccessData = true jquery пытается сконвертировать все ajax-данные в одну строку. Естественно, для файла это не нужно, по-этому установкой данной опции в false мы избавим jQuery от мучений.

Решением, как вы догадались, является установка параметра processData в false. Теперь все заработало - в chrome's developer tools на вкладке Network можно увидеть, что запрос успешно отправился с данными файла. Отлично!

------WebKitFormBoundaryy90AgaMBI1x05pSx
Content-Disposition: form-data; name="userpic"; filename="test_image.jpg"
Content-Type: image/jpeg


------WebKitFormBoundaryy90AgaMBI1x05pSx--

Сервер пока возвращает 404 ошибку. Оно и понятно - я им вообще ещё не занимался, вот он и обиделся.

Серверная часть

На сервере, после настройки rout'ов и добавлении action'а в контроллер (это в каждом фреймворке делается по-разному), у нас появляется доступ к загруженному файлу через глобальный массив $_FILES. Мало того, php уже загрузил файл на сам сервере в папку tmp. Теперь можно делать с ним все, что душа пожелает. Но ограничиваются обычно лишь перемещением файла в более удобную директорию (tmp имеет свойство очищаться) и, возможно, добавлению записи в базу данных.

Собственно, по несложной команде var_dump($_FILES) у меня получился вот такой вывод:

array(1) {
  ["userpic"]=>
  array(5) {
    ["name"]=>
    string(37) "test_image.jpg"
    ["type"]=>
    string(10) "image/jpeg"
    ["tmp_name"]=>
    string(14) "/tmp/phpLsTgFK"
    ["error"]=>
    int(0)
    ["size"]=>
    int(44482)
  }
}

Бинго! Файл успешно загружен на сервер с помощью Ajax.

<input class="fileinput" data-url="'.$url_to_upload.'" name="'.$this->attribute.'" type="file" />

<input class="fileinput" data-url="/lead/ajaxFileUpload?id=12345" name="userpic" type="file" />

$('.fileinput').change(function(){
    var send_url = $(this).attr('data-url');
    var fd = new FormData();

    console.log(this.files);

    fd.append("userpic", this.files[0]);

    $.ajax({
        url: send_url,
        type: "POST",
        data: fd,
        processData: false,
        contentType: false
    });
});

------WebKitFormBoundaryy90AgaMBI1x05pSx
Content-Disposition: form-data; name="userpic"; filename="test_image.jpg"
Content-Type: image/jpeg


------WebKitFormBoundaryy90AgaMBI1x05pSx--

array(1) {
  ["userpic"]=>
  array(5) {
    ["name"]=>
    string(37) "test_image.jpg"
    ["type"]=>
    string(10) "image/jpeg"
    ["tmp_name"]=>
    string(14) "/tmp/phpLsTgFK"
    ["error"]=>
    int(0)
    ["size"]=>
    int(44482)
  }
}