Клавиша / esc

Загрузка файла с прогресс-баром

Создадим несложный вариант передачи файлов на сервер с индикацией процесса загрузки.

Время чтения: 14 мин

Задача

Скопировано

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

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

Загрузка файла на сервер состоит из трёх частей:

  1. Выбор пользователем файла на своём устройстве.
  2. Проверка параметров обработки файла и формирование данных с обращением к серверу.
  3. Обработка данных на сервере и отправка ответа.

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

Решение для загрузки файла

Скопировано

На странице разместим HTML-разметку формы с необходимыми элементами:

        
          
          <div class="demo-wrapper">  <form    class="form-upload"    id="uploadForm"    method="post"    enctype="multipart/form-data"  >    <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/*"      >    </label>    <input      class="form-upload__submit form-upload__submit_purple"      id="uploadForm_Submit"      type="submit"      value="Загрузить файл"    >    <progress id="progressBar" value="0" max="100"></progress>    <div class="form-upload__container">      <span class="form-upload__status" id="uploadForm_Status"></span>      <span id="uploadForm_Size"></span>    </div>  </form></div>
          <div class="demo-wrapper">
  <form
    class="form-upload"
    id="uploadForm"
    method="post"
    enctype="multipart/form-data"
  >
    <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/*"
      >
    </label>
    <input
      class="form-upload__submit form-upload__submit_purple"
      id="uploadForm_Submit"
      type="submit"
      value="Загрузить файл"
    >
    <progress id="progressBar" value="0" max="100"></progress>
    <div class="form-upload__container">
      <span class="form-upload__status" id="uploadForm_Status"></span>
      <span id="uploadForm_Size"></span>
    </div>
  </form>
</div>

        
        
          
        
      

Для внешнего оформления элементов формы создадим следующие CSS-правила:

        
          
          .form-upload {  display: flex;  flex-direction: column;  align-items: flex-end;}.form-upload__label {  display: flex;  align-items: center;}.form-upload__title {  max-width: 200px;  margin-right: 55px;  font-size: 24px;  font-weight: 500;  line-height: 1;}.form-upload__input {  text-transform: lowercase;  font-size: 18px;  font-weight: 300;  font-family: inherit;}.form-upload__input::file-selector-button {  min-width: 190px;  margin-right: 30px;  padding: 9px 15px;  border: none;  border-radius: 6px;  font-weight: inherit;  font-family: inherit;  cursor: pointer;}.form-upload__input,.form-upload__submit,progress,.form-upload__container {  width: 360px;}.form-upload__submit {  display: block;  margin-top: 25px;  padding: 9px 15px;  border: 2px solid transparent;  border-radius: 6px;  color: #000000;  font-size: 18px;  font-weight: 300;  font-family: inherit;  transition: background-color 0.2s linear;}.form-upload__submit:hover {  background-color: #FFFFFF;  cursor: pointer;  transition: background-color 0.2s linear;}.form-upload__submit:focus-visible,.form-upload__submit:focus {  border: 2px solid #ffffff;  outline: none;}.form-upload__submit_purple {  background-color: #C56FFF;}progress {  height: 5px;  margin-top: 25px;  border: none;  background-color: #286C2D;}progress::-webkit-progress-bar {  border: none;  background-color: #286C2D;}progress::-webkit-progress-value {  background-color: #41E847;}progress::-moz-progress-bar {  border: none;  background-color: #41E847;}.form-upload__container {  margin-top: 10px;  font-size: 16px;}.form-upload__status:empty::before {  content: "Не загружено";}
          .form-upload {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

.form-upload__label {
  display: flex;
  align-items: center;
}

.form-upload__title {
  max-width: 200px;
  margin-right: 55px;
  font-size: 24px;
  font-weight: 500;
  line-height: 1;
}

.form-upload__input {
  text-transform: lowercase;
  font-size: 18px;
  font-weight: 300;
  font-family: inherit;
}

.form-upload__input::file-selector-button {
  min-width: 190px;
  margin-right: 30px;
  padding: 9px 15px;
  border: none;
  border-radius: 6px;
  font-weight: inherit;
  font-family: inherit;
  cursor: pointer;
}

.form-upload__input,
.form-upload__submit,
progress,
.form-upload__container {
  width: 360px;
}

.form-upload__submit {
  display: block;
  margin-top: 25px;
  padding: 9px 15px;
  border: 2px solid transparent;
  border-radius: 6px;
  color: #000000;
  font-size: 18px;
  font-weight: 300;
  font-family: inherit;
  transition: background-color 0.2s linear;
}

.form-upload__submit:hover {
  background-color: #FFFFFF;
  cursor: pointer;
  transition: background-color 0.2s linear;
}

.form-upload__submit:focus-visible,
.form-upload__submit:focus {
  border: 2px solid #ffffff;
  outline: none;
}

.form-upload__submit_purple {
  background-color: #C56FFF;
}

progress {
  height: 5px;
  margin-top: 25px;
  border: none;
  background-color: #286C2D;
}

progress::-webkit-progress-bar {
  border: none;
  background-color: #286C2D;
}

progress::-webkit-progress-value {
  background-color: #41E847;
}

progress::-moz-progress-bar {
  border: none;
  background-color: #41E847;
}

.form-upload__container {
  margin-top: 10px;
  font-size: 16px;
}

.form-upload__status:empty::before {
  content: "Не загружено";
}


        
        
          
        
      

В конце HTML-страницы или в отдельном JS-файле добавим код, который обеспечит связь между пользователем и сервером:

        
          
          const BYTES_IN_MB = 1048576const form = document.getElementById('uploadForm')const submitButton = form.querySelector('.form-upload__submit')const fileInput = form.querySelector('.form-upload__input')const sizeText = form.querySelector('#uploadForm_Size')const statusText = form.querySelector('.form-upload__status')const progressBar = form.querySelector('#progressBar')function resetProgress(status = '') {  statusText.textContent = status  sizeText.textContent = ''  progressBar.value = 0}function upload(fileToUpload) {  const formSent = new FormData()  formSent.append('uploadForm_File', fileToUpload)  const xhr = new XMLHttpRequest()  xhr.upload.addEventListener('progress', progressHandler, false)  xhr.addEventListener('load', loadHandler, false)  xhr.addEventListener('error', errorHandler);  xhr.open('POST', 'upload_processing.php')  xhr.send(formSent)}function updateProgress(loaded, total) {  const loadedMb = (loaded/BYTES_IN_MB).toFixed(1)  const totalSizeMb = (total/BYTES_IN_MB).toFixed(1)  const percentLoaded = Math.round((loaded / total) * 100)  progressBar.value = percentLoaded  sizeText.textContent = `${loadedMb} из ${totalSizeMb} МБ`  statusText.textContent = `Загружено ${percentLoaded}% | `}addEventListener('load', function () {  if (fileInput.value) {    resetProgress()  }})fileInput.addEventListener('change', function () {  const file = this.files[0]  if (file.size > 5 * BYTES_IN_MB) {    alert('Принимается файл до 5 МБ')    this.value = null  }  resetProgress()})form.addEventListener('submit', function (event) {  event.preventDefault()  if (fileInput.files.length > 0) {    const fileToUpload = fileInput.files[0]    fileInput.disabled = true    submitButton.disabled = true    resetProgress()    upload(fileToUpload)  } else {    alert('Сначала выберите файл')  }  return false})function progressHandler(event) {  updateProgress(event.loaded, event.total)}function loadHandler(event) {  if (event.target.status !== 200) {    errorHandler()  } else {    statusText.textContent = event.target.responseText    progressBar.value = 0    fileInput.disabled = false    submitButton.disabled = false  }}function errorHandler() {  resetProgress('Ошибка загрузки')  fileInput.disabled = false  submitButton.disabled = false}
          const BYTES_IN_MB = 1048576

const form = document.getElementById('uploadForm')
const submitButton = form.querySelector('.form-upload__submit')
const fileInput = form.querySelector('.form-upload__input')
const sizeText = form.querySelector('#uploadForm_Size')
const statusText = form.querySelector('.form-upload__status')
const progressBar = form.querySelector('#progressBar')

function resetProgress(status = '') {
  statusText.textContent = status
  sizeText.textContent = ''
  progressBar.value = 0
}

function upload(fileToUpload) {
  const formSent = new FormData()
  formSent.append('uploadForm_File', fileToUpload)

  const xhr = new XMLHttpRequest()
  xhr.upload.addEventListener('progress', progressHandler, false)
  xhr.addEventListener('load', loadHandler, false)
  xhr.addEventListener('error', errorHandler);
  xhr.open('POST', 'upload_processing.php')

  xhr.send(formSent)
}

function updateProgress(loaded, total) {
  const loadedMb = (loaded/BYTES_IN_MB).toFixed(1)
  const totalSizeMb = (total/BYTES_IN_MB).toFixed(1)
  const percentLoaded = Math.round((loaded / total) * 100)

  progressBar.value = percentLoaded
  sizeText.textContent = `${loadedMb} из ${totalSizeMb} МБ`
  statusText.textContent = `Загружено ${percentLoaded}% | `
}

addEventListener('load', function () {
  if (fileInput.value) {
    resetProgress()
  }
})

fileInput.addEventListener('change', function () {
  const file = this.files[0]
  if (file.size > 5 * BYTES_IN_MB) {
    alert('Принимается файл до 5 МБ')
    this.value = null
  }

  resetProgress()
})

form.addEventListener('submit', function (event) {
  event.preventDefault()

  if (fileInput.files.length > 0) {
    const fileToUpload = fileInput.files[0]
    fileInput.disabled = true
    submitButton.disabled = true
    resetProgress()
    upload(fileToUpload)
  } else {
    alert('Сначала выберите файл')
  }

  return false
})

function progressHandler(event) {
  updateProgress(event.loaded, event.total)
}

function loadHandler(event) {
  if (event.target.status !== 200) {
    errorHandler()
  } else {
    statusText.textContent = event.target.responseText
    progressBar.value = 0
    fileInput.disabled = false
    submitButton.disabled = false
  }
}

function errorHandler() {
  resetProgress('Ошибка загрузки')
  fileInput.disabled = false
  submitButton.disabled = false
}

        
        
          
        
      

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

Открыть демо в новой вкладке

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

Открыть демо в новой вкладке

Полный вариант загрузки файла с его сохранением на сервере выглядит так:

Разбор решения

Скопировано

Разметка

Скопировано
        
          
          <div class="demo-wrapper">  <form    class="form-upload"    id="uploadForm"    method="post"    enctype="multipart/form-data"  >    <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/*"      >    </label>    <input      class="form-upload__submit form-upload__submit_purple"      id="uploadForm_Submit"      type="submit"      value="Загрузить файл"    >    <progress id="progressBar" value="0" max="100"></progress>    <div class="form-upload__container">      <span class="form-upload__status" id="uploadForm_Status"></span>      <span id="uploadForm_Size"></span>    </div>  </form></div>
          <div class="demo-wrapper">
  <form
    class="form-upload"
    id="uploadForm"
    method="post"
    enctype="multipart/form-data"
  >
    <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/*"
      >
    </label>
    <input
      class="form-upload__submit form-upload__submit_purple"
      id="uploadForm_Submit"
      type="submit"
      value="Загрузить файл"
    >
    <progress id="progressBar" value="0" max="100"></progress>
    <div class="form-upload__container">
      <span class="form-upload__status" id="uploadForm_Status"></span>
      <span id="uploadForm_Size"></span>
    </div>
  </form>
</div>

        
        
          
        
      

Все элементы, которые участвуют в обработке и отправке файла, размещаются внутри формы.

Для формы указывается атрибут enctype со значением multipart/form-data, поскольку будет использоваться элемент управления для выбора файлов.

Файл для отправки пользователь сможет выбрать с помощью элемента <input>, для которого установлен тип file. Формат файлов, которые можно будет загрузить, устанавливается значением атрибута accept. В данном случае допускается использование изображений любого формата.

☝️ Текст на элементе <input type="file" может отображаться по разному в зависимости от браузера и установленного языка OC.

Отправка файла на сервер выполняется при отправке формы. Для этого в JS-коде мы подписываемся на событие submit. Обработчик этого события будет обрабатывать выбранный файл и передавать его на сервер.

Ход выполнения загрузки будет показываться с использованием специального элемента <progress>. В этот тег встроена роль progressbar, благодаря которой скринридеры объявляют прогресс загрузки автоматически.

Чтобы показать текстовую информацию о результатах загрузки, используются текстовые элементы <span>.

Для каждого элемента внутри формы указывается атрибут id — это позволит JS-коду обращаться к нужным элементам для выполнения необходимых действий.

Стили

Скопировано

Внешний вид элемента <progress> может быть разным — это зависит от браузера и операционной системы устройства пользователя. Например, вот так прогресс-бар будет выглядеть на устройствах с macOS и Windows:

Внешний вид прогресс-бара в macOS и Windows

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

  • добавляет верхний отступ;
  • убирает границу по умолчанию;
  • меняет цвет фона.
        
          
          progress {  height: 5px;  margin-top: 25px;  border: none;  background-color: #286C2D;}
          progress {
  height: 5px;
  margin-top: 25px;
  border: none;
  background-color: #286C2D;
}

        
        
          
        
      

В Firefox эти стили не затронут бегунок, поэтому дополнительно потребуется использовать вендорный префикс -moz. Для стилизации в Chrome и Safari как самого элемента, так и его бегунка, необходимо использовать браузерные префиксы -webkit.

В итоге для одинакового отображения прогресс-бара и бегунка во всех основных браузерах, добавим следующие правила:

        
          
          progress::-webkit-progress-bar {  border: none;  background-color: #286C2D;}progress::-webkit-progress-value {  background-color: #41E847;}progress::-moz-progress-bar {  border: none;  background-color: #41E847;}
          progress::-webkit-progress-bar {
  border: none;
  background-color: #286C2D;
}

progress::-webkit-progress-value {
  background-color: #41E847;
}

progress::-moz-progress-bar {
  border: none;
  background-color: #41E847;
}

        
        
          
        
      

Остальным элементам определим стили для организации их взаимного расположения:

        
          
          .form-upload {  display: flex;  flex-direction: column;  align-items: flex-end;}.form-upload__label {  display: flex;  align-items: center;}.form-upload__submit {  display: block;  margin-top: 25px;  padding: 9px 15px;  border: 2px solid transparent;  border-radius: 6px;  color: #000000;}.form-upload__container {  margin-top: 10px;}
          .form-upload {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

.form-upload__label {
  display: flex;
  align-items: center;
}

.form-upload__submit {
  display: block;
  margin-top: 25px;
  padding: 9px 15px;
  border: 2px solid transparent;
  border-radius: 6px;
  color: #000000;
}

.form-upload__container {
  margin-top: 10px;
}

        
        
          
        
      

JavaScript

Скопировано

Для начала объявим константы и получим все необходимые элементы DOM-дерева, чтобы подписываться на события:

        
          
          // сколько байтов в мегабайтеconst BYTES_IN_MB = 1048576const form = document.getElementById('uploadForm')const submitButton = form.querySelector('.form-upload__submit')const fileInput = form.querySelector('.form-upload__input')const sizeText = form.querySelector('#uploadForm_Size')const statusText = form.querySelector('.form-upload__status')const progressBar = form.querySelector('#progressBar')
          // сколько байтов в мегабайте
const BYTES_IN_MB = 1048576

const form = document.getElementById('uploadForm')
const submitButton = form.querySelector('.form-upload__submit')
const fileInput = form.querySelector('.form-upload__input')
const sizeText = form.querySelector('#uploadForm_Size')
const statusText = form.querySelector('.form-upload__status')
const progressBar = form.querySelector('#progressBar')

        
        
          
        
      

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

Создадим функцию сброса состояния загрузки, это понадобится для очистки информации о предыдущих загрузках:

        
          
          function resetProgress(status = '') {  statusText.textContent = status  sizeText.textContent = ''  progressBar.value = 0}
          function resetProgress(status = '') {
  statusText.textContent = status
  sizeText.textContent = ''
  progressBar.value = 0
}

        
        
          
        
      

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

Инициализируем необходимые объекты:

  • formSent - объект FormData, в котором будут храниться данные для отправки;
  • xhr - объект XMLHttpRequest для формирования запроса к серверу.
        
          
          const formSent = new FormData()formSent.append('uploadForm_File', fileToUpload)const xhr = new XMLHttpRequest()
          
const formSent = new FormData()
formSent.append('uploadForm_File', fileToUpload)
const xhr = new XMLHttpRequest()


        
        
          
        
      

После этого устанавливаем параметры XMLHttpRequest:

  • для XMLHttpRequestUpload добавляется обработчик события progress, который выполняет отслеживание состояния загрузки файла;
  • для XMLHttpRequest добавляются обработчики событий load и error позволяющие отслеживать результат загрузки;
  • метод open() инициализирует POST-запрос к управляющему файлу, который хранится на сервере.

Отправка данных выполняется с помощью метода send().

        
          
          function upload(fileToUpload) {  const formSent = new FormData()  formSent.append('uploadForm_File', fileToUpload)  const xhr = new XMLHttpRequest()  xhr.upload.addEventListener('progress', progressHandler, false)  xhr.addEventListener('load', loadHandler, false)  xhr.addEventListener('error', errorHandler);  xhr.open('POST', 'upload_processing.php')  xhr.send(formSent)}
          function upload(fileToUpload) {
  const formSent = new FormData()
  formSent.append('uploadForm_File', fileToUpload)

  const xhr = new XMLHttpRequest()
  xhr.upload.addEventListener('progress', progressHandler, false)
  xhr.addEventListener('load', loadHandler, false)
  xhr.addEventListener('error', errorHandler);
  xhr.open('POST', 'upload_processing.php')

  xhr.send(formSent)
}

        
        
          
        
      

Для показа индикации загрузки файла создадим функцию updateProgress(). Функция будет вызываться при загрузке каждого нового пакета. Это позволит показывать и обновлять прогресс-бар в реальном времени. Посчитаем нужные данные: сколько мегабайт уже загружено, размер файла и процент загрузки. Воспользуемся полученными значениями, чтобы обновить текст на экране.

        
          
          function updateProgress(loaded, total) {  const loadedMb = (loaded/BYTES_IN_MB).toFixed(1)  const totalSizeMb = (total/BYTES_IN_MB).toFixed(1)  const percentLoaded = Math.round((loaded / total) * 100)  progressBar.value = percentLoaded  sizeText.textContent = `${loadedMb} из ${totalSizeMb} МБ`  statusText.textContent = `Загружено ${percentLoaded}% | `}
          function updateProgress(loaded, total) {
  const loadedMb = (loaded/BYTES_IN_MB).toFixed(1)
  const totalSizeMb = (total/BYTES_IN_MB).toFixed(1)
  const percentLoaded = Math.round((loaded / total) * 100)

  progressBar.value = percentLoaded
  sizeText.textContent = `${loadedMb} из ${totalSizeMb} МБ`
  statusText.textContent = `Загружено ${percentLoaded}% | `
}

        
        
          
        
      

Основную логику будут определять функции-обработчики.

Загрузка файлов большого размера увеличивает нагрузку на сервер, поэтому установим максимальный размер файла в 5 МБ, что составляет 5242880 Б. Проверку размера файла выполним на этапе его выбора пользователем. Для этого получим информацию о файле с помощью выражения this.files[0].

        
          
          fileInput.addEventListener('change', function () {  const file = this.files[0]  if (file.size > 5 * BYTES_IN_MB) {    alert('Принимается файл до 5 МБ')    this.value = null  }  resetProgress()});
          fileInput.addEventListener('change', function () {
  const file = this.files[0]
  if (file.size > 5 * BYTES_IN_MB) {
    alert('Принимается файл до 5 МБ')
    this.value = null
  }

  resetProgress()
});

        
        
          
        
      

После нажатия кнопки «Загрузить файл» происходит событие submit. В обработчике этого события мы проверяем что файл выбран и вызываем функцию upload(), отвечающую за отправку данных на сервер. На время выполнения запроса блокируем возможность выбора и отправки нового файла. Если файл не выбран, оповещаем об этом пользователя:

        
          
          form.addEventListener('submit', function (event) {  event.preventDefault()  if (fileInput.files.length > 0) {    const fileToUpload = fileInput.files[0]    fileInput.disabled = true    submitButton.disabled = true    resetProgress()    upload(fileToUpload)  } else {    alert('Сначала выберите файл')  }  return false})
          form.addEventListener('submit', function (event) {
  event.preventDefault()

  if (fileInput.files.length > 0) {
    const fileToUpload = fileInput.files[0]
    fileInput.disabled = true
    submitButton.disabled = true
    resetProgress()
    upload(fileToUpload)
  } else {
    alert('Сначала выберите файл')
  }

  return false
})

        
        
          
        
      

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

        
          
          function loadHandler(event) {  if (event.target.status !== 200) {    errorHandler()  } else {    statusText.textContent = event.target.responseText    progressBar.value = 0    fileInput.disabled = false    submitButton.disabled = false  }}function errorHandler() {  resetProgress('Ошибка загрузки')  fileInput.disabled = false  submitButton.disabled = false}
          function loadHandler(event) {
  if (event.target.status !== 200) {
    errorHandler()
  } else {
    statusText.textContent = event.target.responseText
    progressBar.value = 0
    fileInput.disabled = false
    submitButton.disabled = false
  }
}

function errorHandler() {
  resetProgress('Ошибка загрузки')
  fileInput.disabled = false
  submitButton.disabled = false
}