вторник, 13 апреля 2010 г.

FileAPI HTML5

FileAPI

В этой статье я расскажу о такой чудо-штуке HTML5 как FileAPI. Для тех, кто не в курсе: это расширение возожностей JS в сторону работы с файлами. Теперь можно получать не только имена файлов, но и их MIME тип, размер, а самое главное — содержимое! Тут будет много теории, но на десерт я приготовил кое-что вкусненькое: Drag and Drop file uploader на чистом JS с прогрессбаром!

К сожалению, прогресс идет неравномерно, поэтому далеко не все браузеры уже успели реализовать этот API. Фактически, только один — Firefox (работает в версии 3.6, на более ранних не проверял). Близко к нему находится Google Chrome (dev версия). Там уже можно определять вес и тип файла, но не содержимое.

Все начинается с давно известного нам тега input с атрибутом type="file". Теперь у него есть свойство files, который представляет себе массив (экземпляр класса FileList, если точнее) файлов — объектов класса File. Каждый такой объект может похвастаться наличием следующих свойств:

  • name — имя файла
  • type — MIME тип файла (если удалось определить).
  • size — размер в байтах.

В самом FF есть еще такие методы и свойства:

  • fileName — синоним для name.
  • fileSize — синоним для size.
  • getAsText — получить содержимое файла, рассматривая его как текстовый.
  • getAsDataURL — получить файл в формате Data:URL.
  • getAsBinary — чтение файла в бинарном режиме. Возвращает строку.

Эти свойства и методы не упомянуты в официальной спецификации и самими разработчиками FF объявлены как deprecated. Т.е. их использование нежелательно.
Вообще говоря, по стандарту, должно быть еще свойство urn и метод slice, но в данный момент они ни в одном из браузеров не реализованы.

Как вы, наверно, уже догадались, инпутом теперь можно выбрать не один, а сразу несколько файлов. Это уже немного отходит от нашей темы, но скажу, что установление атрибута multiple в true (По аналогии с disabled, checked) позволяет выбирать по несколько файлов. А еще есть атрибут accept, который позволяет фильтровать файлы на стороне клиента по MIME типу. Например: audio/*, video/* разрешит выбор только аудио и видео файлов.

Для чтения предназначен класс FileReader. Создаете экземпляр этого класса, назначаете обработчики событий и нужным методом запускаете чтение файла. Методы экземпляров:

  • readAsBinaryString(file) — чтение в бинарном режиме.
  • readAsText(file[, encoding]) — чтение в текстовом режиме. Дополнительным аргументом указывается кодировка (по-умолчанию UTF-8).
  • readAsDataURL(/forum/file) — чтение в бинарном режиме и последующей перекодировкой в Data:URL.

Все чтения происходят в асинхронном режиме. Методы принимают аргументом объект класса File, содержимое которого нужно прочитать. По окончанию чтения заполняется свойство result, readyState принимает значение, соответствующее успешному завершению запроса (см. список состояний) и вызываются события.

  • Состояние (Значение) — Описание
  • FileReader.EMPTY (0) — Файл не выбран
  • FileReader.LOADING (1) — Файл обрабатывается
  • FileReader.DONE (2) — Файл обработан
  • onloadstart — вызывается в момент начала чтения файла.
  • onprogress — периодически вызывается в течение чтения файла. В момент завершения чтения не вызывается (Т.е. при реализации прогрессбара прогресс не будет доходить до конца. Нужно использовать свойство onload)
  • onload — вызывается после успешного прочтения файла.
  • onabort — вызывается при отмене чтения.
  • onerror — вызывается при ошибке.
  • onloadend — вызывается при завершении чтения, вне зависимости от успешности.

Для назначения событий используется, как нетрудно догадаться, метод addEventListener либо назначение соответствующего свойства.

Функциям-обработчикам событий первым аргументом передается событие event. Самые интересные свойства этого объекта, на мой взгляд:

  • lengthComputable — true или false в зависимости от того, определена ли длина файла
  • loaded — кол-во обработанных байт
  • total — байт всего

Небольшое демо FileAPI (Смотреть в FF3.6+).

Итак, как я уже обещал, сейчас я опишу создание Drag and Drop аплоадера с прогрессбаром на чистом JS. Идея такая: получаем получаем файл, сброшенный нам пользователем и асинхронно отсылаем его (файл), отслеживая принятые байты.

Для нормальной работы этой функции нужно задать следующие события:

01var drg = function(e){
02    e.stopPropagation();
03    e.preventDefault();
04}, element = document.body; // узел, на который будем "сбрасывать" файлы
05element.addEventListener("dragenter", drg, false); // событие при наведении указателя
06element.addEventListener("dragover", drg, false); // событие при покидании мыши области элемента
07element.addEventListener("drop", function(e){ // непосредственно "сброс"
08    if(!e.dataTransfer.files) return;
09    e.stopPropagation();
10    e.preventDefault();
11 
12    e.dataTransfer.files // тот же список файлов, что и у инпута
13}, false);

Официальная спецификация декларирует новый класс: FormData. Экземпляры этого класса имеют один-единственный метод append, устанавливающий параметр запроса и его значение. Первым аргументом передается строка — имя параметра, вторым — данные на отправку. Это может быть либо строка, либо объект типа File (вообще говоря, не File, а Blob, от которого File наследуется. Но Blob на текущий момент нигде не реализован). Если передать сформированный объект аргументом методу send XHR запроса, то он будет отправлен в режиме multipart.
Но, к сожалению, не все так быстро. FormData пока не реализован ни в одном браузере (среди стабильных билдов. Обещается в FF3.7). Как же тогда отправить файл? Можно вручную сформировать тело запроса, примерно так:
Content-type: multipart/form-data; boundary="<ФЛАГ_ГРАНИЦЫ>"

--<ФЛАГ_ГРАНИЦЫ>
Content-Disposition: form-data; name="<ИМЯ ПАРАМЕТРА>"; filename="<ИМЯ ФАЙЛА>"
Content-Type: application/octet-stream

<БИНАРНОЕ СОДЕРЖИМОЕ ФАЙЛА>
--<ФЛАГ_ГРАНИЦЫ>;


Но тело запроса кодируется (encodeURIComponent) и на сервер приходит закодированное содержимое файла. Можно, конечно, вручную декодировать файлы на стороне сервера, но это не очень хороший способ. Зато в единственном на сегодняшний день браузере, поддерживающем FileAPI реализован метод sendAsBinary, отправляющий XHR-запрос как есть, без перекодировки. Одно но: метод не любит мультибайтовые кодировки. Поэтому имя файла придется перекодировать, разбивая символы длиной в несколько байт на несколько однобайтных.
01// file — файл для загрузки
02// uploadURL - ссылка для загрузки файла
03var xhr = new XMLHttpRequest();
04xhr.open('POST', uploadURL, true);
05  
06if(typeof FormData == 'function'){ // правильный способ
07    var fData = new FormData();
08    fData.append('upfile', file);
09     xhr.send(fData);
10} else if(xhr.sendAsBinary){ // пусть работает хоть как-то
11    var fReader = new FileReader();
12    fReader.addEventListener('load', function(){
13        var boundaryString = 'prevedmedved',
14             boundary = '--' + boundaryString,
15            requestbody = '';
16 
17         requestbody += boundary + '\n'
18                + 'Content-Disposition: form-data; name="upfile"; filename="' + file.name + '"' + '\n' // имя параметра — upfile
19                + 'Content-Type: application/octet-stream' + '\n'
20                 + '\n'
21                + fReader.result // бинарное содержимое файла
22                + '\n'
23                + boundary;
24 
25         xhr.setRequestHeader("Content-type", 'multipart/form-data; boundary="' + boundaryString + '"');
26        xhr.setRequestHeader("Connection", "close");
27        xhr.setRequestHeader("Content-length", requestbody.length);
28        xhr.sendAsBinary(requestbody);
29    }, false);
30     fReader.readAsBinaryString(file);
31}

Вторая версия XHR декларирует свойство upload, служащее "приемником" событий. Т.е. для свойства upload доступен метод addEventListener. События полностью аналогичны событиям FileReader'а, так что приводить описание не буду. Так же, как у FileReader'а, у объекта event есть свойства total и loaded. Рассчитать на их основе процент прогресса и вывести несложно.

+3

Автор: Octane, дата: 12 апреля, 2010 - 22:51
#permalink

А в fData.append('upfile', file); элемент из FileList передаётся?


Автор: B@rmaley.e><e, дата: 12 апреля, 2010 - 23:18
#permalink

Да.



Комментариев нет:

Отправить комментарий