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

Опубликовано: 2022-03-10
Краткое резюме ↬ В этой статье мы будем использовать «ванильный» JavaScript ES2015+ (без фреймворков или библиотек) для завершения этого проекта, и предполагается, что у вас есть практические знания JavaScript в браузере. Этот пример должен быть совместим со всеми вечнозелеными браузерами, а также с IE 10 и 11.

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

Технически это уже было возможно, потому что большинство (если не все ) реализаций ввода выбора файлов позволяли вам перетаскивать файлы поверх него, чтобы выбрать их, но это требует, чтобы вы фактически отображали элемент file . Итак, давайте на самом деле используем API, предоставленные нам браузером, для реализации селектора и загрузки файлов с помощью перетаскивания.

В этой статье мы будем использовать «ванильный» JavaScript ES2015+ (без фреймворков или библиотек) для завершения этого проекта, и предполагается, что у вас есть практические знания JavaScript в браузере. Этот пример — за исключением синтаксиса ES2015+, который можно легко изменить на синтаксис ES5 или транспилировать Babel — должен быть совместим со всеми вечнозелеными браузерами, а также с IE 10 и 11.

Вот краткий обзор того, что вы будете делать:

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

События перетаскивания

Первое, что нам нужно обсудить, — это события, связанные с перетаскиванием, потому что они являются движущей силой этой функции. Всего существует восемь событий, связанных с перетаскиванием браузером: drag , dragend , dragenter , dragexit , dragleave , dragover , dragstart и drop . Мы не будем рассматривать их все, потому что drag , dragend , dragexit и dragstart запускаются для перетаскиваемого элемента, и в нашем случае мы будем перетаскивать файлы из нашей файловой системы, а не элементы DOM. , поэтому эти события никогда не всплывут.

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

Еще после прыжка! Продолжить чтение ниже ↓

Как и следовало ожидать, вы можете зарегистрировать обработчики событий для этих событий так же, как вы регистрируете обработчики событий для большинства событий браузера: через addEventListener .

 let dropArea = document.getElementById('drop-area') dropArea.addEventListener('dragenter', handlerFunction, false) dropArea.addEventListener('dragleave', handlerFunction, false) dropArea.addEventListener('dragover', handlerFunction, false) dropArea.addEventListener('drop', handlerFunction, false)

Вот небольшая таблица, описывающая, что делают эти события, используя dropArea из примера кода, чтобы сделать язык более понятным:

Мероприятие Когда он уволен?
dragenter Перетаскиваемый элемент перетаскивается через dropArea, что делает его целью события перетаскивания, если пользователь перетащит его туда.
dragleave Перетащенный элемент перетаскивается из dropArea в другой элемент, вместо этого он становится целью события перетаскивания.
dragover Каждые несколько сотен миллисекунд, пока перетаскиваемый элемент находится над dropArea и движется.
drop Пользователь отпускает кнопку мыши, перетаскивая перетаскиваемый элемент в dropArea.

Обратите внимание, что перетаскиваемый элемент перетаскивается на дочерний dropArea , dragleave срабатывает на dropArea , а dragenter срабатывает на этом дочернем элементе, поскольку он является новой target . Событие drop будет распространяться до dropArea (если распространение не будет остановлено другим прослушивателем событий до того, как оно попадет туда), поэтому оно все равно будет срабатывать в dropArea несмотря на то, что оно не является target события.

Также обратите внимание, что для создания пользовательских взаимодействий перетаскивания вам необходимо вызвать event.preventDefault() в каждом из слушателей для этих событий. Если вы этого не сделаете, браузер в конечном итоге откроет файл, который вы перетащите, вместо того, чтобы отправить его обработчику события drop .

Настройка нашей формы

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

 <div> <form class="my-form"> <p>Upload multiple files with the file dialog or by dragging and dropping images onto the dashed region</p> <input type="file" multiple accept="image/*" onchange="handleFiles(this.files)"> <label class="button" for="fileElem">Select some files</label> </form> </div>

Довольно простая структура. Вы можете заметить обработчик onchange на input . Мы рассмотрим это позже. Также было бы неплохо добавить action в form и кнопку submit , чтобы помочь тем людям, у которых не включен JavaScript. Затем вы можете использовать JavaScript, чтобы избавиться от них для более чистой формы. В любом случае вам понадобится скрипт на стороне сервера, чтобы принять загрузку, будь то что-то, разработанное внутри компании, или вы используете такой сервис, как Cloudinary, чтобы сделать это за вас. Кроме этих заметок, здесь нет ничего особенного, так что давайте добавим несколько стилей:

 #drop-area { border: 2px dashed #ccc; border-radius: 20px; width: 480px; font-family: sans-serif; margin: 100px auto; padding: 20px; } #drop-area.highlight { border-color: purple; } p { margin-top: 0; } .my-form { margin-bottom: 10px; } #gallery { margin-top: 10px; } #gallery img { width: 150px; margin-bottom: 10px; margin-right: 10px; vertical-align: middle; } .button { display: inline-block; padding: 10px; background: #ccc; cursor: pointer; border-radius: 5px; border: 1px solid #ccc; } .button:hover { background: #ddd; } #fileElem { display: none; }

Многие из этих стилей еще не вошли в игру, но это нормально. Основные моменты на данный момент заключаются в том, что ввод file скрыт, но его label выглядит как кнопка, поэтому люди поймут, что они могут щелкнуть ее, чтобы открыть диалоговое окно выбора файла. Мы также следуем соглашению, обводя область перетаскивания пунктирными линиями.

Добавление функции перетаскивания

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

 let dropArea = document.getElementById('drop-area')

Теперь добавим несколько событий. Мы начнем с добавления обработчиков ко всем событиям, чтобы предотвратить поведение по умолчанию и предотвратить всплытие событий выше необходимого:

 ;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, preventDefaults, false) }) function preventDefaults (e) { e.preventDefault() e.stopPropagation() }

Теперь давайте добавим индикатор, чтобы пользователь знал, что он действительно перетащил элемент в нужную область, используя CSS для изменения цвета границы области перетаскивания. Стили уже должны быть под селектором #drop-area.highlight , поэтому давайте воспользуемся JS для добавления и удаления этого класса highlight при необходимости.

 ;['dragenter', 'dragover'].forEach(eventName => { dropArea.addEventListener(eventName, highlight, false) }) ;['dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, unhighlight, false) }) function highlight(e) { dropArea.classList.add('highlight') } function unhighlight(e) { dropArea.classList.remove('highlight') }

Нам пришлось использовать и dragenter и dragover для выделения из-за того, что я упоминал ранее. Если вы начнете наводить курсор непосредственно на dropArea а затем наведите указатель мыши на один из его дочерних элементов, то будет dragleave , а выделение будет удалено. Событие dragover запускается после событий dragenter и dragleave , поэтому выделение будет добавлено обратно в dropArea до того, как мы увидим, что оно было удалено.

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

Теперь все, что нам нужно сделать, это выяснить, что делать, когда некоторые файлы удалены:

 dropArea.addEventListener('drop', handleDrop, false) function handleDrop(e) { let dt = e.dataTransfer let files = dt.files handleFiles(files) }

Это не приближает нас к завершению, но делает две важные вещи:

  1. Демонстрирует, как получить данные для удаленных файлов.
  2. Возвращает нас к тому же месту, где был input file с его обработчиком onchange : ожидание handleFiles .

Имейте в виду, что files — это не массив, а FileList . Итак, когда мы реализуем handleFiles , нам нужно будет преобразовать его в массив, чтобы упростить его итерацию:

 function handleFiles(files) { ([...files]).forEach(uploadFile) }

Это было разочаровывающим. Давайте перейдем к uploadFile для настоящих мясных вещей.

 function uploadFile(file) { let url = 'YOUR URL HERE' let formData = new FormData() formData.append('file', file) fetch(url, { method: 'POST', body: formData }) .then(() => { /* Done. Inform the user */ }) .catch(() => { /* Error. Inform the user */ }) }

Здесь мы используем FormData , встроенный API браузера для создания данных формы для отправки на сервер. Затем мы используем API fetch для фактической отправки изображения на сервер. Убедитесь, что вы изменили URL-адрес для работы с серверной частью или службой, а formData.append любые дополнительные данные формы, которые могут вам понадобиться, чтобы предоставить серверу всю необходимую информацию. В качестве альтернативы, если вы хотите поддерживать Internet Explorer, вы можете использовать XMLHttpRequest , что означает, что вместо этого uploadFile будет выглядеть так:

 function uploadFile(file) { var url = 'YOUR URL HERE' var xhr = new XMLHttpRequest() var formData = new FormData() xhr.open('POST', url, true) xhr.addEventListener('readystatechange', function(e) { if (xhr.readyState == 4 && xhr.status == 200) { // Done. Inform the user } else if (xhr.readyState == 4 && xhr.status != 200) { // Error. Inform the user } }) formData.append('file', file) xhr.send(formData) }

В зависимости от того, как настроен ваш сервер, вы можете захотеть проверить различные диапазоны номеров status , а не только 200 , но для наших целей это будет работать.

Дополнительные возможности

Это все базовые функции, но часто нам нужно больше функций. В частности, в этом руководстве мы добавим панель предварительного просмотра, которая отображает все выбранные изображения для пользователя, а затем мы добавим индикатор выполнения, который позволит пользователю видеть ход загрузки. Итак, приступим к предварительному просмотру изображений.

Предварительный просмотр изображения

Есть несколько способов сделать это: вы можете подождать, пока изображение не будет загружено, и попросить сервер отправить URL-адрес изображения, но это означает, что вам нужно подождать, а изображения иногда могут быть довольно большими. Альтернативой, которую мы рассмотрим сегодня, является использование API FileReader для данных файла, которые мы получили из события drop . Это асинхронно, и в качестве альтернативы вы можете использовать FileReaderSync, но мы можем попытаться прочитать несколько больших файлов подряд, так что это может надолго заблокировать поток и действительно испортить работу. Итак, давайте создадим функцию previewFile и посмотрим, как она работает:

 function previewFile(file) { let reader = new FileReader() reader.readAsDataURL(file) reader.onloadend = function() { let img = document.createElement('img') img.src = reader.result document.getElementById('gallery').appendChild(img) } }

Здесь мы создаем new FileReader и вызываем для него readAsDataURL с объектом File . Как уже упоминалось, это асинхронно, поэтому нам нужно добавить обработчик события onloadend , чтобы получить результат чтения. Затем мы используем базовый URL-адрес данных 64 в качестве src для нового элемента изображения и добавляем его в элемент gallery . Теперь нужно сделать только две вещи, чтобы это заработало: добавить элемент gallery и убедиться, что previewFile действительно вызывается.

Во-первых, добавьте следующий HTML-код сразу после конца тега form :

 <div></div>

Ничего особенного; это просто див. Для него уже заданы стили и изображения в нем, так что делать там больше нечего. Теперь давайте изменим функцию handleFiles на следующую:

 function handleFiles(files) { files = [...files] files.forEach(uploadFile) files.forEach(previewFile) }

Есть несколько способов сделать это, например, компоновка или одиночный обратный вызов forEach , запускающий в нем uploadFile и previewFile , но это тоже работает. И при этом, когда вы перетаскиваете или выбираете некоторые изображения, они должны почти мгновенно отображаться под формой. Самое интересное в этом то, что в некоторых приложениях вы можете не захотеть загружать изображения, а вместо этого хранить их URL-адреса данных в localStorage или каком-либо другом кэше на стороне клиента, чтобы приложение могло получить к нему доступ позже. Я лично не могу придумать никаких хороших вариантов использования для этого, но я готов поспорить, что они есть.

Отслеживание прогресса

Если что-то может занять некоторое время, индикатор выполнения может помочь пользователю понять, что на самом деле происходит прогресс, и указать, сколько времени потребуется для завершения. Добавить индикатор прогресса довольно просто благодаря тегу progress HTML5. Давайте начнем с того, что на этот раз добавим это в код HTML.

 <progress max=100 value=0></progress>

Вы можете вставить это сразу после label или между form и галереей div , в зависимости от того, что вам больше нравится. Если уж на то пошло, вы можете поместить его в любое место внутри тегов body . Для этого примера стили не добавлялись, поэтому будет показана реализация браузера по умолчанию, которую можно обслуживать. Теперь давайте поработаем над добавлением JavaScript. Сначала мы рассмотрим реализацию с использованием fetch , а затем покажем версию для XMLHttpRequest . Для начала нам понадобится пара новых переменных в верхней части скрипта:

 let filesDone = 0 let filesToDo = 0 let progressBar = document.getElementById('progress-bar')

При использовании fetch мы можем определить только завершение загрузки, поэтому единственная информация, которую мы отслеживаем, — это количество файлов, выбранных для загрузки (как filesToDo ) и количество файлов, которые завершили загрузку (как filesDone ). Мы также сохраняем ссылку на элемент #progress-bar , чтобы мы могли быстро обновлять его. Теперь давайте создадим пару функций для управления прогрессом:

 function initializeProgress(numfiles) { progressBar.value = 0 filesDone = 0 filesToDo = numfiles } function progressDone() { filesDone++ progressBar.value = filesDone / filesToDo * 100 }

Когда мы начнем загрузку, будет вызван initializeProgress для сброса индикатора выполнения. Затем, после каждой завершенной загрузки, мы будем вызывать progressDone , чтобы увеличить количество завершенных загрузок и обновить индикатор выполнения, чтобы показать текущий прогресс. Итак, давайте вызовем эти функции, обновив пару старых функций:

 function handleFiles(files) { files = [...files] initializeProgress(files.length) // <- Add this line files.forEach(uploadFile) files.forEach(previewFile) } function uploadFile(file) { let url = 'YOUR URL HERE' let formData = new FormData() formData.append('file', file) fetch(url, { method: 'POST', body: formData }) .then(progressDone) // <- Add `progressDone` call here .catch(() => { /* Error. Inform the user */ }) }

Вот и все. Теперь давайте взглянем на реализацию XMLHttpRequest . Мы могли бы просто сделать быстрое обновление для uploadFile , но XMLHttpRequest на самом деле дает нам больше функциональности, чем fetch , а именно мы можем добавить прослушиватель событий для отслеживания хода загрузки по каждому запросу, который будет периодически давать нам информацию о том, какая часть запроса законченный. Из-за этого нам нужно отслеживать процент выполнения каждого запроса, а не просто количество выполненных запросов. Итак, начнем с замены объявлений для filesDone и filesToDo на следующие:

 let uploadProgress = []

Затем нам также нужно обновить наши функции. Мы переименуем progressDone в updateProgress и изменим их следующим образом:

 function initializeProgress(numFiles) { progressBar.value = 0 uploadProgress = [] for(let i = numFiles; i > 0; i--) { uploadProgress.push(0) } } function updateProgress(fileNumber, percent) { uploadProgress[fileNumber] = percent let total = uploadProgress.reduce((tot, curr) => tot + curr, 0) / uploadProgress.length progressBar.value = total }

Теперь initializeProgress инициализирует массив длиной, равной numFiles , заполненный нулями, что означает, что каждый файл заполнен на 0%. В updateProgress мы узнаем, прогресс какого изображения обновляется, и меняем значение этого индекса на указанный percent . Затем мы рассчитываем общий процент выполнения, взяв среднее значение всех процентов, и обновляем индикатор выполнения, чтобы отразить рассчитанную сумму. Мы по-прежнему вызываем initializeProgress в handleFiles так же, как и в примере с fetch , так что теперь все, что нам нужно обновить, это uploadFile для вызова updateProgress .

 function uploadFile(file, i) { // <- Add `i` parameter var url = 'YOUR URL HERE' var xhr = new XMLHttpRequest() var formData = new FormData() xhr.open('POST', url, true) // Add following event listener xhr.upload.addEventListener("progress", function(e) { updateProgress(i, (e.loaded * 100.0 / e.total) || 100) }) xhr.addEventListener('readystatechange', function(e) { if (xhr.readyState == 4 && xhr.status == 200) { // Done. Inform the user } else if (xhr.readyState == 4 && xhr.status != 200) { // Error. Inform the user } }) formData.append('file', file) xhr.send(formData) }

Первое, что нужно отметить, это то, что мы добавили параметр i . Это индекс файла в списке файлов. Нам не нужно обновлять handleFiles для передачи этого параметра, потому что он использует forEach , который уже дает индекс элемента в качестве второго параметра для обратных вызовов. Мы также добавили прослушиватель событий progress в xhr.upload , чтобы мы могли вызывать updateProgress с прогрессом. Объект события (обозначаемый в коде как e ) имеет две соответствующие части информации о нем: loaded , который содержит количество байтов, которые были загружены до сих пор, и total , который содержит общее количество байтов в файле.

|| 100 Там находится || 100 штук, потому что иногда, если есть ошибка, e.loaded и e.total будут равны нулю, что означает, что вычисление выйдет как NaN , поэтому вместо этого используется 100 , чтобы сообщить, что файл готов. Вы также можете использовать 0 . В любом случае ошибка будет отображаться в обработчике readystatechange , чтобы вы могли сообщить о ней пользователю. Это сделано просто для предотвращения создания исключений при попытке выполнить математические операции с NaN .

Заключение

Это последняя часть. Теперь у вас есть веб-страница, на которую вы можете загружать изображения с помощью перетаскивания, предварительно просматривать загружаемые изображения и видеть ход загрузки на индикаторе выполнения. Вы можете увидеть окончательную версию (с XMLHttpRequest ) в действии на CodePen, но имейте в виду, что служба, в которую я загружаю файлы, имеет ограничения, поэтому, если много людей будут тестировать ее, она может на какое-то время сломаться.