Блог / Асинхронная загрузка файлов через скрытый IFRAME: IE отличился
25 октября 2013
Описанный в этой заметке способ асинхронной загрузки файлов основан на предусмотренной в HTML-стандарте возможности передачи данных из формы в любой IFRAME, имеющийся на странице. Форма связана с IFRAME через атрибут «target», который должен совпадать с атрибутом «name» у IFRAME.
На данную тему в интернете можно найти много заметок, но описание подводных камней такого случая, в котором при помощи JavaScript создаётся не только скрытый IFRAME, но и вся форма, мне не попалось, поэтому решил поделиться своим опытом в этом вопросе.
Начал с того, что написал своего рода HTML прототип, чтобы установить адекватность работы этого метода во всех браузерах.
<html>
<head></head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data" target="frame">
<input type="file" name="upload" onchange="submit();" />
</form>
<iframe src="upload.php" name="frame"></iframe>
</body>
</html>
Чтобы отслеживать, что приходит с формы на сервер, создал файлик «upload.php» со следующим кодом:
<html>
<head></head>
<body>
<?php print('$_POST:' . print_r($_POST, 1) . ' $_FILES:' . print_r($_FILES, 1)); ?>
</body>
</html>
Проверил - работает даже в IE (Internet Explorer 8 в режиме IE7).
А теперь попробую сделать то же самое при помощи JavaScript. При изучении интернет публикаций по данной теме неоднократно натыкался на замечания разных авторов, что со скрытым IFRAME могут возникать всякие проблемы в разных браузерах, поэтому скрывать его лучше оборачиванием в скрытый DIV (display:none). В приведённых примерах скрываю IFRAME именно таким образом.
Документ:
<html>
<head>
<script type="text/javascript" src="script.js"></script>
</head>
<body></body>
</html>
JavaScript часть - файл «script.js»:
var frame, form;
window.onload = function() {
// Создаём форму и устанавливаем значения атрибутов
form = document.createElement("FORM");
form.setAttribute('action', 'upload.php');
form.setAttribute('method', 'post');
form.setAttribute('enctype', 'multipart/form-data');
form.setAttribute('target', 'frame');
// Создаём INPUT и устанавливаем значения атрибутов
var input = document.createElement("INPUT");
input.setAttribute('type', 'file');
input.setAttribute('name', 'upload');
// Навешиваем на INPUT обработчик события onChange
if (input.addEventListener)
input.addEventListener("change", function() {upload()}, false);
else
input.attachEvent("onchange", function() {upload()});
// Помещаем INPUT в форму
form.appendChild(input);
// Помещаем форму на страницу
document.body.appendChild(form);
// Создаём IFRAME и устанавливаем значения атрибутов
frame = document.createElement("IFRAME");
frame.setAttribute('src', 'upload.php');
frame.setAttribute('name', 'frame');
// Создаём DIV обёртку для IFRAME
var wrapper = document.createElement('DIV');
// Помещаем IFRAME в DIV обёртку
wrapper.appendChild(frame);
// Делаем обёртку (и её содержимое) невидимой
wrapper.style.display = 'none';
// Помещаем невидимую DIV обёртку содержащую IFRAME на страницу
document.body.appendChild(wrapper);
}
плюс функция, которая назначена в обработчике события «onChange» элемента INPUT:
function upload() {
// Сабмитим форму
form.submit();
// Выводим результат alert-ом
setTimeout(function() {
alert(frame.contentWindow.document.body.innerHTML);
}, 1000)
}
Я думал, что сделал всё по уму, но не тут-то было. В Mozilla Firefox, Google Chrome и Opera всё работает именно так, как и должно было, но вот IE отличился. Все элементы создались, на первый взгляд, нормально, но при сабмите формы, почему-то, данные отправлялись не в IFRAME, а в новое окно, при этом массив $_FILES оказался пустым, а в массиве $_POST появился элемент с ключом «upload». Очевидно, что данные с моего INPUT почему-то попали не в $_FILES, а в $_POST. Такая ситуация возникает, если у формы не указан способ кодирования данных при их отправке на сервер "multipart/form-data" (атрибут «enctype»). Удивился. Стал смотреть DOM средствами разработчика.
Изучение DOM-а показало что атрибут «enctype» у формы стоит именно такой, как и нужно, а вот INPUT и IFRAME вместо атрибута «name» имеют атрибут «submitName». Удивился ещё сильнее. Стало понятно, почему данные с формы улетели не в IFRAME, а в новое окно - потому что IFRAME с именем указанным в атрибуте «target» у формы на странице отсутствует (это имя, почему-то, превратилось в атрибут «submitName»).
Хотя ситуация с попаданием данных с INPUT-а в массив $_POST вместо $_FILES пока и осталась непонятной, решил начать с устранения проблемы атрибутов «name» у INPUT и IFRAME.
Попробовал задавать значение атрибута «name» не через метод setAttribute(), а просто как свойство объекта: input.name = 'upload' и frame.name = 'frame'. Результат тот же.
Погуглив, быстро выяснил, что проблема «submitName» у IE действительно имеет место быть. Решение этой проблемы оказалось очень простым - нужно создавать элементы через innerHTML сразу с прописанным атрибутом «name». Понравилось решение, найденное на просторах интернета, позволяющее получить элемент в виде объекта, не смотря на то, что он, по сути, создаётся при помощи innerHTML. Для удобства использования оформил этот способ в функцию.
function createElm(A) {
if (typeof A == 'string') {
var B = document.createElement('div');
B.innerHTML = A;
return B.firstChild;
}
}
Следует обратить внимание на то, что при использовании этого подхода в IE почему-то не удаётся задать для INPUT атрибут «type» через setAttribute() или через свойство объекта, поэтому type="file" пришлось тоже прописывать в innerHTML.
var frame, form;
window.onload = function() {
// Создаём форму и устанавливаем значения атрибутов
form = document.createElement("FORM");
form.setAttribute('action', 'upload.php');
form.setAttribute('method', 'post');
form.setAttribute('enctype', 'multipart/form-data');
form.setAttribute('target', 'frame');
// Создаём INPUT (сразу с атрибутами type и name)
var input = createElm('<input type="file" name="upload" />');
// Навешиваем на INPUT обработчик события onChange
if (input.addEventListener)
input.addEventListener("change", function() {upload()}, false);
else
input.attachEvent("onchange", function() {upload()});
// Помещаем INPUT в форму
form.appendChild(input);
// Помещаем форму на страницу
document.body.appendChild(form);
// Создаём IFRAME (сразу с атрибутом name) и устанавливаем значения атрибутов
frame = createElm('<iframe name="frame"></iframe>');
frame.setAttribute('src', 'upload.php');
// Создаём DIV обёртку для IFRAME
var wrapper = document.createElement('DIV');
// Помещаем IFRAME в DIV обёртку
wrapper.appendChild(frame);
// Делаем обёртку (и её содержимое) невидимой
wrapper.style.display = 'none';
// Помещаем невидимую DIV обёртку содержащую IFRAME на страницу
document.body.appendChild(wrapper);
}
Наконец-то заработало и в IE.
Но проблема в IE с тем, что данные с INPUT-а формы не попадают в массив $_FILES, всё ещё сохранилась. Ещё раз внимательно посмотрел DOM средствами разработчика - у формы действительно стоит enctype="multipart/form-data". WTF???!!! После долгих раздумий, экспериментов и гуглений, попробовал прописать у формы атрибут «enctype» прямо в innerHTML.
var frame, form;
window.onload = function() {
// Создаём форму (сразу с атрибутом enctype) и устанавливаем значения атрибутов
form = createElm('<form enctype="multipart/form-data"></form>');
form.setAttribute('action', 'upload.php');
form.setAttribute('method', 'post');
form.setAttribute('target', 'frame');
// Создаём INPUT (сразу с атрибутами type и name)
var input = createElm('<input type="file" name="upload" />');
// Навешиваем на INPUT обработчик события onChange
if (input.addEventListener)
input.addEventListener("change", function() {upload()}, false);
else
input.attachEvent("onchange", function() {upload()});
// Помещаем INPUT в форму
form.appendChild(input);
// Помещаем форму на страницу
document.body.appendChild(form);
// Создаём IFRAME (сразу с атрибутом name) и устанавливаем значения атрибутов
frame = createElm('<iframe name="frame"></iframe>');
frame.setAttribute('src', 'upload.php');
// Создаём DIV обёртку для IFRAME
var wrapper = document.createElement('DIV');
// Помещаем IFRAME в DIV обёртку
wrapper.appendChild(frame);
// Делаем обёртку (и её содержимое) невидимой
wrapper.style.display = 'none';
// Помещаем невидимую DIV обёртку содержащую IFRAME на страницу
document.body.appendChild(wrapper);
}
О, чудо!!! Заработало!!! В свете данного открытия встаёт закономерный вопрос - на сколько можно доверять средствам разработчика IE?
Ну и в заключении, чтобы загрузка файлов таки реально заработала, поменял код файла «upload.php»:
<html>
<head></head>
<body>
<?php
$error = '';
$uploadErr = array(
1 => 'Размер принятого файла изображения превысил максимально допустимый размер, который задан директивой upload_max_filesize конфигурационного файла php.ini',
2 => 'Размер загружаемого файла изображения превысил значение MAX_FILE_SIZE, указанное в HTML-форме',
3 => 'Загружаемый файл изображения был получен только частично',
4 => 'Файл изображения не был загружен',
5 => 'Отсутствует временная папка',
6 => 'Не удалось записать файл изображения на диск'
);
if (isset($_FILES['upload']) && @$_FILES['upload']['error'] && @$_FILES['upload']['error'] != 4)
$error = $uploadErr[$_FILES['upload']['error']];
if (isset($_FILES['upload']) && !$error && !@$_FILES['upload']['error']) {
if (!@move_uploaded_file($_FILES['upload']['tmp_name'], 'uploads/' . $_FILES['upload']['name']))
$error = 'Не удалось переместить загруженный файл';
}
if (isset($_FILES['upload']) && !$error)
print('Файл `' . htmlspecialchars($_FILES['upload']['name']) . '` успешно загружен');
else
print($error);
?>
</body>
</html>
Файлы загружаются в папку «uploads», алерты сообщают о результатах загрузки. Работает во всех браузерах, в том числе и в IE. Цель достигнута.
В прикреплённом к статье архиве Вы найдёте окончательный вариант скрипта асинхронной загрузки файлов через скрытый IFRAME.