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. Идея такая: получаем получаем файл, сброшенный нам пользователем и асинхронно отсылаем его (файл), отслеживая принятые байты. Для нормальной работы этой функции нужно задать следующие события: 04 | }, element = document.body; |
05 | element.addEventListener( "dragenter" , drg, false ); |
06 | element.addEventListener( "dragover" , drg, false ); |
07 | element.addEventListener( "drop" , function (e){ |
08 | if (!e.dataTransfer.files) return ; |
Официальная спецификация декларирует новый класс: 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-запрос как есть, без перекодировки. Одно но: метод не любит мультибайтовые кодировки. Поэтому имя файла придется перекодировать, разбивая символы длиной в несколько байт на несколько однобайтных. 03 | var xhr = new XMLHttpRequest(); |
04 | xhr.open( 'POST' , uploadURL, true ); |
06 | if ( typeof FormData == 'function' ){ |
07 | var fData = new FormData(); |
08 | fData.append( 'upfile' , file); |
10 | } else if (xhr.sendAsBinary){ |
11 | var fReader = new FileReader(); |
12 | fReader.addEventListener( 'load' , function (){ | 13 | var boundaryString = 'prevedmedved' , |
14 | boundary = '--' + boundaryString, |
17 | requestbody += boundary + '\n' |
18 | + 'Content-Disposition: form-data; name="upfile"; filename="' + file.name + '"' + '\n' |
19 | + 'Content-Type: application/octet-stream' + '\n' |
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); |
30 | fReader.readAsBinaryString(file); |
Вторая версия XHR декларирует свойство upload, служащее "приемником" событий. Т.е. для свойства upload доступен метод addEventListener . События полностью аналогичны событиям FileReader 'а, так что приводить описание не буду. Так же, как у FileReader'а, у объекта event есть свойства total и loaded. Рассчитать на их основе процент прогресса и вывести несложно. |
А в fData.append('upfile', file); элемент из FileList передаётся?