Pull to refresh

HTML5 File API: множественная загрузка файлов на сервер

Reading time 7 min
Views 86K
Когда передо мной в очередной раз встала задача об одновременной загрузке нескольких файлов на сервер (без перезагрузки страницы, само собой), я стал блуждать по интернетам в поисках довольно корявого jQuery-плагина, который позволяет имитировать ajax-загрузку файла (того самого плагина, который со скрытым фрэймом: от java- и flash- плагинов сразу было решено отказаться). В процессе поиска я вспомнил, что в грядущем стандарте html 5 возможности по работе с файлами должны быть существенно расширены, и часть этих возможностей доступна уже сейчас. В итоге было решено опробовать их в действии.

Рассматривать возможности File API будем на примере одновременной загрузки нескольких картинок на сервер. В конце статьи приводится готовое решение, оформленное в виде jQuery-плагина.


Не хочу это читать, интересует готовое решение.

Итак, какие же преимущества дает нам использование File API:
  1. Независимость от внешних плагинов
  2. Возможность контролировать процесс загрузки и отображать информацию о нем (прогрессбар всегда добавляет терпения пользователю)
  3. Возможность прочитать файл и узнать его размер до начала загрузки (в нашем примере это дает нам возможность отсеять файлы, не содержащие изображений и показывать миниатюры картинок)
  4. Возможность выбрать сразу несколько файлов через стандартное поле выбора файла
  5. Возможность использовать интерфейс drag and drop для выбора файлов. Да-да, мы сможем перетаскивать файлы для загрузки прямо с рабочего стола или, например, из проводника!

Из недостатков можно отметить только недостаточную поддержку в браузерах. Сейчас File API поддерживают только Firefox ≥ 3.6 и Chrome ≥ 6.0. Есть такое ощущение, что и Safari уже совсем скоро подтянется, а вот про IE и Opera пока ничего не ясно (может быть, кто-то располагает сведениями?). Расстроило конечно, что File API не поддерживает IE9 Beta: это странно, учитывая что разработчики IE сейчас взяли курс на обильную поддержку html 5. Но как бы то ни было, очевидно, что в будущем всем браузерам придется подтянуться.

Работающий пример можно увидеть по адресу http://safron.pro/playground/html5uploader/, ниже приведены только наиболее важные фрагменты кода.

Для начала разберемся с html-кодом. Нам понадобится дефолтный элемент input, контейнер для перетаскивания файлов и список ul, куда мы будем помещать миниатюрки изображений:
<div>
 <input type="file" name="file" id="file-field" multiple="true" />
</div>
<div id="img-container">
 <ul id="img-list"></ul>
</div>


Ничего особенного, кроме того, что для элемента input указан атрибут multiple="true". Это необходимо для того, чтобы в стандартном диалоге выбора файлов можно было выделять их сразу несколько. Кстати, начиная с Firefox 4, разработчики браузера обещают, что ненавистные многим верстальщикам стандартные поля выбора файла можно будет скрывать, а диалог показывать, вызвав событие click для скрытого элемента.

Теперь перейдем к JavaScript (обратите внимание, что я использовал jQuery для упрощения манипуляций с DOM. Тот, кто по каким-либо причинам захочет отказаться от jQuery, сможет без труда переделать скрипты таким образом, чтобы обойтись без него). Сначала сохраним в переменных ссылки на html-элементы, снявшиеся в главных ролях. Далее определим обработчики событий для стандартного поля выбора файлов и для области, куда можно будет перетаскивать файлы.
  // Стандарный input для файлов
  var fileInput = $('#file-field');
  
  // ul-список, содержащий миниатюрки выбранных файлов
  var imgList = $('ul#img-list');
  
  // Контейнер, куда можно помещать файлы методом drag and drop
  var dropBox = $('#img-container');
 
  // Обработка события выбора файлов в стандартном поле
  fileInput.bind({
    change: function() {
      displayFiles(this.files);
    }
  });
     
  // Обработка событий drag and drop при перетаскивании файлов на элемент dropBox
  dropBox.bind({
    dragenter: function() {
      $(this).addClass('highlighted');
      return false;
    },
    dragover: function() {
      return false;
    },
    dragleave: function() {
      $(this).removeClass('highlighted');
      return false;
    },
    drop: function(e) {
      var dt = e.originalEvent.dataTransfer;
      displayFiles(dt.files);
      return false;
    }
  });


И в том и в другом случае в обработчике мы получаем доступ к объекту FileList, который по сути представляет собой массив объектов File. Этот массив передается функции displayFiles(), текст которой приведен ниже.
  function displayFiles(files) {
    $.each(files, function(i, file) {      
      if (!file.type.match(/image.*/)) {
        // Отсеиваем не картинки
        return true;
      }           
      // Создаем элемент li и помещаем в него название, миниатюру и progress bar,
      // а также создаем ему свойство file, куда помещаем объект File (при загрузке понадобится)
      var li = $('<li/>').appendTo(imgList);
      $('<div/>').text(file.name).appendTo(li);
      var img = $('<img/>').appendTo(li);
      $('<div/>').addClass('progress').text('0%').appendTo(li);
      li.get(0).file = file;
 
      // Создаем объект FileReader и по завершении чтения файла, отображаем миниатюру и обновляем
      // инфу обо всех файлах
      var reader = new FileReader();
      reader.onload = (function(aImg) {
        return function(e) {
          aImg.attr('src', e.target.result);
          aImg.attr('width', 150);
          /* ... обновляем инфу о выбранных файлах ... */
        };
      })(img);
      
      reader.readAsDataURL(file);
    });
  }


Объект File содержит метаданные о файле, такие как его имя, размер и тип (в формате MIME, например, image/gif) соответственно в свойствах name, size и type. Для доступа же к содержимому файла существует специальный объект FileReader.

Внутри функции displayFiles() мы проходимся по переданному массиву файлов и сначала отсеиваем те, которые не являются изображениями. Далее для каждого изображения создается элемент списка li, куда помещается пустой пока элемент img (обратите внимание, что в кажом элементе li также создается свойство file, содержащее соответствующий объект). После чего создается экземпляр FileReader и для него определяется обработчик onload, в котором данные передаются прямо в атрибут src созданного ранее элемента img. Метод readAsDataURL() объекта FileReader принимает параметром объект File и запускает чтение данных из него. В результате для всех выбранных через стандартное поле или перетащенных прямо в браузер картинок, мы видим их миниатюры (искусственно уменьшенные до 150 пикселей).

Что еще осталось сделать? Осталось только реализовать саму загрузку всех выбранных файлов на сервер. Для этого создадим какую-нибудь кнопку или ссылку, при нажатии на которую останется только пробежаться по всем созданным элементам li, прочитать их свойство file и передать в функцию uploadFile(), текст которой приведен ниже. Отмечу, что здесь для упрощения я реализовал загрузку через функцию, а в реальном примере, расположенном по адресу http://safron.pro/playground/html5uploader/, я собрал все действия по загрузке в объект uploaderObject, при создании которого можно передать дополнительные параметры, такие как функции обратного вызова для получения информации о процессе загрузки.
function uploadFile(file, url) {
  
  var reader = new FileReader();
 
  reader.onload = function() {    
    var xhr = new XMLHttpRequest();    
    
    xhr.upload.addEventListener("progress", function(e) {
      if (e.lengthComputable) {
        var progress = (e.loaded * 100) / e.total;
        /* ... обновляем инфу о процессе загрузки ... */
      }
    }, false);
    
    /* ... можно обрабатывать еще события load и error объекта xhr.upload ... */
 
    xhr.onreadystatechange = function () {
      if (this.readyState == 4) {
        if(this.status == 200) {
          /* ... все ок! смотрим в this.responseText ... */
        } else {
          /* ... ошибка! ... */
        }
      }
    };
    
    xhr.open("POST", url);
    var boundary = "xxxxxxxxx";    
    // Устанавливаем заголовки
    xhr.setRequestHeader("Content-Type", "multipart/form-data, boundary="+boundary);
    xhr.setRequestHeader("Cache-Control", "no-cache");    
    // Формируем тело запроса
    var body = "--" + boundary + "\r\n";
    body += "Content-Disposition: form-data; name='myFile'; filename='" + file.name + "'\r\n";
    body += "Content-Type: application/octet-stream\r\n\r\n";
    body += reader.result + "\r\n";
    body += "--" + boundary + "--";
 
    if(xhr.sendAsBinary) {
      // только для firefox
      xhr.sendAsBinary(body);
    } else {
      // chrome (так гласит спецификация W3C)
      xhr.send(body);
    }
  };
  // Читаем файл
  reader.readAsBinaryString(file);
}


Здесь создается экземпляр уже знакомого нам объекта FileReader, точно так же, как и выше; ему присваивается обработчик события onload, в котором создается XMLHttpRequest (к сожалению, пока нельзя воспользоваться ajax-интерфейсом jQuery, поскольку там еще не предусмотрена загрузка файлов). В XMLHttpRequest второй версии появилось свойство upload, содержащее объект-загрузчик, который может обрабатывать события progress, load и error (подробнее см. http://www.w3.org/TR/XMLHttpRequest2/#xmlhttprequesteventtarget). В примере выше показана только обработка события progress. Далее присваиваем обработчик завершения запроса самому реквесту (в отличие от событий объекта-загрузчика он вызывается уже тогда, когда все данные загружены и ответ от сервера получен), добавляем два дополнительных заголовка и формируем тело запроса, читая данные из свойства result объекта FileReader. После этого загрузка запускается. Отмечу только, что по нынешней спецификации W3C подразумевается, что метод send() объекта XMLHttpRequest может принимать в параметре бинарные данные, что успешно и реализовано в Google Chrome, однако в Firefox сделано по-своему, через особый метод sendAsBinary(). Поэтому перед началом отправки проверяем, определен ли метод sendAsBinary() в объекте реквеста, и, если да, используем его.

Вот, собственно, и все. С нетерпением ждем утверждения и распространения html 5!

Кое-какие ссылки

  1. http://safron.pro/playground/html5uploader/ — работающий пример того, что описывалось выше (плюс еще кое-что)
  2. http://safron.pro/playground/html5uploader/full.zip — весь код целиком в архиве
  3. http://html5test.com — проверка браузеров на соответсвие html 5 (очень наглядно)
  4. http://playground.html5rocks.com — площадка для экпериментов с кодом от Google (ее интерфейс будет знаком тем, кто использовал многочисленные API Google)


UPD
Для упрощения использования всего вышеизложенного, был создан JQuery-плагин. При помощи него можно загружать файлы через File API там, где это возможно, и реализовать замену (например, обычную отправку формы) там, где нет. По просьбам трудящихся и соотносясь с замечаниями комментаторов, была добавлена загрузка через объект FormData в браузерах, которые его поддерживают (Chrome, Safari 5+, FF 4+). В самом верху файла с плагином есть описание параметров, методов, а также краткие примеры использования. Более полный пример использования можно увидеть здесь (это изначальный пример из этой статьи, только переделанный на использование плагина, его полный код, включая серверную часть, можно скачать здесь [see UPD2]).

Использованные источники

  1. https://developer.mozilla.org/en/using_files_from_web_applications — статья о файловом интерфейсе на сайте девелоперов Mozilla
  2. https://developer.mozilla.org/En/XMLHttpRequest/Using_XMLHttpRequest — cтатья об использовании XMLHttpRequest там же
  3. http://www.w3.org/TR/FileAPI/ — текущая спецификация File API на сайте W3C
  4. http://www.w3.org/TR/XMLHttpRequest2/ — текущая спецификация XMLHttpRequest там же



UPD2
По просьбе пользователя glebovgin плагин был доработан таким образом, чтобы можно было отправлять не только непосредственно объект File, но также Blob-данные (объект Blob). Это может быть полезно, если есть необходимость отправлять на сервер, например, содержимое canvas, ну или просто вручную сгенерированные данные.

В демке (которая переехала немного на другой адрес) был добавлен пример отправки картинки из canvas. На данный момент эта возможность работает в FF, Chrome, IE10.

Исходный код ныне доступен на GitHub. Замечания, предложения, улучшения приветствуются!
Tags:
Hubs:
+97
Comments 57
Comments Comments 57

Articles