Загрузка файлов через FileReader

в 12:59, , рубрики: file api, file reader, filereader, html 5, html5, javascript, upload, Песочница, метки: , , , , ,
Зачем я сделал ещё один велосипед?

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

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

На Хабре нашлись две познавательные статьи: Особенности загрузки файлов на HTML5 и Загрузка файлов с помощью html5 File API, с преферансом и танцовщицами. Вторая статья относительно старая да и код, приведённый там, по словам самого же автора «привязан к проекту и его особенностям», а следовательно требует длительного допиливания. Первая статья, без какой-либо конкретной демо с готовым кодом, но дала мне много пищи для размышлений и примерное направление, в которое копать.

Мною был выбран метод загрузки через FileReader. На текущий момент его поддерживают все новые версии популярных браузеров (подробнее). Включая даже Internet Explorer 10, в составе Windows 8, которая, к слову, уже не за горами (26 октября начало розничной продажи).

Где же демо?

Демо можно посмотреть здесь, либо скачать архив.

Что умеет демо?

Демо умеет загружать одновременно до 10 файлов, не более 5 МБ каждый и не более 50 МБ всего. Причем это ограничение сбрасывается после загрузки каждой партии файлов. Показывает прогресс-бар по мере загрузки. Включает и отключает кнопки по мере того, есть или нет в списке файлы, над которыми можно произвести действия (загрузить/удалить).

Демо имеет по минимому «проекто-спефичного» кода и его можно достаточно легко и быстро внедрить в любой проект.

И как это работает?

Принцип работы, в принципе, секрета Полишинеля не составляет.

1) Для драг-н-дропа, который, к слову, работает на всех новых версиях десктопных браузеров, мы вешаем на события примерно следующее:

$('.dropbox')
.on('drop', function(event) {
	if (event.originalEvent.dataTransfer.files.length) {
		// Если были заброшены файлы передадим массив функции addFiles.
		event.preventDefault();
		event.stopPropagation();

		addFiles(event.originalEvent.dataTransfer.files);
		$(this).css('border-color', 'gray');
		$(this).css('color', 'gray');
	}
})
.on('dragenter', function(event) {
	// Просто подсвечиваем дропбокс при наведении.
	$(this).css('border-color', 'green');
	$(this).css('color', 'green');
})
.on('dragleave', function(event) {
	// Убираем подсветку.
	$(this).css('border-color', 'gray');
	$(this).css('color', 'gray');
});

2) Оставим возможность добавлять файл через стандартный input[type=file] добавив к нему также аттрибут multiple, который позволит нам выбирать сразу несколько файлов. О его поддержке браузерами можно даже не задумываться (напоминаю, речь идёт о бэк-енде, который вряд ли будет использоваться в старом браузере).

<input type="file" name="file" size="1" multiple />
$('input[type=file]').on('change', function(event) {
	addFiles(this.files);
});

3) Собственно сама функция addFiles, которая упоминалась в листингах выше. Здесь я её приведу частично, опустив не критичные вещи, полностью можно посмотреть в скрипте из архива (/js/FRUploader.js). Эта функция не последняя инстанция, она всего лишь добавляет файлы в список выбранных, попутно проверяя файлы на различные ограничения (в моём демо предусмотрена конфигурация максимального размера файла, ограничения общего объема файлов и их количества):

function addFiles(files) {
	$.each(files, function(i, v) {

		// Проверим на различные превышения лимитов.
		if (v.size > maxfs) {
			maxfsFiles.push(v.name);
		} else if (flist.length >= maxfc) {
			$('div#maxfcerr').show('fast');
			return false;
		} else if (maxts - curts < v.size) {
			$('div#maxtserr').show('fast');
			return false;
		} else {
			// Считаем файл.
			var fr = new FileReader();
			fr.file = v;
			fr.readAsDataURL(v);

			// Создаем строку для таблицы и заполняем её данными.
			var row = document.createElement('tr');
			/*
			 * Здесь опущен фрагмент по созданию элементов и текстовых узлов, для строки таблицы со списком выбранных файлов.
			 */
			$('table tbody').append(row);

			// Добавляем наш файл в массив.
			flist.push({file: v, trnum: 'id' + index});

		}
	});
}

4) Функция, которая непосредственно и загружает файлы на сервер. Вызывается по клику на кнопку Загрузить.

function uploadFile(file, trnum) {
	if (file) {
		var xhr = new XMLHttpRequest();

		upload = xhr.upload;

		// Создаем прослушиватель события progress, который будет "двигать" прогресс-бар.
		upload.addEventListener('progress', function(event) {
			if (event.lengthComputable) {
				var pbar = $('tr.' + trnum + ' td.size div.pbar');
				pbar.css('width', Math.round((event.loaded / event.total) * 100) + 'px');
			}
		}, false);
		// Создаем прослушиватель события load, который по окончанию загрузки подсветит прогресс-бар зеленым.
		upload.addEventListener('load', function(event) {
			var pbar = $('tr.' + trnum + ' td.size div.pbar');
			pbar.css('width', '100px');
			pbar.css('background', 'green');
		}, false);
		// Создаем прослушиватель события error, который при ошибке подсветит прогресс-бар красным.
		upload.addEventListener('error', function(event) {
			var pbar = $('tr.' + trnum + ' td.size div.pbar');
			pbar.css('width', '100px');
			pbar.css('background', 'red');
		}, false);

		// Откроем соединение.
		xhr.open('POST', 'handler.php');

		// Устанавливаем заголовки.
		xhr.setRequestHeader('Cache-Control', 'no-cache');
		xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
		xhr.setRequestHeader("X-File-Name", file.name);
		// Отправляем файл.
		xhr.send(file);
	}
}

5) Ну и, конечно же, обработчик на сервере (handler.php). Спешу напомнить тому, кто захочет взять мой код за основу, что несомненно не следует полагаться исключительно на проверку файлов JavaScript'ом. На сервере также необходимо проверить, проходит ли файл по всем ограничениям. В демо-примере это опущено:

if (!array_key_exists('HTTP_X_FILE_NAME', $_SERVER) || !array_key_exists('CONTENT_LENGTH', $_SERVER))
	exit();

$fname = $_SERVER['HTTP_X_FILE_NAME'];
$fsize = $_SERVER['CONTENT_LENGTH'];

if (!$fsize)
	exit();

file_put_contents("upload/".$fname, file_get_contents("php://input"));

Как вы могли заметить, в случае с загрузкой через FileReader, в отличие от FormData, мы можем считывать и записывать файл непосредственно из stdin (php://input).

Хочу превьюшки!

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

if (file.type.search(/image/.*/)  != -1) {
	var thumb = new Image();
	thumb.src = ev.target.result;
	thumb.addEventListener("load", function() {
		maxwidth = 120;
		maxheight = 90;
		if (thumb.width > thumb.height) {
			thumb.height = thumb.height / (thumb.width / maxwidth);
			thumb.width = maxwidth;
		} else {
			thumb.width = thumb.width / (thumb.height / maxheight);
			thumb.height = maxwidth;
		}
	}, false);
	thumb.load;
	td.appendChild(thumb);
	delete thumb;
}

Но при попытке загрузить несколько крайне тяжеловесных фотографий (8 фото от 10 до 16 МБ каждое) у меня упали все браузеры, кроме Оперы 12.02, которая после очень длительных раздумий всё-таки оклемалась, но на любое действие реагировала крайне медлительно.

Связано это с тем, что превью изображения загружается через data:/ и base64 кодированное содержимое файла. Ради превьюшки 120х90 встраницу включается 16-мегабайтное изображение 5184х3456. Я очень долго пытался найти хоть какой-то способ на лету изменить размер полученного изображения и использовать для превьюшки, но либо JavaScript этого не умеет, либо я не умею искать. Если кто-нибудь в комментариях подскажет способ решения проблемы — буду крайне благодарен.

Автор: benkaminski

  1. Роман:

    Уменьшай качество изображения и размер непосредственно в обработчике, и на страницу ответом отправляй и всё норм будет! Ну а если тебе хочется сделать “прям на лету” то Blob тебе в помощь, уменьшаешь при помощи него массив, таким образом ты ужмешь на много и в принципе ничего не потеряешь!!

  2. bykastar:

    К сожалению ссылки на демо сломались

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js