FileAPI
В этой статье я расскажу о такой чудо-штуке HTML5 как FileAPI. Для тех, кто не в курсе: это расширение возможностей JS в сторону работы с файлами. Теперь можно определять не только имена файлов, но и их MIME тип, размер, а самое главное — содержимое! Тут будет много теории, но на десерт я приготовил кое-что вкусненькое: Drag and Drop file uploader на чистом JS с прогрессбаром!
К сожалению, прогресс идет неравномерно, поэтому далеко не все браузеры уже успели реализовать этот API. Впереди планеты всей - Firefox и Chrome (на момент написания статьи). Так же поддержка FileAPI появилась в Опере 11.10.
Все начинается с давно известного нам тега input с атрибутом type="file" . Теперь у него есть свойство files , который представляет себе массив (экземпляр класса FileList , если точнее) файлов — объектов класса File. Каждый такой объект может похвастаться наличием следующих свойств (унаследованных от класса Blob):
- name — имя файла
- type — MIME тип файла (если удалось определить).
- size — размер в байтах.
В FF 3.6 есть еще такие методы и свойства:
- fileName — синоним для name.
- fileSize — синоним для size.
- getAsText — получить содержимое файла, рассматривая его как текстовый.
- getAsDataURL — получить файл в формате Data:URL.
- getAsBinary — чтение файла в бинарном режиме. Возвращает строку.
Эти свойства и методы не упомянуты в официальной спецификации и самими разработчиками FF объявлены как deprecated. Т.е. их использование нежелательно.
В Chrome также реализован метод 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 — байт всего
Итак, как я уже обещал, сейчас я опишу создание Drag and Drop аплоадера с прогрессбаром на чистом JS. Идея такая: получаем файл, сброшенный нам пользователем и асинхронно отсылаем его (файл), отслеживая принятые байты.
Для нормальной работы этой функции нужно задать следующие события:
var drg = function(e){
e.stopPropagation();
e.preventDefault();
}, element = document.body; // узел, на который будем "сбрасывать" файлы
element.addEventListener("dragenter", drg, false); // событие при наведении указателя
element.addEventListener("dragover", drg, false); // событие при покидании мыши области элемента
element.addEventListener("drop", function(e){ // непосредственно "сброс"
if(!e.dataTransfer.files) return;
e.stopPropagation();
e.preventDefault();
e.dataTransfer.files // тот же список файлов, что и у инпута
}, false);
Официальная спецификация декларирует новый класс: FormData . Экземпляры этого класса имеют один-единственный метод append , устанавливающий параметр запроса и его значение. Первым аргументом передается строка — имя параметра, вторым — данные на отправку. Это может быть либо строка, либо объект типа File (Blob, если быть точным). Если передать сформированный объект аргументом методу send XHR запроса, то он будет отправлен в режиме multipart.
Но, к сожалению, не все так быстро. FormData пока реализован только в Chrome (среди стабильных билдов. Обещается в FF 4). Для FF 3.6 можно вручную сформировать тело запроса, примерно так:
Content-type: multipart/form-data; boundary="<ФЛАГ_ГРАНИЦЫ>"
--<ФЛАГ_ГРАНИЦЫ>
Content-Disposition: form-data; name="<ИМЯ ПАРАМЕТРА>"; filename="<ИМЯ ФАЙЛА>"
Content-Type: application/octet-stream
<БИНАРНОЕ СОДЕРЖИМОЕ ФАЙЛА>
--<ФЛАГ_ГРАНИЦЫ>;
Но тело запроса кодируется (encodeURIComponent) и на сервер приходит закодированное содержимое файла. Можно, конечно, вручную декодировать файлы на стороне сервера, но это не очень хороший способ. Зато в FF в обход спецификации реализован метод sendAsBinary , отправляющий XHR-запрос как есть, без перекодировки. Одно но: метод не любит мультибайтовые кодировки. Поэтому имя файла придется перекодировать, разбивая символы длиной в несколько байт на несколько однобайтных.
// file — файл для загрузки
// uploadURL - ссылка для загрузки файла
var xhr = new XMLHttpRequest();
xhr.open('POST', uploadURL, true);
if(typeof FormData == 'function'){ // правильный способ
var fData = new FormData();
fData.append('upfile', file);
xhr.send(fData);
} else if(xhr.sendAsBinary){ // пусть работает хоть как-то
var fReader = new FileReader();
fReader.addEventListener('load', function(){
var boundaryString = 'prevedmedved',
boundary = '--' + boundaryString,
requestbody = '';
requestbody += boundary + '\r\n'
+ 'Content-Disposition: form-data; name="upfile"; filename="' + file.name + '"' + '\r\n' // имя параметра — upfile
+ 'Content-Type: application/octet-stream' + '\r\n'
+ '\r\n'
+ fReader.result // бинарное содержимое файла
+ '\r\n'
+ boundary;
xhr.setRequestHeader("Content-type", 'multipart/form-data; boundary="' + boundaryString + '"');
xhr.setRequestHeader("Connection", "close");
xhr.setRequestHeader("Content-length", requestbody.length);
xhr.sendAsBinary(requestbody);
}, false);
fReader.readAsBinaryString(file);
}
Вторая версия XHR декларирует свойство upload, служащее "приемником" событий. Т.е. для свойства upload доступен метод addEventListener . События полностью аналогичны событиям FileReader 'а, так что приводить описание не буду. Так же, как у FileReader'а, у объекта event есть свойства total и loaded. Рассчитать на их основе процент прогресса и вывести несложно.
Почитать:
— FileAPI
— Спецификация FileAPI
— XMLHttpRequest 2
— Спецификация XMLHttpRequest 2
— FilesystemAPI & FileAPI
— FilesystemAPI
|
А в fData.append('upfile', file); элемент из FileList передаётся?
Да.
Ну вот, а я думал пост на похожую тему про драг-н-дроп =)
Только вот по поводу аплоада ошибка. Видно, что сам не пробовал.. Там процент только при даунлоаде считается, а при аплоаде онпрогресс не вызывается.
В смысле? При аплоаде и событие вызывается, и e.total и e.loaded правильные показываются.
e.loaded:e.total
3473:2525947
347537:2525947
642449:2525947
1854865:2525947
2525947 / 1024 / 1024 = 2.4 Мб вес файла.
Хммм.. Помню были траблы с этим. Да, вот еще запись на эту тему http://hacks.mozilla.org/2009/12/uploading-files-with-xmlhttprequest/
Ну рад, что теперь все работает
Эту статью я читал. Там непонятный пример: они отправляют только содержимое файла, не указывая нужных для multipart заголовков (Content-Disposition, Content-Type для блоков).
Возможно ли такими темпами в будующем обработка данных на стороне клиента, в частности ресайз изображений?
Да.
Проблема на nix сервере (на win работает). Разница в символе переноса строки '\n\r' поменял на '\n', но всеравно не работает. Что-то еще нужно. Кстати у меня окончание тела запроса выглядит так: + boundary + '--' + '\n'; - где то вычитал.
И еще одно различие:
req.setRequestHeader("Content-type", 'multipart/form-data; boundary="' + boundaryString + '" charset=windows-1251')
— в конец добавлено: ' charset=windows-1251'
СТОП: при отправке с заголовком 'multipart/form-data' кодирование не осуществляется. Но по спецификации запрос должен отправляться в UTF8. Перекодировать вручную или браузер сам перекодирует? Или вообще ничего не делать?
Я уже запутался в этих различиях, как сделать чтоб работало на nix сервере??? =(
Что-то неправильно именно тут, в составлении запроса:
Помогите.
А может контент-тип base64 сделать?
У меня не загружает файлы на сервер подскажите как написать сценарий на php.
В актуальных версиях Google Chrome есть поддержка FormData - можно использовать его.
Также можно отсылать содержимое файла, закодировав его в base64. Только нужно будет на стороне сервера декодировать.
Немого обновил статью в соответствие с текущим положением дел.
Простой пример не помешал бы. Чтобы пощупать можно было. Буду благодарен.
Небольшая ошибка
это событие при проходе по области элемента... а для события при покидании мыши:
Отличная статья. Спасибо. У меня есть такой вопрос, я выбираю например 5 файлов и добавляю на страницу имена выбранных файлов. Хочу удалить например 3-ий файл из выбранных, но с помощью delete document.getElementById("file").files[2] () не получается. Как можно это сделать?
А какой будет серверная часть для этого кода?
здесь лежат серверные файлы (как, впрочем, и все остальные)