Пользователь
0,0
рейтинг
30 ноября 2010 в 16:11

Разработка → HTML5 File API: множественная загрузка файлов на сервер из песочницы

Когда передо мной в очередной раз встала задача об одновременной загрузке нескольких файлов на сервер (без перезагрузки страницы, само собой), я стал блуждать по интернетам в поисках довольно корявого 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.su/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.su/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.su/playground/html5uploader/ — работающий пример того, что описывалось выше (плюс еще кое-что)
  2. http://safron.su/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. Замечания, предложения, улучшения приветствуются!
Саша Сафронов @safron
карма
54,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (57)

  • +1
    Очень вовремя! Вот только собирался использовать мультиаплоад в своем проекте и вот замечательная статья.
    Довольно четко и очень понятно объяснено.
    Огромное спасибо.
    • +1
      на здоровие ;)
    • 0
      Как вы собираетесь реализовать поддержку старых браузеров?
      • 0
        забить?)
  • +1
    Изящное решение на html5 + вечный грубый костыль со скрытым вреймом для IE6-8…
  • 0
    Эх, жаль что это можно будет использовать только года через 3-4 когда умрут все браузеры которые не понимают html5
    • +2
      Не скажите, смотря для каких целей. Например, в данном случае я делал интерфейс для одной почтенной женщины-фотографа, она использует (а если бы и не использовала, то можно было бы убедить) файрфокс и ее все устраивает.

      Или вот пример с gmail — они же отказались от поддержки IE6, хотя его до сих пор используют 15 (или даже 20) % пользователей, которые получают предупреждение, что их браузер устарел.

      Тут два варианта: делать костыли для более старых браузеров и честно говорить пользователям, что надо обновить браузер.

      Но это я так. Согласен, чтоэту технологию пока рано запускать в массы. Как минимум надо дождаться, когда все последние версии браузеров будут ее поддерживать.
    • +2
      Зря Вы так. Для массового пользователя — да, а вот для своего удобства — уже сейчас.
    • 0
      Мало по малому, но уже можно начинать внедрять
  • +1
    К сожалению в реальных проектах придется использовать еще и альтернативный вариант загрузки, пока такой функционал не будут поддерживать основные броузеры :(
  • +17
    Дедушка Мороз, я хорошо себя вел в этом году, пожалуйста, сделай так чтобы IE не стало…
  • НЛО прилетело и опубликовало эту надпись здесь
  • +1
    Расстроило конечно, что File API не поддерживает IE9 Beta: это странно, учитывая что разработчики IE сейчас взяли курс на обильную поддержку html 5. Но как бы то ни было, очевидно, что в будущем всем браузерам придется подтянуться.
    Ничего странного, все просто. File API — черновик, его еще будут дописывать и переписывать. Если бы в IE9 внедрили FileAPI в текущем виде, то ничего хорошего не вышло бы. А мозиловцы вообще любят всякую экспериментальщину внедрять. Отсюда и остался в FF 3.6 метод sendAsBinary. В 4-ке уже должен быть FormData, который и описывается в последней версии черновика.
    • 0
      Согласен, весь HTML5 пока черновик, но ничего плохого в том, если бы ИЕ поддерживал уже тоже не было бы. Вообще, остальные браузеры регулярно обновляют младшие версии (может, подверсии? как правильно?), добавляя новые возможности. А уж если в ИЕ какой-то возможности нет, то еще год-другой надо ждать, пока выйдет следующий.
      • 0
        Вот в этом-то и проблема. Если Хром, FF, Опера обновляются достаточно часто, то IE — крайне редко. Поэтому внедрять что-то экспериментальное в IE опасно, т.к. может так случиться, что через неделю после релиза черновик перепишут, выпустят новые версии браузеров и… IE останется не у дел, со старыми реализациями (и снова хаки для IE). Разработчики оперы тоже не любят экспериментальные фичи. Вспомнить хотя бы border-radius (и многие другие css3 свойства), реализовывать которые Opera Software не спешили.

        А вообще, реализовывать такой функционал следует только при уверенности, что Вы сможете среагировать на изменение стандарта. Если Вы работаете над сайтом, который Вы точно будете поддерживать через полгода-год, то не страшно. А если Вы работаете над заказом, то лучше на это не рассчитывать. Иначе вместе с очередным обновлением браузеров может умереть весь новый функционал, а исполнителя и след простыл.
        • +1
          Ну ок, убедили — хорошо, что ИЕ пока не поддерживает :)
        • +1
          По моему IE как был не удел так и будет.
        • 0
          Если Хром, FF, Опера обновляются достаточно часто, то IE — крайне редко
          а так нечестно, да
  • +1
    Для загрузки картинок (небольших файлов) ваш код условно подходит.
    По-нормальному, надо без извращений с ручным формированием тела запроса использовать FormData
    • 0
      правильно, но к сож. файрфокс пока не поддерживает. Выйдет официальо 4й — переделаем!
    • 0
      Переписал код так, чтоб где это возможно использовался FormData. Если интересно, см. апдейт внизу статьи.
  • 0
    вовремя… надо бы jquery plugin заделать
    • 0
      сделаете? покажете?
      • –1
        искал добровольцев :)
    • 0
      Сделано! Если интересно, см. апдейт к статье.
  • 0
    Спасибо! Применю на своем проекте, как раз не будет лишним)

    • +1
      и да здравствует HTML5 в W3C
      www.w3.org/TR/FileAPI/ — текущая спецификация File API на сайте W3C

  • 0
    Для скрытия инпута можно просто сделать ему visibility:hidden и растянуть поверх графического элемента
  • 0
    М… меня опередили *сворачивает черновик* =)
  • 0
    Последний год использовал www.plupload.com/.
    Опенсурс библиотека для загрузки файлов. Поставил на сайт — если у пользователя оказывался не хтмл5-браузер, автоматически библиотека переключала на другой способ загрузки.
    Апи очень простое, рекомендую.
    • 0
      Спасибо за линк! Это громоздкое, но зато комплексное решение по загрузке файлов. И, насколько я понял, оно уже использует html 5 там, где это возможно
      • 0
        Да, так и есть. Более того, через апи самому можно выставлять приоритет способов загрузки. Я ранее ставил так: gears -> html5 -> flash -> silverlight -> html4.
        • 0
          ок, спс за инфо. Я гуглил готовые решения перед тем как сделать свой вариант, но на это не натыкался.
  • 0
    Ссылка по теме: bolknote.ru/2009/11/30/~2322
  • 0
    На правах рекламы:
    code.google.com/p/jquery-html5-upload/
  • +2
    Спасибо
    вместо $('ul#img-list') используйте $('#img-list'). Во втором случае получим элемент намного быстрей, так как будет использоваться родной document.getElementById
    • 0
      спасибо за инфо. Это, знаете ли, просто привычка, чтоб в коде видеть, какой именно элемент имеет такой айди (надо в комментариях писать — никак не могу переучиться). Потом везде убрал, а тут забыл.
  • 0
    Сегодня весь вечер внедрял загрузку по этому посту и нашел очень неприятную штуковину.
    В частности, этот код:
    if (self.xhr.sendAsBinary) {
        // firefox
        self.xhr.sendAsBinary(body);
    } else {
        // chrome (W3C spec.)
        self.xhr.send(body);
    }

    > метод send() объекта XMLHttpRequest может принимать в параметре бинарные данные, что успешно и реализовано в Google Chrome
    Попробовал на chrome 7 и trunk chromium 9 — они не смогли. Сейчас думаю о возможных решениях проблемы, первое, что пришло на ум — это реализация метода sendAsBinary с помощью FileWriter API. Это апи разработчики совсем недавно добавили в хромиум и firefox. Вот код:
    if (XMLHttpRequest.prototype.sendAsBinary === undefined && Uint8Array) {
        XMLHttpRequest.prototype.sendAsBinary = function(data) {
            var blob = new BlobBuilder(),
                arrb = new ArrayBuffer(data.length),
                ui8a = new Uint8Array(arrb, 0);
            for (var i=0; i<data.length; i++) {
                ui8a[i] = (data.charCodeAt(i) & 0xff);
            }
            blob.append(arrb);
            var blob = blob.getBlob();
            this.send(blob);
        }
    }
    • +1
      А если взять reader.readAsDataURL,
      отсечь от начала «data:image/png;base64,»
      разбить оставшися текст на строки по 74 символов каждая (включая, \r\n),
      Поставить хэдер Content-Transfer-Encoding: base64

      и отправить?
      • 0
        А почему по 72 именно?
        • 0
          74*
          • 0
            просто я отправил из почтовой программы Thunderbird себе письмо с вложенным файлом.
            Потом я посмотрел исходный код письма.
            Так вот, Thunderbird закодировал файл в строку base64 и после каждого 72 символа он ставил
            \r\n (либо просто \n)
            Из-за этого подумал, что 74 — это максимальная длина строки в письмах.
            ===
            P.S. Но вот сейчас взял первое попавшееся письмо на gmail, выбрал «Показать оригинал».
            Увидел, что там в строке base64 76 печатных символов.

            В rfc искал чего-то ничего по этому поводу не нашёл.

  • +1
    Хорошая статья, спасибо.
    Но у способа есть минус: большие файлы загружаются в память, браузер висит и отжирает много памяти.
    • 0
      Как справедливо отметил выше товарищ Demetros, для этой задачи лучше использовать объект FormData, который будет реализован в файрфоксе (см. описание на сайте MDC), начиная с версии 4 (в хроме уже есть, про остальные не в курсе). Очень вероятно, что браузер тогда будет кушать гораздо меньше памяти, ибо возьмет на себя всю черновую работу по чтению файлов.

      Пока же можно ограничивать кол-во одновременных загрузок и максимальный размер одного файла.
      • 0
        вот метод FormData:
        void append(DOMString name, Blob value);

        и чего? value что должно содержать? не тело ли файла? раз у неё тип blob

        • +1
          Вы зря ссылку внимательно не посмотрели, там есть строка:
          formData.append("afile", fileInputElement.files[0]); 
          

          Как видите, метод append достаточно умен, чтоб принимать и blob-данные и просто объект File. Я верю, что разработчики оптимизируют процесс чтения и загрузки дял второго случая.
          • +1
            а да, спасибо.

            Я просто заходил на http://dev.w3.org/2006/webapi/XMLHttpRequest-2/Overview.html#the-formdata-interface
            там этого нету.
            • 0
              Теперь там, где это возможно (Safari, Chrome, FF 4+) используется FormData, что позволяет избежать загрузки файлов в память (само собой, если есть цель сэкономить память, не стоит их открывать файлы, чтоб делать превьюшки).
              Если интересно, см. апдейт статьи.
              • 0
                Спасибо!
  • 0
    За статью больше спасибо
    Для MooTools тоже есть очень похожий плагин mootools.net/forge/p/uploadmanager
  • 0
    За статью спасибо.
    Приведенный плагин нужно чуть-чуть допилить — FF 3.6 не поддеоживает FileReader, но поддерживает File.getAsBinary()

    } else if($.support.fileReading && xhr.sendAsBinary) {

    заменить на

    • 0
      } else if(($.support.fileReading || item.file.getAsBinary) && xhr.sendAsBinary) { (извините, нечаянно нажал отправить)
    • +1
      Опять соврал — тестировал под FF 3.5. Также еще одно замечание по поводу имени файла — вместо строки
      var filename = item.replaceName || item.file.name; 
      

      нужно
       var filename = item.replaceName || item.file.name || item.file.fileName;
      

  • 0
    xhr.setRequestHeader(«Content-Type», «multipart/form-data, boundary=»+boundary);
    Запятую нужно заменить на ";"
    self.xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary='+boundary);

    У меня node.js(formidable) не захотела обрабатывать такую форму.
    Да и ещё… если есть возможность получить правельный mime-type для файла, то почему бы его не выставить?
    body += 'Content-Type: ' + params.file.type + '\r\n\r\n';
  • 0
    Спасибо за статью, ваши наработки оказались очень полезными.
  • 0
    На данный момент могу сказать что всё работает отлично в Mozilla Firefox 26, Chromium 27. В Opera 12.16 при Drag&Drop при перетаскивании нескольких файлов — залетает только 1, через мультивыбор из поля input:file — нормально, как и должно. А для всяких недобраузеров (кроме Opera), — просто делаются по старинке несколько input:file полей с возможностью добавить ещё, пользователи IE сами выбрали страдания, пусть наслаждаются.
    • 0
      Ну я для недобраузеров делаю еще проще: просто одно поле input:file оставляю :)

      Еще стоит отметить, что все работает отлично также в ИЕ 10+

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.