Файловые загрузки

Stimulus — это JavaScript-фреймворк, ориентированный на организацию поведения веб-страниц через контроллеры, которые связываются с DOM через data-атрибуты. Одной из часто встречающихся задач является реализация загрузки файлов с клиента на сервер. Stimulus предоставляет удобные механизмы для управления этим процессом, включая события, действия и состояние контроллера.


Настройка контроллера для загрузки файлов

Создание контроллера начинается с наследования от класса Controller:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["input", "progress"]

  connect() {
    console.log("File upload controller connected")
  }
}

Ключевые моменты:

  • static targets определяет элементы DOM, с которыми контроллер будет взаимодействовать. Например, input для <input type="file"> и progress для индикатора загрузки.
  • Метод connect() вызывается при подключении контроллера к DOM.

Связывание действий с событиями

Для обработки выбора файла используется действие change на input:

<input type="file" data-controller="file-upload" data-file-upload-target="input" data-action="change->file-upload#handleFile">
<progress data-file-upload-target="progress" value="0" max="100"></progress>

Контроллер получает событие через метод:

handleFile(event) {
  const file = event.target.files[0]
  if (file) {
    this.uploadFile(file)
  }
}

Асинхронная загрузка через Fetch API

Загрузка файлов осуществляется с помощью FormData и fetch:

uploadFile(file) {
  const formData = new FormData()
  formData.append("file", file)

  fetch("/upload", {
    method: "POST",
    body: formData
  })
  .then(response => response.json())
  .then(data => this.uploadSuccess(data))
  .catch(error => this.uploadError(error))
}

Особенности:

  • FormData автоматически формирует тело запроса с нужными заголовками.
  • Обработка успешного ответа и ошибок вынесена в отдельные методы: uploadSuccess и uploadError.

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

Для отображения прогресса используется XMLHttpRequest вместо fetch, так как стандартный fetch не предоставляет прогресс:

uploadFile(file) {
  const xhr = new XMLHttpRequest()
  const formData = new FormData()
  formData.append("file", file)

  xhr.open("POST", "/upload", true)

  xhr.upload.addEventListener("progress", event => {
    if (event.lengthComputable) {
      const percent = (event.loaded / event.total) * 100
      this.progressTarget.value = percent
    }
  })

  xhr.addEventListener("load", () => this.uploadSuccess(JSON.parse(xhr.responseText)))
  xhr.addEventListener("error", () => this.uploadError("Ошибка загрузки"))

  xhr.send(formData)
}

Ключевые моменты:

  • xhr.upload.addEventListener("progress", …) позволяет отслеживать прогресс передачи данных.
  • Использование lengthComputable предотвращает деление на ноль и некорректные значения.

Множественные файлы и валидация

Stimulus упрощает работу с множественными файлами через input.multiple и массив FileList:

handleFile(event) {
  const files = Array.from(event.target.files)
  files.forEach(file => {
    if (this.validateFile(file)) {
      this.uploadFile(file)
    }
  })
}

validateFile(file) {
  const allowedTypes = ["image/jpeg", "image/png", "application/pdf"]
  const maxSize = 5 * 1024 * 1024 // 5 MB
  if (!allowedTypes.includes(file.type)) return false
  if (file.size > maxSize) return false
  return true
}

Особенности:

  • Проверка типа и размера файлов повышает безопасность и предотвращает загрузку нежелательных данных.
  • Метод Array.from() превращает FileList в массив для удобного перебора.

Динамическое обновление интерфейса

Stimulus позволяет легко управлять состоянием элементов:

uploadSuccess(data) {
  this.progressTarget.value = 100
  this.inputTarget.value = ""
  console.log("Загрузка завершена:", data)
}

uploadError(error) {
  this.progressTarget.value = 0
  console.error("Ошибка загрузки:", error)
}

Принципы:

  • Отдельные методы для успеха и ошибки упрощают поддержку и расширение функционала.
  • Управление DOM через targets минимизирует необходимость поиска элементов вручную.

Поддержка drag-and-drop

Stimulus удобно интегрируется с drag-and-drop:

<div data-controller="file-upload" data-action="drop->file-upload#handleDrop dragover->file-upload#allowDrop" class="drop-zone">
  Перетащите файлы сюда
</div>
allowDrop(event) {
  event.preventDefault()
}

handleDrop(event) {
  event.preventDefault()
  const files = Array.from(event.dataTransfer.files)
  files.forEach(file => this.uploadFile(file))
}

Преимущества:

  • События dragover и drop обеспечивают интерактивность.
  • Stimulus сохраняет чистоту кода, позволяя контроллеру управлять всеми событиями одного блока.

Интеграция с серверной логикой

Stimulus работает с любым сервером, поддерживающим HTTP-запросы:

  • Rails с Active Storage: direct_upload: true интегрируется с stimulus-rails.
  • Node.js/Express: обработка multipart/form-data через multer.
  • Django: request.FILES для получения загруженных файлов.

Фреймворк не накладывает ограничений на бэкенд, обеспечивая легковесное и декларативное управление фронтендом.


Выводы по архитектуре

  • Контроллер Stimulus концентрирует логику загрузки, события и взаимодействие с DOM.
  • targets и actions создают декларативный интерфейс, устраняя необходимость прямого доступа к элементам через querySelector.
  • Разделение методов на обработку событий, валидацию и отправку упрощает поддержку и расширение функционала.

Фокус на структурированности и реактивности делает Stimulus идеальным инструментом для реализации сложных сценариев загрузки файлов с минимальной связкой кода и DOM.