Pull to refresh

Асинхронная загрузка файлов скрытым iframe

Недавно, в связи с разработкой одного проекта возникла необходимость асинхронной загрузки файлов. Сайт разрабатывался с использованием jQuery библиотеки, так что можно было взять готовый плагин и пользовать на здоровье. Однако захотелось разобраться самому, и вот что из этого получилось.

Немного теории


Выбор пал на технологию с использованием фрейма. Суть заключена в использовании скрытого iframe элемента в качестве транспорта для передачи файла на сервер. Все гениально просто.
HTML стандарт предусматривает возможность передачи данных формы из родительского окна в любой встроенный в это окно фрейм. Связь фрейма и формы осуществляется через target атрибут в форме и соответствующий атрибут name в фрейме. В упрощенном виде схема выглядит следующим образом:
<form action="/async-upload" target="iframe-name">
<input type="file" name="async-file" /&glt;
<input type="submit" value="загрузить" />
</form>
<iframe name="iframe-name" src="#"></iframe>

Выбираю файл в форме, самбичу, браузер передает его на обработку во фрейм. Текущая страница не перегружается, файл попадает на сервер и обработчик async-upload работает с ним как обычно. Все довольны, всем спасибо.

Техническое задание


Нужно что бы в форме добавления комментария была кнопка через которую можно будет асинхронно загружать файлы пока редактируются другие поля, файлы которые загрузились нужно тут же отобразить, с возможностью вставлять ссылки на них в основное тело комментария.

Минимальная реализация


При реализации с включением iframe напрямую в HTML код могут возникать проблемы такие как загрязнение хистори браузера. Опуская ньюансы — для того что бы избежать упомянутых проблем создаю фрейм динамически. Тут то и пригодиться яваскрипт (jQuery). Профита ради кладу фрейм перед файловым инпутом, по условию нужно закачивать файл сразу после выбора, кнопка загрузки отпадает. Для того что бы не сабмитить всю форму в которой лежит файл импут вперемешку с другими элементами, динамически обрамляю нужный импут формой. В результате всех коллабораций является код.
$('input[type=file]').each(function(x){
var form = $('<form action="async-upload.php" method="post" enctype="multipart/form-data" target="iframe-name' + x + '"></form>');
//динамически создается айфрейм перед инпут файлом, что бы не сабмитить родительскую форму целиком обрамляю инпут формой и сабмичу во фрейм
$(this).before('<iframe name="iframe-name' + x + '" src="#"></iframe>').wrap(form).delay(1500).change(function(){
$(this).parent().submit().prev().one('load',
function(){
$(this).next()[0].reset(); //очищает инпут для того что бы не сабмитить файл в родительской форме
alert($(this).contents().find('body').html());
});
});
});

Поподробнее о цепочке вызовов:
1. Селектором выбираю файл импут, для поддержки нескольких файл импутов запускаю цикл.
2. Перед инпутом вставляю пустой айфрейм с name iframe-name + номер импута
3. Обрамляю импут формой, action которой есть серверный скрипт принимающий файл, а target имя айфрейма созданного в предыдущем шаге.
4. Задержка в цепочке delay необходима для того что бы в браузере прошли все необходимые процессы для создания обрамляющей формы, полторы секунды вполне достаточно.
5. Вешаю обработчик на событие change те. на выбор файла в инпуте
6. Цепочкой
...parent().submit()...
отправляю форму в фрейм.
7. Следующим шагом
...prev().one('load', function(){...
вешаю обработчик события загрузки на frame, те жду пока серверный скрипт загрузит файл из формы.
8. B этом обработчике чищу файл инпут что бы он повторно не сабмитился уже в основной форме.
Дополнительно скрипт принимающий файл async-upload.php может возвратить полезные данные такие как был ли файл принят или нет, размер на сервере и тд. и их можно выбрать из фрейма. Я делаю это так:
$(this).contents().find('/*тут какой нибудь селектор*/').html();

Теперь нужно добавить минимальное количество CSS, хорошенько перемешать и быстрый аплоадер готов.
iframe{
display:none;
}
form{
margin:0;
}

Правда он не юзабилен, но все же это перезаряжаемый файл аплоадер который может загружать файлы пока заполняются остальные элементы формы.

Дальше больше


Теперь осталось доделать самое вкусное, внешний вид. Для этого придется немного изменить html вывод импута, добавить яваскрипта и стилей.
Привожу свою реализацию в сокращенном виде:
Импут обернул в div с собственно классом file-input, добавил индикатор состояния.
<div class="file-input">
<input type="file" name="async-file" size="0" />
<div class="indicator">
<div class="ready">Выбрать файл</div>
<div class="go">Загрузка...</div>
</div>
</div>

Дальше модифицированный яваскипт:
if (!Array.indexOf) { //поддержка indexOf обособливо для ие, нужно будет для проверки расширения
Array.prototype.indexOf = function (obj, start) {
for (var i = (start || 0); i < this.length; i++) {
if (this[i] == obj) {
return i;
}
}
return -1;
}
}
$('.file-input').each(function(x){
var i = $(this).children('input'); //это импут
var d = $('.indicator', this); //это индикатор
var a = ['jpg', 'png', 'zip']; //массив разрешенных расширений для клиентской проверки
var form = $('<form action="async-upload" method="post" enctype="multipart/form-data" target="iframe-name' + x + '"></form>');

i.before('<iframe name="iframe-name' + x + '" src="#"></iframe>').delay(1500).wrap(form).parent().append(d);
var $s = function(){ //проверка данных и сабмит формы
var ext = i.val().split('.').pop();
if (a.indexOf(ext) == -1){ //сама проверка расшерения
i.parent().each(
function(){
this.reset();
}
);
return alert('недопустимое расширение файла!');
}
i.parent().addClass('progress');
$(this).parent().submit().prev().one('load',
function(){
i.parent().removeClass('progress');
$(this).next()[0].reset(); //очищает инпут для того что бы не сабмитить файл в родительской форме
alert($(this).contents().find('body').html()); //вывод алертом сообщения от сервера, загруженного во фрейм
});
}
i.change($s);//обработчик на изменения файл инпута
});

И стили:
.file-input{
overflow:hidden;
position:relative;
}
.file-input iframe{
display:none;
}
.file-input form{
margin:0;
display:inline-block;
overflow:hidden;
position:relative;
}
.file-input input{
opacity:0;
position:absolute;
color:#fff;
font-size:250px; /*делаю кнопку browse побольше, пожирнее */
height:22px;
right:0;
}
.file-input form.progress input{
left:-10000px; /*В случае если идет загрузка, убираю файл импут подальше, что бы не кликали лишний раз */
}
.file-input .indicator div{/* Вид кнопки в ожидании */
color:#000;
width:200px;
background:#84CBFC;
height:22px;
text-align:center;
font-weight:bold;
border: 1px solid #0F9DFC;
text-shadow:1px 1px 1px #fff;
-webkit-border-radius:5px;
-moz-border-radius:5px;
border-radius:5px;
}
.file-input .indicator div.go{/* А это в прогрессе */
display:none;
background: url(load_bg.gif) repeat-x;
}
/* В случае загрузки показываю прогресс и скрываю ожидание */
.file-input form.progress .indicator div.ready{
display:none;
}
.file-input form.progress .indicator div.go{
display:block;
}

И немного для IE8 и ниже, как же без них.
div.file-input{
height:22px;
width:200px;
}
.file-input input{
filter:alpha(opacity=0);
}


Букв получилось больше но зато добавил клиентскую проверку на расширение загружаемых файлов, что не в коей мере не отменяет серверную проверку.
Как было отмечено это сокращенная форма, в поекте была добавлена таблица в которую загружались данные возвращаемые серверным скриптом в айфрейм (строка таблицы с именем файла, ссылкой на него, вызовом AJAX функции на удаление этого файла и другие плюшки). Не стал приводить их тут так как это довольно спецефичная реализация, да и просто лишний раз перегрузит код, так что оставляю место для фантазии.

Плюсы и минусы


Такой аплоадер удобно использовать если не планируеться загрузка файлов больших размеров.
Из плюсов можно отметить простоту и кроссбраузерность.
Негативная сторона, это то что все же при заргузки фрейма в браузере включаеться индикатор, по сути дела страница перегружаеться во фрейме. А так же щелчки при загрузки в IE.

Это все


Хочеться отметить, что помимо реализации асинхронной загрузки на спрятаном фрейме существуют чисто AJAX решения использующие в качестве транспорта XHR запросы, в этом случае отсутсвуют побочные эффекты в виде индикатора загрузки, но к сожалению не все браузеры поддерживают этот метод. Используя HTML5 атрибут multiply для файл инпута можно реализовать выбор на загрузку нескольких файлов за раз. При желании можно даже организовать прогресс бар для загрузки файлов большого размера.

Ссылки в тему:
Styling an input type=«file» Хорошая статья которая помогла мне понять как же все таки изменить вид стандартного файл инпута. Автор Peter-Paul Koch.
Ajax file upload with pure JavaScript Описание техники чистой XHR загрузки, понятные примеры. Все очень хорошо описано. Автор Ionut G. Stan
Ajax Upload; A file upload script with progress-bar, drag-and-drop. AJAX загрузчик с реализацией всех свистулек и колокольчиков. Из возможностей, драг и дроп файлов, прогресс бар, возможность отмены загрузки. фалбек на скрытый фрейм для браузеров не поддерживающих возможность загрузки через чистый XHR, мульти загрузка. Автор Andrew Valums.

PS. картинка load-bg.gif используемая в примерах так же прилагается, не забудьте ее переименовать при сохранении
load-bg.gif
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.