Javascript.RU

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

+15

Автор: Koc, дата: 12 апреля, 2010 - 18:13
#permalink

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

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


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

Да.


Автор: Илья Кантор, дата: 13 апреля, 2010 - 14:54
#permalink

Ну вот, а я думал пост на похожую тему про драг-н-дроп =)

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


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

В смысле? При аплоаде и событие вызывается, и e.total и e.loaded правильные показываются.
e.loaded:e.total
3473:2525947
347537:2525947
642449:2525947
1854865:2525947

2525947 / 1024 / 1024 = 2.4 Мб вес файла.


Автор: Илья Кантор, дата: 13 апреля, 2010 - 18:09
#permalink

Хммм.. Помню были траблы с этим. Да, вот еще запись на эту тему http://hacks.mozilla.org/2009/12/uploading-files-with-xmlhttprequest/
Ну рад, что теперь все работает


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

Эту статью я читал. Там непонятный пример: они отправляют только содержимое файла, не указывая нужных для multipart заголовков (Content-Disposition, Content-Type для блоков).


Автор: alexpts1 (не зарегистрирован), дата: 24 июня, 2010 - 16:21
#permalink

Возможно ли такими темпами в будующем обработка данных на стороне клиента, в частности ресайз изображений?


Автор: B@rmaley.e><e, дата: 26 июня, 2010 - 22:13
#permalink

Да.


Автор: Yamazl, дата: 4 августа, 2010 - 13:44
#permalink

Проблема на 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 сервере??? =(


Автор: Yamazl, дата: 4 августа, 2010 - 13:58
#permalink

Что-то неправильно именно тут, в составлении запроса:

var boundaryString = '1BLEF0SOKA110DS467A'
        var boundary = '--' + boundaryString
        var requestbody = '';
        requestbody += boundary + '\n'
        + 'Content-Disposition: form-data; name="fileadd"; filename="' + filearr.fname + '"' + '\n'
        + 'Content-Type: application/octet-stream' + '\n'
//        + 'Content-Transfer-Encoding: binary' + '\n'
        + '\n'
        + filearr.fbody
        + '\n'
        + boundary;
//        + boundary + '--' + '\n';
//        req.setRequestHeader("Content-type", 'multipart/form-data; boundary="' + boundaryString + '" charset=windows-1251')
        req.setRequestHeader("Content-type", 'multipart/form-data; boundary="' + boundaryString + '"')
        req.setRequestHeader("Connection", "close")
        req.setRequestHeader("Content-length", requestbody.length)
        req.sendAsBinary(requestbody)

Помогите.


Автор: Mikle, дата: 22 ноября, 2010 - 17:41
#permalink

А может контент-тип base64 сделать?


Автор: Гость (не зарегистрирован), дата: 2 сентября, 2010 - 20:11
#permalink

У меня не загружает файлы на сервер подскажите как написать сценарий на php.


Автор: B@rmaley.e><e, дата: 23 ноября, 2010 - 19:47
#permalink

В актуальных версиях Google Chrome есть поддержка FormData - можно использовать его.
Также можно отсылать содержимое файла, закодировав его в base64. Только нужно будет на стороне сервера декодировать.


Автор: B@rmaley.e><e, дата: 29 декабря, 2010 - 11:08
#permalink

Немого обновил статью в соответствие с текущим положением дел.


Автор: art_maestro, дата: 4 августа, 2011 - 16:36
#permalink

Простой пример не помешал бы. Чтобы пощупать можно было. Буду благодарен.


Автор: KMZ (не зарегистрирован), дата: 6 марта, 2012 - 13:44
#permalink

Небольшая ошибка

element.addEventListener("dragover", drg, false); // событие при покидании мыши области элемента

это событие при проходе по области элемента... а для события при покидании мыши:

element.addEventListener("dragleave", drg, false); // событие при покидании мыши области элемента

Автор: Tigran Vardanyan (не зарегистрирован), дата: 18 августа, 2012 - 22:30
#permalink

Отличная статья. Спасибо. У меня есть такой вопрос, я выбираю например 5 файлов и добавляю на страницу имена выбранных файлов. Хочу удалить например 3-ий файл из выбранных, но с помощью delete document.getElementById("file").files[2] () не получается. Как можно это сделать?


Автор: Chiz, дата: 2 декабря, 2013 - 23:43
#permalink

А какой будет серверная часть для этого кода?


Автор: woland_812 (не зарегистрирован), дата: 7 января, 2014 - 13:45
#permalink

здесь лежат серверные файлы (как, впрочем, и все остальные)


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

Приветствуются комментарии:
  • Полезные.
  • Дополняющие прочитанное.
  • Вопросы по прочитанному. Именно по прочитанному, чтобы ответ на него помог другим разобраться в предмете статьи. Другие вопросы могут быть удалены.
    Для остальных вопросов и обсуждений есть форум.
P.S. Лучшее "спасибо" - не комментарий, как все здорово, а рекомендация или ссылка на статью.
Содержание этого поля является приватным и не предназначено к показу.
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Разрешены HTML-таги: <strike> <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <u> <i> <b> <pre> <img> <abbr> <blockquote> <h1> <h2> <h3> <h4> <h5> <p> <div> <span> <sub> <sup>
  • Строки и параграфы переносятся автоматически.
  • Текстовые смайлы будут заменены на графические.

Подробнее о форматировании

CAPTCHA
Антиспам
12 + 6 =
Введите результат. Например, для 1+3, введите 4.
 
Поиск по сайту
Другие записи этого автора
Больше записей нет. Прокомментируйте эту запись - может быть, тогда он что-нибудь еще хорошее напишет ;)
Содержание

Учебник javascript

Основные элементы языка

Сундучок с инструментами

Интерфейсы

Все об AJAX

Оптимизация

Разное

Дерево всех статей

Популярные таги
Последние комментарии
Последние темы на форуме
Forum