Создание компонента React для загрузки изображений
Загрузка и сохранение изображений, созданных пользователями, является очень распространенным вариантом использования в веб-приложениях. Но когда я искал решение, подходящее для моих нужд, я не смог его найти. Все варианты попали в одну из двух категорий: библиотеки с привязкой к поставщику, предоставляемые компаниями по хранению изображений, которые не давали свободы, необходимой мне для моего проекта, или чрезвычайно упрощенные и уродливые варианты, которые не обеспечивали гладкого UX. Я искал.
Теперь вполне возможно, что я просто плохо гуглю, и там есть отличные варианты, однако, будучи предприимчивым, я решил сделать свой собственный с нуля! Итак, вот история о том, как я сделал это простым, но полнофункциональным способом.
Процесс проектирования
Но сначала давайте посмотрим на требования к этому загрузчику изображений:
- Элегантный дизайн, который можно использовать в любых будущих проектах, которые я создаю (возможность повторного использования - ключ к успеху!).
- Настраиваемость для адаптации к различным вариантам использования — круглые изображения профиля и изображения с обычным соотношением сторон являются двумя ключевыми вариантами использования.
- Возможность для пользователя обрезать или редактировать изображения простым способом при загрузке.
- Результатом должна быть строка base64, которую я могу загрузить в базу данных по своему выбору в виде большого двоичного объекта.
- Элементы управления на стороне разработчика для предотвращения злонамеренного использования (ограничение размера/типа файла, ограничение объема загрузки и т.д.)
Итак, с учетом этих 5 ключевых требований я решил приступить к строительству!
Начало проекта
Для начала я использовал свой любимый инструмент командной строки для создания нового проекта React, предоставленного Vite:
npm create vite@latest
Это позволяет вам выбирать из нескольких вариантов, но я выбрал вариант react-ts, поскольку он автоматически настраивает поддержку typescript. Я также выбрал их экспериментальный компилятор rust, поскольку он обеспечивает гораздо лучшую производительность, чем традиционный Webpack, особенно при горячей перезагрузке во время разработки.
После этого я загружаю свои единственные другие 2 зависимости:
- react-cropper, потрясающий проект FOSS, предоставляющий все, что мне нужно для обрезки изображений, и
- storybook, который позволит мне и другим легко протестировать внешний вид моего компонента в браузере на ходу. Хотя технически это не является зависимостью для создания этого загрузчика изображений, он все же необходим для того, чего я хочу достичь.
И все, время кодировать!
Код
Для моего компонента мне нужна функция перетаскивания, а также возможность загрузки по щелчку. Для этого я использовал div
с обработчиком onDrop
, а внутри него добавил традиционный ввод файла:
<div id="drop-zone" onDrop={() => dropHandler(event)} onDragOver={() => dragOverHandler(event)}>
<p id="drop-label">Click or drag a file to <i>upload</i>.</p>
<input id="image-input" type="file" accept=".png,.jpg,.jpeg,.gif" onInput={(e) => {handleFile(e)}} />
{fileInput && <p id="file-name">{fileName}</p>}
</div>
Как вы можете заметить, input
позволяет мне ограничить допустимые типы файлов наиболее распространенными форматами изображений. Но это работает только при клике для загрузки. Для функции dropHandler
я делаю все немного по-другому:
const dropHandler = (ev: any) => {
ev.preventDefault();
if (ev.dataTransfer.items) {
[...ev.dataTransfer.items].forEach((item, i) => {
if (item.kind === "file" && (item.type === "image/png" || item.type === "image/gif" || item.type === "image/jpg" || item.type === "image/jpeg")) {
const file = item.getAsFile();
if(props.sizeLimit && file.size > props.sizeLimit)
{
setStatusMessage("File is too large.");
}
else
{
setFileName(file.name);
getBase64(file);
}
}
else
{
setStatusMessage("Invalid file type.");
}
});
}
}
Хотя это и не самый чистый код, он позволяет мне легко настроить, какие типы файлов я буду принимать. Я также проверяю размер файла по необязательному параметру fileSize
, который может быть передан родительским элементом.
Последняя важная вещь, которую следует отметить здесь, - это моя функция getBase64
, которая преобразует входной файл в строку base64:
const getBase64 = (file: any) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function () {
setFileInput(reader.result);
};
reader.onerror = function (error) {
console.log('Error: ', error);
};
return reader.result;
}
Здесь я использую FileReader
для преобразования файла в строку и сохранения результата в моем состоянии ввода файла.
Для функции обрезки изображения я уже упоминал, что использовал react-cropper
, так как он уже содержит все, что мне нужно. Я могу просто поместить его в элемент dialog
и использовать showModal()
, как только пользователь загрузит изображение:
<dialog ref={dialogRef} id="editor">
<div id={props.round ? "round" : "rect"}>
<Cropper
src={fileInput}
style={{height: 500, width: 500}}
initialAspectRatio={props.aspect}
aspectRatio={props.aspect}
guides={false}
ref={cropperRef}
/>
</div>
<div id="editor-button-row">
<button id="crop-button" onClick={onCrop}>Crop</button>
</div>
</dialog>
Это дает мне следующий вид, в данном случае с реквизитом "round"
, переданным для круглой фотографии:
Пользователь может выбрать обрезанную часть изображения, которую он хочет, и react-cropper выполнит задачу вывода строки base64 результирующего изображения. Затем я сохраняю его в свой объект состояния croppedImage
и закрываю модальное диалоговое окно:
const onCrop = () => {
const cropper = cropperRef.current?.cropper;
setCroppedImage(cropper.getCroppedCanvas().toDataURL());
dialogRef.current?.close();
};
Последний компонент предназначен для отображения обрезанного изображения пользователю. Это позволяет им проверить внешний вид изображения перед его загрузкой. Если нет, они могут очистить изображение и повторить попытку или отредактировать его для повторной обрезки:
<div id="img-display">
<div id="clear-button" onClick={() => {clearFileInput()}}>𐌢</div>
<img id={props.round ? "round" : ""} src={croppedImage} />
<div id="options-row">
<button id="edit-button" onClick={showEditor}>Edit</button>
<button id="save-button" onClick={() => saveImage()}>Save</button>
</div>
</div>
Что дает следующее (опять же, с возможностью выбора круглого изображения):
Единственное, что еще нужно добавить, - это несколько простых вариантов оформления, например, разрешить пользователю выбрать основной цвет, чтобы он мог сочетаться с остальной частью своего приложения. Для этого вы можете ознакомиться с полным репозиторием кода здесь:
https://github.com/Sammortinger/React-ImageUpload
В нашем блоге можно изучить возможность загрузки, обработки и конвертация изображения в React.