Задача
СкопированоЗагрузка пользователем файлов на сервер — часто встречающаяся задача при создании сайтов и приложений. Текущие возможности JavaScript позволяют нам выбирать нужный файл простым его перетаскиванием в установленную область страницы браузера.
Широкую поддержку событий перетаскивания обеспечивают современные десктопные браузеры, среди мобильных браузеров такая поддержка пока низкая. Поэтому, если необходимо реализовать передачу файла на сервер также и для пользователей мобильных устройств, стоит добавлять возможность выбрать файл с использованием элемента <input type.
В статье будет рассматриваться вариант выбора файла с использованием перетаскивания.
Загрузка файла на сервер состоит из трёх частей:
- Выбор пользователем файла на своём устройстве.
- Проверка параметров обработки файла и формирование данных с обращением к серверу.
- Обработка данных на сервере и отправка ответа клиенту.
Организовать полный процесс загрузки файла возможно только с использованием серверной части, реализация которой выходит за рамки данной статьи. Поэтому далее будет рассмотрена организация отправки файла на стороне клиента: HTML-разметка, стилизация элементов и JavaScript-код для передачи файла на сервер.
Сама же серверная часть для обмена файлами может быть реализована на разных языках программирования. Например, про обработку файлов на стороне сервера с использованием PHP можно подробнее узнать в документации PHP.
Решение для загрузки файла
СкопированоНа странице разместим HTML-разметку с необходимыми элементами:
<div id="uploadFile_Loader" class="upload-zone"> <form class="form-upload" id="uploadForm" method="post" enctype="multipart/form-data"> <div class="upload-zone_dragover"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" viewBox="0 0 24 24" class="upload-loader__image"> <path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242M12 12v9"/> <path d="m16 16-4-4-4 4"/> </svg> <p>Перетащи файл сюда</p> <span class="form-upload__hint" id="hint">Можно загружать только картинки</span> </div> <label class="form-upload__label" for="uploadForm_file"> <span class="form-upload__title">Или нажми кнопку</span> <input class="form-upload__input" id="uploadForm_File" type="file" name="file_name" accept="image/*" aria-describedby="hint"> </label> <div class="form-upload__container"> <span class="form-upload__hint" id="uploadForm_Hint"></span> </div> </form></div>
<div id="uploadFile_Loader" class="upload-zone">
<form class="form-upload" id="uploadForm" method="post" enctype="multipart/form-data">
<div class="upload-zone_dragover">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" viewBox="0 0 24 24" class="upload-loader__image">
<path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242M12 12v9"/>
<path d="m16 16-4-4-4 4"/>
</svg>
<p>Перетащи файл сюда</p>
<span class="form-upload__hint" id="hint">Можно загружать только картинки</span>
</div>
<label class="form-upload__label" for="uploadForm_file">
<span class="form-upload__title">Или нажми кнопку</span>
<input class="form-upload__input" id="uploadForm_File" type="file" name="file_name" accept="image/*" aria-describedby="hint">
</label>
<div class="form-upload__container">
<span class="form-upload__hint" id="uploadForm_Hint"></span>
</div>
</form>
</div>
Для внешнего оформления элементов создадим следующие CSS-правила:
.form-upload { display: grid; align-items: center; width: 80vw; min-width: 360px;}.upload-zone_dragover { display: grid; height: 50vh; min-height: 360px; margin-bottom: 25px; border: 1px solid currentColor; color: #FFFFFF; font-weight: 500; font-size: 18px; place-content: center; text-align: center;}.upload-zone_dragover svg { width: 10vw; margin: auto; pointer-events: none;}.form-upload__hint { margin-top: 10px; font-size: 14px; font-weight: 400;}.upload-zone_dragover._active { color: #c56fff; background-color: #c56fff77;}.form-upload__label { display: flex; justify-content: space-between; align-items: center;}.form-upload__title { margin-right: 55px; font-size: 18px; font-weight: 500;}.form-upload__input { font-family: inherit; font-size: 18px;}.form-upload__input::file-selector-button { margin-right: 30px; border: none; border-radius: 6px; padding: 9px 15px; font-family: inherit; font-weight: inherit; transition: background-color 0.2s linear; cursor: pointer;}.form-upload__input::file-selector-button:hover { background-color: #c56fff;}.form-upload__container { width: 360px; margin-top: 10px; font-size: 16px;}.upload-zone_dragover { background-color: #593273;}.upload-hint,.upload-status { width: 75%;}.upload-hint { display: none;}.upload-hint_visible { display: block; pointer-events: none;}.upload-loader { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%;}.upload-loader_visible { display: flex; justify-content: center; align-items: center; background-color: #593273;}.upload-loader__image { width: 150px; height: 150px;}@media (max-width: 768px) { .upload-zone { padding: 55px 30px; } .form-upload__title { display: block; margin-right: 0; } .form-upload__input::file-selector-button { min-width: initial; margin-right: 10px; }}
.form-upload {
display: grid;
align-items: center;
width: 80vw;
min-width: 360px;
}
.upload-zone_dragover {
display: grid;
height: 50vh;
min-height: 360px;
margin-bottom: 25px;
border: 1px solid currentColor;
color: #FFFFFF;
font-weight: 500;
font-size: 18px;
place-content: center;
text-align: center;
}
.upload-zone_dragover svg {
width: 10vw;
margin: auto;
pointer-events: none;
}
.form-upload__hint {
margin-top: 10px;
font-size: 14px;
font-weight: 400;
}
.upload-zone_dragover._active {
color: #c56fff;
background-color: #c56fff77;
}
.form-upload__label {
display: flex;
justify-content: space-between;
align-items: center;
}
.form-upload__title {
margin-right: 55px;
font-size: 18px;
font-weight: 500;
}
.form-upload__input {
font-family: inherit;
font-size: 18px;
}
.form-upload__input::file-selector-button {
margin-right: 30px;
border: none;
border-radius: 6px;
padding: 9px 15px;
font-family: inherit;
font-weight: inherit;
transition: background-color 0.2s linear;
cursor: pointer;
}
.form-upload__input::file-selector-button:hover {
background-color: #c56fff;
}
.form-upload__container {
width: 360px;
margin-top: 10px;
font-size: 16px;
}
.upload-zone_dragover {
background-color: #593273;
}
.upload-hint,
.upload-status {
width: 75%;
}
.upload-hint {
display: none;
}
.upload-hint_visible {
display: block;
pointer-events: none;
}
.upload-loader {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.upload-loader_visible {
display: flex;
justify-content: center;
align-items: center;
background-color: #593273;
}
.upload-loader__image {
width: 150px;
height: 150px;
}
@media (max-width: 768px) {
.upload-zone {
padding: 55px 30px;
}
.form-upload__title {
display: block;
margin-right: 0;
}
.form-upload__input::file-selector-button {
min-width: initial;
margin-right: 10px;
}
}
В конце HTML-страницы или в отдельном JavaScript-файле добавим код, который обеспечит связь между пользователем и сервером:
const dropFileZone = document.querySelector(".upload-zone_dragover")const statusText = document.getElementById("uploadForm_Hint")const uploadInput = document.querySelector(".form-upload__input")let setStatus = (text) => { statusText.textContent = text}const uploadUrl = "/unicorns";["dragover", "drop"].forEach(function(event) { document.addEventListener(event, function(evt) { evt.preventDefault() return false })})dropFileZone.addEventListener("dragenter", function() { dropFileZone.classList.add("_active")})dropFileZone.addEventListener("dragleave", function() { dropFileZone.classList.remove("_active")})dropFileZone.addEventListener("drop", function() { dropFileZone.classList.remove("_active") const file = event.dataTransfer?.files[0] if (!file) { return } if (file.type.startsWith("image/")) { uploadInput.files = event.dataTransfer.files processingUploadFile() } else { setStatus("Можно загружать только изображения") return false }})uploadInput.addEventListener("change", (event) => { const file = uploadInput.files?.[0] if (file && file.type.startsWith("image/")) { processingUploadFile() } else { setStatus("Можно загружать только изображения") return false }})function processingUploadFile(file) { if (file) { const dropZoneData = new FormData() const xhr = new XMLHttpRequest() dropZoneData.append("file", file) xhr.open("POST", uploadUrl, true) xhr.send(dropZoneData) xhr.onload = function () { if (xhr.status == 200) { setStatus("Всё загружено") } else { setStatus("Oшибка загрузки") } HTMLElement.style.display = "none" } }}function processingDownloadFileWithFetch() { fetch(url, { method: "POST", }).then(async (res) => { const reader = res?.body?.getReader(); while (true && reader) { const { value, done } = await reader?.read() console.log("value", value) if (done) break console.log("Received", value) } })}
const dropFileZone = document.querySelector(".upload-zone_dragover")
const statusText = document.getElementById("uploadForm_Hint")
const uploadInput = document.querySelector(".form-upload__input")
let setStatus = (text) => {
statusText.textContent = text
}
const uploadUrl = "/unicorns";
["dragover", "drop"].forEach(function(event) {
document.addEventListener(event, function(evt) {
evt.preventDefault()
return false
})
})
dropFileZone.addEventListener("dragenter", function() {
dropFileZone.classList.add("_active")
})
dropFileZone.addEventListener("dragleave", function() {
dropFileZone.classList.remove("_active")
})
dropFileZone.addEventListener("drop", function() {
dropFileZone.classList.remove("_active")
const file = event.dataTransfer?.files[0]
if (!file) {
return
}
if (file.type.startsWith("image/")) {
uploadInput.files = event.dataTransfer.files
processingUploadFile()
} else {
setStatus("Можно загружать только изображения")
return false
}
})
uploadInput.addEventListener("change", (event) => {
const file = uploadInput.files?.[0]
if (file && file.type.startsWith("image/")) {
processingUploadFile()
} else {
setStatus("Можно загружать только изображения")
return false
}
})
function processingUploadFile(file) {
if (file) {
const dropZoneData = new FormData()
const xhr = new XMLHttpRequest()
dropZoneData.append("file", file)
xhr.open("POST", uploadUrl, true)
xhr.send(dropZoneData)
xhr.onload = function () {
if (xhr.status == 200) {
setStatus("Всё загружено")
} else {
setStatus("Oшибка загрузки")
}
HTMLElement.style.display = "none"
}
}
}
function processingDownloadFileWithFetch() {
fetch(url, {
method: "POST",
}).then(async (res) => {
const reader = res?.body?.getReader();
while (true && reader) {
const { value, done } = await reader?.read()
console.log("value", value)
if (done) break
console.log("Received", value)
}
})
}
Полный вариант загрузки файла с его сохранением на сервере выглядит так:
Разбор решения
СкопированоРазметка
СкопированоДля обработки файла используется контейнер с идентификатором upload. Внутри этого блока помещается форма <form> с элементами, которые обеспечивают информационное взаимодействие с пользователем. Например, с помощью изменения цвета фона этой области при перетаскивании элемента.
Для каждого элемента, который участвует в процессе обработки файла, указывается атрибут id — это позволит JavaScript-коду обращаться к нужным элементам для выполнения необходимых действий.
<div id="uploadFile_Loader" class="upload-zone"> <form class="form-upload" id="uploadForm" method="post" enctype="multipart/form-data"> <div class="upload-zone_dragover"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" viewBox="0 0 24 24" class="upload-loader__image"> <path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242M12 12v9"/> <path d="m16 16-4-4-4 4"/> </svg> <p>Перетащи файл сюда</p> <span class="form-upload__hint" id="hint">Можно загружать только картинки</span> </div> <label class="form-upload__label" for="uploadForm_file"> <span class="form-upload__title">Или нажми кнопку</span> <input class="form-upload__input" id="uploadForm_File" type="file" name="file_name" accept="image/*" aria-describedby="hint"> </label> <div class="form-upload__container"> <span class="form-upload__hint" id="uploadForm_Hint"></span> </div> </form></div>
<div id="uploadFile_Loader" class="upload-zone">
<form class="form-upload" id="uploadForm" method="post" enctype="multipart/form-data">
<div class="upload-zone_dragover">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" viewBox="0 0 24 24" class="upload-loader__image">
<path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242M12 12v9"/>
<path d="m16 16-4-4-4 4"/>
</svg>
<p>Перетащи файл сюда</p>
<span class="form-upload__hint" id="hint">Можно загружать только картинки</span>
</div>
<label class="form-upload__label" for="uploadForm_file">
<span class="form-upload__title">Или нажми кнопку</span>
<input class="form-upload__input" id="uploadForm_File" type="file" name="file_name" accept="image/*" aria-describedby="hint">
</label>
<div class="form-upload__container">
<span class="form-upload__hint" id="uploadForm_Hint"></span>
</div>
</form>
</div>
Кроме самой области для перетаскивания файла используем специальное поле для его загрузки <input type. Так добавим альтернативный способ загрузки для тех пользователей, которые не пользуются мышкой, и заодно выполним один из критериев WCAG.
Поле свяжем с подсказкой о том, что можно загружать только картинки, при помощи aria. Этот атрибут программно связывает подсказку с полем и полезен для пользователей скринридеров.
Для отображения загрузки файла также можно использовать специальный элемент <progress> — этот вариант подробно рассмотрен в рецепте «Загрузка файла с прогресс-баром». В этот тег уже встроена роль progressbar, благодаря которой скринридеры объявляют прогресс загрузки автоматически.
Стили
СкопированоСтилизуем область для загрузки файла. Зададим ей минимальную высоту, рамку, выровняем элементы по центру.
.upload-zone_dragover { display: grid; height: 50vh; min-height: 360px; margin-bottom: 25px; border: 1px solid currentColor; color: #FFFFFF; font-weight: 500; font-size: 18px; place-content: center; text-align: center;}
.upload-zone_dragover {
display: grid;
height: 50vh;
min-height: 360px;
margin-bottom: 25px;
border: 1px solid currentColor;
color: #FFFFFF;
font-weight: 500;
font-size: 18px;
place-content: center;
text-align: center;
}
При перетаскивании файла в область загрузки будем менять фоновый цвет при помощи дополнительного класса:
.upload-zone_dragover { background-color: #593273;}
.upload-zone_dragover {
background-color: #593273;
}
JavaScript
СкопированоДля начала объявим переменные и получим все необходимые элементы DOM-дерева, чтобы подписываться на события:
dropустанавливает область обработки выбранного файла;File Zone statusуказывает на подсказку о загрузке файла;Text setнужна для хранения текста статуса;Status uploadустанавливает область кнопки для загрузки файла без перетаскивания.Input
const dropFileZone = document.querySelector(".upload-zone_dragover")const statusText = document.getElementById("uploadForm_Hint")const uploadInput = document.querySelector(".form-upload__input")let setStatus = (text) => { statusText.textContent = text}const uploadUrl = "/unicorns"
const dropFileZone = document.querySelector(".upload-zone_dragover")
const statusText = document.getElementById("uploadForm_Hint")
const uploadInput = document.querySelector(".form-upload__input")
let setStatus = (text) => {
statusText.textContent = text
}
const uploadUrl = "/unicorns"
Поскольку переменная set объявляется без присвоения значения, используется ключевое слово let. Об отличиях переменных и принципах работы с ними более развёрнуто рассказывается в статье «Переменные const, let и var».
При отслеживании перетаскивания файла будут использоваться следующие события:
dragoverвыполняется во время перемещения файла над областью обработки файла;dragenterсрабатывает, когда файл входит в область обработки файла;dragleaveсрабатывает, если файл покидает область обработки, но ещё не «сброшен»;dropвыполняется в тот момент, когда пользователь отпустил кнопку мыши и выбранный файл был помещён («сброшен») в заданную область.
Когда при перетаскивании выбранный файл будет находиться в пределах активной страницы, браузер его откроет. Чтобы файл был обработан в назначенной для этого области, необходимо отменить стандартное поведение браузера для событий dragover и drop путём вызова метода prevent:
["dragover", "drop"].forEach(function(event) { document.addEventListener(event, function(evt) { evt.preventDefault() return false })})dropFileZone.addEventListener("dragenter", function() { dropFileZone.classList.add("_active")})dropFileZone.addEventListener("dragleave", function() { dropFileZone.classList.remove("_active")})
["dragover", "drop"].forEach(function(event) {
document.addEventListener(event, function(evt) {
evt.preventDefault()
return false
})
})
dropFileZone.addEventListener("dragenter", function() {
dropFileZone.classList.add("_active")
})
dropFileZone.addEventListener("dragleave", function() {
dropFileZone.classList.remove("_active")
})
Чтобы отправить файл на сервер без перезагрузки страницы, воспользуемся XML — набором механизмов для обмена данными между клиентом и сервером без перезагрузки страницы. Подробней о нём можно почитать на MDN.
Основную работу будет выполнять функция processing, которая принимает выбранный пользователем файл file и отправляет его на сервер:
function processingUploadFile(file) { // Код функции рассматривается ниже}
function processingUploadFile(file) {
// Код функции рассматривается ниже
}
Первым делом объявляем переменные:
drop, в которой с использованием объектаZone Data Formбудут храниться данные для отправки на сервер;Data xhrдля обращения к серверу с использованиемXML.Http Request
const dropZoneData = new FormData()const xhr = new XMLHttpRequest()
const dropZoneData = new FormData()
const xhr = new XMLHttpRequest()
После этого указываем последовательность работы XML при передаче файла на сервер:
- Выбранный файл сохраняется для отправки.
- Для
XMLдобавляется обработчик событияHttp Request progress, который отслеживает процесс загрузки файла. Чтобы показать скрытый графический элемент индикатора загрузки, ему добавляется классupload, а подсказка о загрузке скрывается через удаление класса- loader _ visible upload.- hint _ visible - Метод
openвыполняет POST-запрос к управляющему файлу, который хранится на сервере.( ) - Выбранный пользователем файл передаётся на сервер.
- Для
XMLвыполняется обработка события загрузки файла.Http Request
Если файл сохранён на сервере, индикатор загрузки скрывается и пользователю показывается сообщение об успешной загрузке файла. Если файл не принят сервером, индикатор загрузки скрывается и пользователю показывается сообщение об ошибке.
dropZoneData.append("file", file)xhr.open("POST", uploadUrl, true)xhr.send(dropZoneData)xhr.onload = function () { if (xhr.status == 200) { setStatus("Всё загружено") } else { setStatus("Oшибка загрузки") } HTMLElement.style.display = "none"}
dropZoneData.append("file", file)
xhr.open("POST", uploadUrl, true)
xhr.send(dropZoneData)
xhr.onload = function () {
if (xhr.status == 200) {
setStatus("Всё загружено")
} else {
setStatus("Oшибка загрузки")
}
HTMLElement.style.display = "none"
}