Многопоточный загрузчик файлов на JS (jQuery)

в 10:03, , рубрики: file upload, jquery, Песочница, метки: ,

Доброго времени суток, коллеги. В этой статье я опишу опыт создание многопоточного загрузчика файлов (с ограниченной нагрузкой на сервер) на JS (jQuery).

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

Решение №1

Так как задача была простенькая, и, естественно, решений уже написано предостаточно, я обратился за помощью к гуглу. Но, поиск положительных результатов не дал – большинство предлагаемых решений используют Flash (что из-за некоторых специфик не позволяло использовать такие решения мне) или же написанные библиотеки на JS были сильно громадными и что самое прискорбное – нерабочими. Пришлось собирать велосипед.

Решение №2

Задача была срочной, поэтому нужно было срочное решение. Не долго думаю я прикрутил к полю инпут атрибут multiple (доступен с HTML5).

 <form id='FilesupLoadForm'>
	<input type='file' id=’fileinput’ name='files' multiple="multiple" >
	<input type="submit" value='upload'>
</form> 

Далее следовали маленькие изменения обработчика на получение не одного файла, а массива файлов – и задача решена! (наивности (неопытности) моей не было придела).
Как многие со смехом уже додумали, что на первую, нормальную, партию файлов nginx ответил пятьсот третей. Надо было думать дальше.

Решение №3

Так как прошлое решение было сделано красиво и удобно для админов, было решено отталкиваться от него. Нужно было решить проблему ошибки №503, которую возвращал nginx из-за длительной обработки файлов.
Полминуты на обдумывание и появляется новое решение: будем отправлять ajax-ом не сразу все файлы, а по одному.

Решение, примерно, имело следующий вид:

jQuery.each($('#fileinput')[0].files, function(i, f) {   
var file = new FormData();
	file.append('file', f);   
	$.ajax({
		url: 'uploader.php',
		data: file,  
		async : false,
		contentType: false,
		processData: false,
		dataType: "JSON",
		type: 'POST',  
		beforeSend: function() {},
		complete: function(event, request, settings) {},
success: function(data){ }
});
}); 

Все просто: перебираем массив файлов (который находится в нужном инпуте), создаем экземпляр класса для работы с файлами (об этом дальше) и ajax-ом отправляем запрос на сервер. Стоит обратить внимание на параметр "async: false" — тут мы задаем синхронное выполнение ajax-запроса, так как асинхронное создаст нам множество запросов на сервер, чем мы с легкостью его сами и положим.
Решение работает, ошибки нет, но вот одна проблема – работает то оно медленно. И тут мне пришла в голову идея еще одного решения поставленной задачи.

Решение №4

Для ускорения загрузки файлов на сервер можно увеличить количество запросов, в которых будут передаваться файлы. Такое решение упереться в решение двух проблем:
1).Большим количеством запросов я быстро полажу к чертям свой сервер.
2) Большой объем одновременно загружаемых файлов забьет нам весь канал.
Судя по проблемам наш загрузчик должен считать количество запущенных запросов и объем передаваемых данных и при некоторых условиях ожидать окончания одних запросов, для начала других.
Приступим к коду:

	 FilesUploader = {
		
		dataArr : new Array(),
		
		fStek : new Array(),
		
		vStek : 0,
		
		deley : 100, 

		debug : true,

		maxFilesSize : 1024*1024*10, 

		maxThreads : 10,

Легенда:
dataArr – массив с данными для отправки.
fStek – сюда записываем идентификаторы таймаутов, для дальнейшей остановки рекурсии и очистки памяти от незавершенных функций.
vStek – количество вызванных потоков.
deley – задержка рекурсии функции, проверяющей потоки и объемы.
debug – режим дэбага. Нужно для отладки, но в этом примере все её признаки я удалил.
maxFilesSize – максимальная сума объемов загружаемых файлов
maxThreads – максимальное количество потоков.

Полную ясность в переменных (особенно fStek и deley) внесет вторая рассматриваемая функция FilesUploader.controller(). А пока что перейдем к инициализации класса:

		run : function() { 
			jQuery.each($('#fileinput')[0].files, function(i, f) {   
				FilesUploader.dataArr.push(f); 
			}); 
			FilesUploader.controller(); 
		},

Именно на эту функцию вешается обработка события клика кнопки в форме. Работа функции просто: пробегаем по файлам (jQuery.each), занесенным в инпут, и добавляем (FilesUploader.dataArr.push(f)) запись о каждом в массив. Далее вызываем контроллер, который есть важнейшим и сложнейшим звеном системы:

	controller : function() { 		
		FilesUploader.fStek.push(setTimeout(FilesUploader.controller, FilesUploader.deley));
		if(FilesUploader.vStek>=this.maxThreads) {  return; }
		item = FilesUploader.dataArr.pop(); 
		if(item) { 
			if(FilesUploader.maxFilesSize-item.size < 0)	{ 
				FilesUploader.dataArr.push(item);
				return;
			}
			FilesUploader.maxFilesSize-=item.size;
			FilesUploader.vStek++;
			FilesUploader.worker(item);
		} else clearTimeout(FilesUploader.fStek.pop());	
	},

В первой строчке функции мы асинхронно вызываем (через некоторый период времени) эту же функцию (т.е. создаем рекурсию), и заносим идентификатор вызванной функции в переменную, для возможности прервать её выполнение.
Далее идет условие проверки потоков.
После получение файл из массива (FilesUploader.dataArr.pop()) проверяем его на наличие.
1. Если файла нет — тогда «убиваем» вызванные функции, по их идентификатору (clearTimeout(FilesUploader.fStek.pop()));
2. Если файл существует, делаем проверку на объем загружаемых файлов, и если он превышен – возвращаем файл обратно в стек и выходим из функции, иначе, если не превышен: отнимаем объем, увеличиваем счетчик запушенных потоков и вызываем следующую функцию (FilesUploader.worker(item)).

worker : function(item) { 
var file = new FormData();
	file.append('file', item);   
	$.ajax({
		url: 'uploader.php',
		data: file,  
		contentType: false,
		processData: false,
		dataType: "JSON",
		type: 'POST',  
		beforeSend: function() {},
		complete: function(event, request, settings) { 
			FilesUploader.maxFilesSize+=this.fileData.size; 
FilesUploader.vStek--; 
},
success: function(data){ }
});
},

Для отправки файла на сервер средствами ajax, нужно поместить в него данные о файле (file.append()) в экземпляр класса FormData.
Далее уже вызываем функцию $.ajax, которая передаст наш файл загрузчику на сервер. По завершению каждого запроса (функция complete()) нужно увеличить допустимый объем и уменьшить количество исполняемых потоков (что и делается в строках “FilesUploader.maxFilesSize+=this.fileData.size” и “FilesUploader.vStek—“).

И последний штрих – функция вывода в консоль и закрывающая скобка:

	    out : function(message) {
			if(console.log && this.debug) console.log(message);
		}
		
		
	}

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

Автор: IgaIst


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


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