Как реализовать асинхронную загрузку файлов с помощью JavaScript и PHP

Владимир | | CodeIgniter, HTML, JavaScript, PHP.

Логотип к статье Асинхронная загрузка файлов

В комментариях к одной из предыдущих статей меня попросили показать пример загрузки файлов на север с помощью технологии AJAX и фреймворка CodeIgniter.

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

Механизм отправки

Прежде всего, нужно четко понимать, что отправить (загрузить) на сервер файл с помощью AJAX нельзя.

Тем не менее, можно организовать процесс загрузки так, что с точки зрения посетителя загрузка будет выглядеть асинхронной. Т.е. посетитель укажет имя файла и нажмет кнопку «Загрузить». После этого, увидит умную надпись вроде «Подождите, идет загрузка…» или какую-нибудь анимацию. А после окончания загрузки – сообщение с результатами. Страница, которую он видит, перезагружена не будет.

Но при этом отправка файла будет выполнена обычным способом.

Идея заключается в использовании невидимого фрейма (iframe), атрибута формы target и JavaScript.

Работает это так. Создаем страницу с формой и скрытым фреймом (index.html).

<form action="http://www.mysite.com/upload.php" method="post" target="hiddenframe" enctype="multipart/form-data">
<input type="file" id="userfile" name="userfile" />
<input type="submit" value="Загрузить" />
</form>
<div id="res"></div>
<iframe id="hiddenframe" name="hiddenframe" style="width:0px; height:0px; border:0px"></iframe>

Обратите внимание, что в атрибуте target формы указан name фрейма.

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

Примечание
. Как вы знаете, в iframe может быть вставлена любая html страница или результат запроса.

Теперь нужно передать данные из фрейма на основную страницу.

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

Для этого в ответ сервера необходимо вставить JavaScript код, который будет выполнен после окончания загрузки.

Обычно этот код просто вызывает js-функцию, объявленную в основной странице и передает ей результаты выполнения запроса.

Схематически асинхронная отправка файла изображена на диаграмме.

Процесс загрузки файла и обновления страницы

Примечание. Описанная техника называется Remote Scripting. Подробнее о ней можно почитать в этой статье.

Теперь посмотрим, как это работает на практике.

Как я и обещал, в примере будет использоваться фреймворк CodeIgniter.

Прежде всего, создаем контроллер (application/controllers/asyncuploader.php), с двумя методами.

class AsyncUploader extends Controller {
	function AsyncUploader() {
		parent::Controller();
		$this->load->helper('url');
		$this->load->helper('form');
	}
	function index() {
		$pageData['title'] = "Асинхронная загрузка файлов";
		$this->load->view('main', $pageData);
	}
	function do_upload() {
	//...
	}
}

index() – отображает основную страницу с формой;
do_upload() – получает запрос с файлами и возвращает результат (ниже мы рассмотрим его подробно).

После этого создаем представление (application/views/main.php).

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN">
<html xmlns="http://www.w3.org/1999/xhtml" lang="ru">
<head>
<title><?php echo $title; ?></title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<script src="<?php echo base_url()."scripts.js" ?>" type="text/javascript"></script>
</head>
<body>
<?php
$attr = array('target' => 'hiddenframe', 'onsubmit' => 'hideBtn()');
echo form_open_multipart('asyncuploader/do_upload', $attr);
?>
<p>
<input type="file" id="userfile" name="userfile" />
</p>
<div id="sendBtn">
<input type="submit" value="Загрузить" />
</div>
<?php
echo form_close();
?>
<div id="res"></div>
<iframe id="hiddenframe" name="hiddenframe" style="width:0px; height:0px; border:0px"></iframe>
</body>
</html>

Тут все достаточно просто. Мы создали форму (строки 10-20) с полем file и кнопкой «Загрузить».

После формы расположены пустой блок «res», в который будет вставлен ответ сервера и невидимый iframe (строка 23).

При отправке данных формы будет вызвана функция hideBtn(), размещенная в файле scripts.js. Этот файл загружается в заголовке страницы (строка 6).

Рассмотрим его подробнее.

function hideBtn() {
	document.getElementById("sendBtn").innerHTML = "Подождите, идет загрузка...";
}

function handleResponse(mes) {
	var resElement = document.getElementById("res");
	document.getElementById("sendBtn").innerHTML =
		"<input type=\"submit\" value=\"Загрузить\" />";
	if (mes.error != null) {
		resElement.innerHTML = "Возникли ошибки во время загрузки файла: " + mes.error;
	}
	else {
		resElement.innerHTML = "Файл " + mes.file_name + " загружен";
	}
}

Здесь объявлены две функции:
hideBtn() – прячет кнопку «Загрузить» и показывает вместо нее сообщение;
handleResponse() – эта функция отображает результаты загрузки на странице. Она вызывается из iframe. А в качестве параметра получает объект с сообщениями сервера.

Теперь переходим к методу do_upload().

Он принимает данные формы, проверяет и сохраняет файл и возвращает результат.

function do_upload() {
	$config['upload_path'] = './uploads/';
	$config['allowed_types'] = 'zip|rar';
	$config['max_size']	= '500';

	$this->load->library('upload', $config);

	if ( ! $this->upload->do_upload())
	{
		$mes = array('error' => $this->upload->display_errors());
	}
	else
	{
		$mes = $this->upload->data();
	}
	//создаем js массив
	$res = "<script type=\"text/javascript\">";
	$res .= "var data = new Object;";
	foreach ($mes as $key => $item) {
		$res .= "data.".$key." = \"".$item."\";";
	}
	$res .= "window.parent.handleResponse(data);";
	$res .= "</script>";
	echo $res;
}

Для загрузки файла используется библиотека upload, входящая в состав CodeIgniter.

Пользоваться ей несложно. Достаточно создать массив с настройками и загрузить библиотеку (строки 2-6). В настройках можно задать размещение загружаемых файлов, их допустимый тип, размер и т.п.

Примечание. Подробнее почитать о работе с библиотекой можно в официальном руководстве в разделе File Uploading Class.

После того, как настройки заданы, вызываем метод do_upload() (строка 8).

Дальше начинается более интересная часть. Мы должны сформировать ответ сервера с JavaScript кодом, который вызовет функцию handleResponse() основной страницы и передаст ей результаты загрузки.

Прежде всего, обратите внимание, что в зависимости от результатов выполнения у нас будет массив, содержащий либо описание ошибки, либо данные загруженного файла (строка 14).

Теперь посмотрите на строки 17-23. Мы создаем текстовую строку, содержащую обычный JavaScript код. Когда ответ сервера будет загружет во фрейм, код будет автоматически выполнен.

Разберем его подробнее.

Нам нужно передать результаты обработки, которые находятся в массиве $mes (php). Т.к. количество полей может меняться, то удобно использовать объект JavaScript, названия свойств которого будут совпадать с ключами массива.

Для этого мы используем цикл (php), каждая итерация которого создает строку вида:

data.имя_ключа = "значение";

После этого, мы вызываем функцию handleResponse (строка 22), которой передаем сформированный js-объект.

Таким образом, функция handleResponse в параметре получит объект со всеми необходимыми данными, доступ которым можно получить через его свойства, что мы и делаем (листинг функции приведен ранее).

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

До встречи!