Загрузка изображений в React
Одним из требований к моему выпускному проекту в Flatiron School было включение в него того, что ранее не изучалось в рамках учебного курса. Из-за своей любви к красивому дизайну я выбрал загрузку изображений для выполнения этого требования. Продумав цикл "запрос - ответ", мне нужно было научиться выбирать изображение через файловый браузер, загружать его через запрос POST или PATCH, хранить в базе данных и возвращать изображение с помощью запроса GET.
Настройка
Прежде чем мы продолжим, скажу, что моё приложение построено с использованием:
- React v18.2.0
- Ruby v2.7.4
- Rails v7.0.5
Выбор файла
Хотя в процессе обучения я начал с бэкенда, я решил, что имеет смысл представить это руководство в порядке цикла "запрос - ответ".
Помимо возможности доступа к файловому браузеру для выбора изображения для загрузки, мне также хотелось иметь возможность перемещать это изображение. Изучая несколько вариантов, я наткнулся на react-dropzone, "простой React-хук для создания HTML5-совместимой зоны перемещения файлов". Установка была простой, а документация хорошо написана и содержит множество примеров различных приложений.
Изначально я надеялся, что react-dropzone также является программой загрузки, но при дальнейшем чтении документации выяснилось, что это не так. В документации рекомендуются другие загрузчики, но, как я вскоре узнал, загрузка в React очень проста.
Я создал компонент DropZone, который будет использоваться в различных формах моего приложения. Этот компонент получает реквизит setState
, используемый в хуке useCallback
. Хотя DropZone может работать с файлами любого типа, я настроил свой компонент так, чтобы он принимал только изображения.
import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
function DropZone({ setState }) {
const onDrop = useCallback(acceptedFiles => {
setState(acceptedFiles[0])
}, []);
const {getRootProps, getInputProps, isDragActive} = useDropzone({
onDrop,
accept: {'image/*': []}
});
return (
<div {...getRootProps()} className={isDragActive ? 'active' : ""}>
<input {...getInputProps()} />
{
isDragActive
? <p>Drop the file here ...</p>
: <p>Drag 'n' drop an image here—or click to select file</p>
}
</div>
);
}
export default DropZone;
Это простое использование react-dropzone. Он имеет гораздо больше возможностей, включая возможность предварительного просмотра изображения после его загрузки в состояние.
Когда файл изображения выбран и успешно находится в состоянии, следующей задачей было загрузить его на сервер.
Прикрепление и извлечение
Этот шаг потребовал небольшого исследования. При обычном запросе POST или PATCH я использовал JSON.stringify()
в теле запроса. Однако когда потребовалось прикрепить файл, это решение оказалось неудачным.
Вместо этого я использовал конструктор FormData и его метод append()
для присоединения файла изображения.
function handleImgSubmit(e, setErrors, form, img, setUser) {
e.preventDefault();
setErrors();
const profile = new FormData();
profile.append('first_name', form.firstName);
profile.append('last_name', form.lastName);
profile.append('phone', form.phone);
profile.append('city', form.city);
profile.append('state', form.state);
profile.append('bio', form.bio);
profile.append('venue_id', form.venueId);
profile.append('video_url', form.videoUrl);
if(img) {
profile.append('avatar', img);
}
// Important! Do not add headers to fetch requests when using FormData()
fetch("/profiles", {
method: "POST",
body: profile
})
.then(r => {
if(r.ok) {
r.json().then((data) => { setUser(data) });
} else {
r.json().then((err) => setErrors(err.errors));
}
});
}
Этот метод безупречно работает при отправке файлов на бэкенд. Обратите внимание, запрос на выборку не содержит headers
или JSON.stringify()
. Конструктор FormData позаботится об этих проблемах, и отправка fetch-запроса с headers
приведет к ошибке сервера. FormData можно использовать в любом запросе, независимо от того, прикреплен файл или нет. Это позволяет сэкономить немного кода при рефакторинге, как в данном примере.
const data = new FormData();
Object.keys(form).map(key => data.append(key, form[key]));
Когда фронтенд завершен, следующим шагом будет создание места для размещения файла.
Управление базой данных
Существует множество различных способов управления хранением файлов на бэкенде. Поскольку мой бэкенд представляет собой Rails API, я решил, что имеет смысл использовать Active Storage. Как указано в документации, для преобразования обработки изображений требуется стороннее программное обеспечение и гем image_processing
. Active Storage позволяет легко конфигурировать онлайн-сервисы хранения данных, предлагаемые через Amazon, Google или Microsoft Azure.
Для того чтобы связать изображение с записью, используйте макрос has_one_attached
.
class Profile < ApplicationRecord
belongs_to :user
belongs_to :venue
has_one_attached :avatar
end
Rails 6.0+ также позволяет перейти к включению attachment
в качестве типа атрибута. rails generate model Profile avatar:attachment
.
В контроллере вызовите avatar.attach
, чтобы прикрепить файл изображения к профилю.
class ProfilesController < ApplicationController
def create
profile = @current_user.build_profile(profile_params) if @current_user.profile.nil?
profile.avatar.attach(profile_params[:avatar]) unless profile_params[:avatar].nil?
venue = Venue.find(profile_params[:venue_id])
venue.profiles << profile
profile.save!
render json: @current_user, status: :created
end
private
def profile_params
params.permit(:avatar, :first_name, :last_name, :bio, :phone, :city, :state, :venue_id, :video_url)
end
end
Теперь файл изображения прикреплен и связан с нужной записью. Как отправить его обратно на фронтенд?
Ответ
После того как файл загружен на сервер, настало время отправить его обратно на фронтенд. В действительности на фронтенд отправляется не сам файл, а ссылка на его местоположение на сервере. Работа с этой ссылкой на фронтенде аналогична работе с ссылкой на любом другом URL.
В состав Rails входит помощник для доступа к URL-адресу прикрепленного файла изображения. Сначала добавьте помощник в класс с помощью include
. Затем создайте частный метод с использованием помощника url_for
для доступа к URL-адресу файла изображения.
class ProfileSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :id, :avatar, :first_name, :last_name, :phone, :city, :state, :bio, :video_url
has_one :user
has_one :venue
private
def avatar
url_for(object.avatar) if object.avatar.attached?
end
end
Теперь URL-адрес файла изображения будет сериализован при формировании контроллером JSON.
Заключение
Вот и все! Работа с файлами изображений относительно проста. Использование хуков React, таких как react-dropzone, позволяет очень просто создать элегантный селектор файлов. Конструктор FormData выполняет фронтенд-присоединение, а Active Storage управляет ассоциациями и доступом на стороне сервера. Возврат изображения теперь сводится к возврату URL-адреса изображения. Надеюсь, этот обзор поможет вам. Счастливого кодинга!