Когда передо мной в очередной раз встала задача об одновременной загрузке нескольких файлов на сервер (без перезагрузки страницы, само собой), я стал блуждать по интернетам в поисках довольно корявого jQuery-плагина, который позволяет имитировать ajax-загрузку файла (того самого плагина, который со скрытым фрэймом: от java- и flash- плагинов сразу было решено отказаться). В процессе поиска я вспомнил, что в грядущем стандарте html 5 возможности по работе с файлами должны быть существенно расширены, и часть этих возможностей доступна уже сейчас. В итоге было решено опробовать их в действии.
Рассматривать возможности File API будем на примере одновременной загрузки нескольких картинок на сервер. В конце статьи приводится готовое решение, оформленное в виде jQuery-плагина.
Не хочу это читать, интересует готовое решение.
Итак, какие же преимущества дает нам использование File API:
Из недостатков можно отметить только недостаточную поддержку в браузерах. Сейчас File API поддерживают только Firefox ≥ 3.6 и Chrome ≥ 6.0. Есть такое ощущение, что и Safari уже совсем скоро подтянется, а вот про IE и Opera пока ничего не ясно (может быть, кто-то располагает сведениями?). Расстроило конечно, что File API не поддерживает IE9 Beta: это странно, учитывая что разработчики IE сейчас взяли курс на обильную поддержку html 5. Но как бы то ни было, очевидно, что в будущем всем браузерам придется подтянуться.
Работающий пример можно увидеть по адресу http://safron.pro/playground/html5uploader/, ниже приведены только наиболее важные фрагменты кода.
Для начала разберемся с html-кодом. Нам понадобится дефолтный элемент input, контейнер для перетаскивания файлов и список ul, куда мы будем помещать миниатюрки изображений:
Ничего особенного, кроме того, что для элемента input указан атрибут
Теперь перейдем к JavaScript (обратите внимание, что я использовал jQuery для упрощения манипуляций с DOM. Тот, кто по каким-либо причинам захочет отказаться от jQuery, сможет без труда переделать скрипты таким образом, чтобы обойтись без него). Сначала сохраним в переменных ссылки на html-элементы, снявшиеся в главных ролях. Далее определим обработчики событий для стандартного поля выбора файлов и для области, куда можно будет перетаскивать файлы.
И в том и в другом случае в обработчике мы получаем доступ к объекту FileList, который по сути представляет собой массив объектов File. Этот массив передается функции displayFiles(), текст которой приведен ниже.
Объект 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.pro/playground/html5uploader/, я собрал все действия по загрузке в объект uploaderObject, при создании которого можно передать дополнительные параметры, такие как функции обратного вызова для получения информации о процессе загрузки.
Здесь создается экземпляр уже знакомого нам объекта 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!
UPD
Для упрощения использования всего вышеизложенного, был создан JQuery-плагин. При помощи него можно загружать файлы через File API там, где это возможно, и реализовать замену (например, обычную отправку формы) там, где нет. По просьбам трудящихся и соотносясь с замечаниями комментаторов, была добавлена загрузка через объект FormData в браузерах, которые его поддерживают (Chrome, Safari 5+, FF 4+). В самом верху файла с плагином есть описание параметров, методов, а также краткие примеры использования. Более полный пример использования можно увидеть здесь (это изначальный пример из этой статьи, только переделанный на использование плагина, его полный код, включая серверную часть,можно скачать здесь [see UPD2]).
UPD2
По просьбе пользователя glebovgin плагин был доработан таким образом, чтобы можно было отправлять не только непосредственно объект File, но также Blob-данные (объект Blob). Это может быть полезно, если есть необходимость отправлять на сервер, например, содержимое canvas, ну или просто вручную сгенерированные данные.
В демке (которая переехала немного на другой адрес) был добавлен пример отправки картинки из canvas. На данный момент эта возможность работает в FF, Chrome, IE10.
Исходный код ныне доступен на GitHub. Замечания, предложения, улучшения приветствуются!
Рассматривать возможности File API будем на примере одновременной загрузки нескольких картинок на сервер. В конце статьи приводится готовое решение, оформленное в виде jQuery-плагина.
Не хочу это читать, интересует готовое решение.
Итак, какие же преимущества дает нам использование File API:
- Независимость от внешних плагинов
- Возможность контролировать процесс загрузки и отображать информацию о нем (прогрессбар всегда добавляет терпения пользователю)
- Возможность прочитать файл и узнать его размер до начала загрузки (в нашем примере это дает нам возможность отсеять файлы, не содержащие изображений и показывать миниатюры картинок)
- Возможность выбрать сразу несколько файлов через стандартное поле выбора файла
- Возможность использовать интерфейс drag and drop для выбора файлов. Да-да, мы сможем перетаскивать файлы для загрузки прямо с рабочего стола или, например, из проводника!
Из недостатков можно отметить только недостаточную поддержку в браузерах. Сейчас File API поддерживают только Firefox ≥ 3.6 и Chrome ≥ 6.0. Есть такое ощущение, что и Safari уже совсем скоро подтянется, а вот про IE и Opera пока ничего не ясно (может быть, кто-то располагает сведениями?). Расстроило конечно, что File API не поддерживает IE9 Beta: это странно, учитывая что разработчики IE сейчас взяли курс на обильную поддержку html 5. Но как бы то ни было, очевидно, что в будущем всем браузерам придется подтянуться.
Работающий пример можно увидеть по адресу http://safron.pro/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.pro/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!
Кое-какие ссылки
- http://safron.pro/playground/html5uploader/ — работающий пример того, что описывалось выше (плюс еще кое-что)
- http://safron.pro/playground/html5uploader/full.zip — весь код целиком в архиве
- http://html5test.com — проверка браузеров на соответсвие html 5 (очень наглядно)
- http://playground.html5rocks.com — площадка для экпериментов с кодом от Google (ее интерфейс будет знаком тем, кто использовал многочисленные API Google)
UPD
Для упрощения использования всего вышеизложенного, был создан JQuery-плагин. При помощи него можно загружать файлы через File API там, где это возможно, и реализовать замену (например, обычную отправку формы) там, где нет. По просьбам трудящихся и соотносясь с замечаниями комментаторов, была добавлена загрузка через объект FormData в браузерах, которые его поддерживают (Chrome, Safari 5+, FF 4+). В самом верху файла с плагином есть описание параметров, методов, а также краткие примеры использования. Более полный пример использования можно увидеть здесь (это изначальный пример из этой статьи, только переделанный на использование плагина, его полный код, включая серверную часть,
Использованные источники
- https://developer.mozilla.org/en/using_files_from_web_applications — статья о файловом интерфейсе на сайте девелоперов Mozilla
- https://developer.mozilla.org/En/XMLHttpRequest/Using_XMLHttpRequest — cтатья об использовании XMLHttpRequest там же
- http://www.w3.org/TR/FileAPI/ — текущая спецификация File API на сайте W3C
- http://www.w3.org/TR/XMLHttpRequest2/ — текущая спецификация XMLHttpRequest там же
UPD2
По просьбе пользователя glebovgin плагин был доработан таким образом, чтобы можно было отправлять не только непосредственно объект File, но также Blob-данные (объект Blob). Это может быть полезно, если есть необходимость отправлять на сервер, например, содержимое canvas, ну или просто вручную сгенерированные данные.
В демке (которая переехала немного на другой адрес) был добавлен пример отправки картинки из canvas. На данный момент эта возможность работает в FF, Chrome, IE10.
Исходный код ныне доступен на GitHub. Замечания, предложения, улучшения приветствуются!
