Кроссбраузерная отправка формы с файлом или как переписать весь отправщик несколько раз после тестирования в IE

Задача: отправка и обработка файлов с помощью FormData и FileReader в форме со всеми возможными полями и пересылкой дополнительных параметров для каждого поля c объединением всех данных формы (кроме файлов и системных полей) в общий массив.

Поддержка: все современные браузеры, IE 10+.

Плагины: jquery-2.1.4

image

Для начала разберемся, что же такое FormData

Formdata — тип данных в рамках технологии XHR2, данные в нем хранятся в виде пар ключ / значение.
new Formdata () — это конструктор для создания объекта FormData.

Подробнее о FormData

image

FormData имеет множество методов для полноценной работы с ней, таких как:

  • .get () — возвращает данные по ключу;
  • .getAll () — возвращает массив всех значений, ассоциированных с этим ключом;
  • .has () — возвращает булевое значение касательно наличия объекта;
  • .set () — добавляет значение к уже существующему ключу и, если его нет, создает его;
  • .append () — создает новую пару ключ / значение;
  • .delete () — удаляет объект по ключу;
  • .forEach () — на нем остановимся подробнее:
    В начале работы с FormData появилась весьма сложная проблема из-за того что встал вопрос: как можно перебрать данные в этом объекте? На русскоязычных ресурсах данных найдено не было, зато при получении списка всех методов объекта был найден forEach (), который позволил очень легко перебирать данные. Но появилась проблема, связанная с поддержкой браузерами. Так что этот метод не годится — нужна полная поддержка.

Также FormData можно перебирать с помощью цикла for…of (доступно в ECMAScript 6, с нативной поддержкой которого также есть проблемы).

Главная проблема FormData заключается в Internet explorer (как всегда), а вернее, в его поддержке. Из всех методов, которые есть в FormData, Internet explorer поддерживает только append (), что уничтожает всю простоту использования. Следовательно, мы не можем собрать форму с помощью простого вызова конструктора и последующего изменения данных в ней, и придется это делать вручную:

  • Получим все данные формы через serializeArray (), переберем, проверим их на пустоту и, вместе с заголовком (data-title), если это не системное поле (type=«hidden»), занесем в ассоциативный массив, отдельный для каждого поля, а далее добавим в наш массив для данных формы.
  • Системные поля мы сразу добавляем методом append () в FormData.

Файлы будем собирать с помощью списка, который формирует пользователь при закачке и дальнейшими манипуляциями со списком на клиенте, то есть будем сравнивать те файлы, которые у нас остались в списке, с теми, что хранятся в input type=«file» и с помощью переборки добавлять только те, что оставил пользователь.

Теперь познакомимся с FileReader

FileReader — это объект, который позволяет веб-приложениям асинхронно читать содержимое файлов (или буферы данных), хранящиеся на компьютере пользователя, используя объекты File или Blob, с помощью которых задается файл или данные для чтения.

Подробнее о filereader

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

Теперь к самой задаче
Форма, которую мы будет пересылать:

Для удобства пользователей предоставим им возможность добавления сразу большого количества файлов. С этой целью укажем в поле name значение file[] и атрибут multiple, с ограничением только картинки accept=«image».

Для пользователей также будем выводить список файлов, которые они загрузили с раздельным progress bar-ом для каждого файла и возможностью удаления перед отправкой. И тут мы столкнулись с проблемой. Дело в том, что fileList (массив загруженных файлов) у нашего input предназначен только для чтения, и удалить только выбранный пользователем файл мы не можем. Так что было решено перед отправкой на сервер сверять список, который уже сформировал пользователь, с тем что уже загружено. И при совпадении со списком файл будет добавляться в FormData.

1) Создаем саму функцию отправки через ajax:

var form = form; //текущая форма

    function formSend(formObject, form) {
        $.ajax({
            type: "POST",
            url: 'form-handler.php',
            dataType: 'json',
            contentType: false,
            processData: false,
            data: formObject,
            success: function() {
                $(form).trigger('reset');
 //при успешной отправке сбрасываем форму в дефолтное состояние
                alert('Success');
            }
        });
    };

2) Создаем функцию сборки формы:


function formData_assembly(form) {
        var formSendAll = new FormData(), //создаем объект FormData
            form_arr = $(form).find(':input,select,textarea').serializeArray(), //собираем все данные с формы без файлов
            formdata = {}; //ассациативный массив для хранения данных с формы

        for (var i = 0; i < form_arr.length; i++) {
            if (form_arr[i].value.length > 0) { //перебераем массив с данными формы и проверяем на заполненность
                var current_input = $(form).find('input[name=' + 
                        form_arr[i].name + 
                        '],select[name=' + 
                        form_arr[i].name + 
                        '],textarea[name=' + 
                        form_arr[i].name + ']'),
                    value_arr = {}; // новые массив с данными каждого поля + заголовок
                var title = $(current_input).attr('data-title'); //заголовок поля
                if ($(current_input).attr('type') != 'hidden') { //проверяем не является ли поле системным
                    value_arr['value'] = form_arr[i].value;
                    value_arr['title'] = title;
                    formdata[form_arr[i].name] = value_arr;
                } else {
                    formSendAll.append(form_arr[i].name, form_arr[i].value); //системные поля пересылаем отдельно от общей формы
                }
            }
        }
        formdata = JSON.stringify(formdata);
        formSendAll.append('formData', formdata); // добавляем все поля в formdata

        // file
        if ($(form).find('input[type=file]').hasClass('js_file_check')) { //проверяем есть ли input type file для пересылки
            var current_input = $(form).find('input[type=file]');
            if ($(current_input).val().length > 0) { //проверяем на заполненность
                $('.js_file_list li').each(function() {
                    var list_file_name = $(this).find('span').text();
                    for (var k = 0; k < $(current_input)[0].files.length; k++) {
                        if (list_file_name == $(current_input)[0].files[k].name) { //сверяем список выбранных файлов для загрузки
                            formSendAll.append($(current_input).attr('name'), $(current_input)[0].files[k]); // добавляем только те что остались в списке
                        }
                    }
                })
            }
        }
        formSend(formSendAll, form);
    }
    formData_assembly(form);

3) Оборачиваем все это в функцию для удобного вызова по событию:

function submit_function(form){...}

4) Вешаем функцию на событие клика на кнопку отправки формы:

$('.js_btn_submit').click(function (e) {
	e.preventDefault();
	var current_form = $(this).closest('form');//Текущая форма
	submit_function(current_form);
})

Теперь у нас есть полноценный отправщик формы, осталось только написать обработчик для файлов.

1) Создадим функцию отслеживания состояния input type=file:


function checkFile(){
	var inputs = document.getElementsByClassName('js_file_check');
	for (var i = 0; i < inputs.length; i++) {
  	inputs[i].addEventListener('change', handleFileSelect, false);
	}
}
checkFile();

2) Напишем обработчик ошибок:

var reader;

function abortRead() {
    reader.abort();
}

function errorHandler(evt) {
    switch (evt.target.error.code) {
        case evt.target.error.NOT_FOUND_ERR:
            alert('File Not Found!');
            break;
        case evt.target.error.NOT_READABLE_ERR:
            alert('File is not readable');
            break;
        case evt.target.error.ABORT_ERR:
            break; // noop
        default:
            alert('An error occurred reading this file.');
    };
}

3) Напишем функцию для переборки файлов в fileList нашего input type=file:

function handleFileSelect(evt) {
    var thisInput = $(this); //input type file для множественных загрузок
    for (var i = 0; i < thisInput[0].files.length; i++) { //перебираем все загруженные файлы и запускаем обработчик для каждого
        reader_file(thisInput[0].files[i]); //добавляем обработчик для каждого файла
    }
}

4) Теперь непосредственно сам обработчик:

function reader_file(file) {
    var reader = new FileReader(),
        fileName = file.name;
    reader.onerror = errorHandler; //функция для обработки ошибок
    $('.js_file_list').append('
  • ' + fileName + '
  • '); //добавляем все новые файлы в список на клиенте reader.onabort = function(e) { alert('File read cancelled'); }; reader.onload = function(e) { //событие успешного окончания загрузки //что-нибудь делаем } reader.onprogress = function(event) { // вывод процентной полосы загрузки if (event.lengthComputable) { var percent = parseInt(((event.loaded / event.total) * 100), 10); $('.js_progress_bar').css('width', percent + '%'); } } if (reader.readAsBinaryString === undefined) { // если браузер не поддерживает readAsBinaryString reader.readAsBinaryString = function(fileData) { var binary = "", pt = this, reader = new FileReader(); reader.onload = function(e) { var bytes = new Uint8Array(reader.result); var length = bytes.byteLength; for (var i = 0; i < length; i++) { binary += String.fromCharCode(bytes[i]); } pt.content = binary; $(pt).trigger('onload'); } } reader.readAsArrayBuffer(file); } else { reader.readAsBinaryString(file); } }

    5) Добавим возможность удаления файлов из списка:
    
    $(document).on('click', '.js_file_remove', function() {
        var list_item = $(this).closest('li');
        $(list_item).remove();
    });
    

    6) Можем использовать наш отправщик, не забыв поднять локальный сервер:

    ссылка на демо

    Комментарии (4)

    • 31 марта 2017 в 11:10

      +1

      Спасибо, очень полезная статья, как раз недавно столкнулась с этой проблемой.
      • 31 марта 2017 в 11:24

        +1

        Ага, удобно то, что на сервере получаешь сгруппированные данные. Особенно если файл-обрабочик ajax запросов является общим (выступает в роли контроллера-маршрутизатора). Тогда просто необходимо отделять служебные данные (для маршрутизации запроса) от массива пользовательских данных.
        Респект за статью! Надеюсь многие возьмут ее себе на вооружение, а то нормального frontend+верстальщика на фрилансе днем с фонарем не сыскать.
    • 31 марта 2017 в 11:29

      0

      >Вешаем функцию на событие клика на кнопку отправки формы:

      WAT?

    • 31 марта 2017 в 11:49

      0

      >Главная проблема FormData заключается в Internet explorer (как всегда), а вернее, в его поддержке.
      А вы пробовали искать полифил? Из статьи не очень понял, ак вы в итоге решили проблему с ИЕ.

    © Habrahabr.ru