Привет друг! В этой статье попробуем реализовать компонент - индикатор загрузки для отправленного документа с клиента.
Чего мы хотим добиться? тут 🥸.
И так - поехали !🛼
1. Создаём компоненту FileUpload.tsx.
Накидаем сходу немного стилей. И спрячем input type="file", отрисовав вместо него кнопку - 'Выбрать файл'. Подготовим состояние для сохранения файла.
import {ChangeEvent, FC, useRef, useState} from 'react';
export const FileUpload = () => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setSelectedFile(event.target.files[0]);
}
};
const handleUpload = async () => {
if (!selectedFile) {
alert('Please select a file to upload!');
return;
}
};
const handleButtonClick = () => {
// Триггерим клик по скрытому input
inputRef.current?.click();
};
return (
<div style={{width: '300px', margin: '0 auto', textAlign: 'center', height: '100%'}}>
<div style={{display: 'flex', flexDirection: 'column', gap: '10px'}}>
<input
type="file"
ref={inputRef}
onChange={handleFileChange}
style={{
display:'none',
}}
/>
<button
type="button"
onClick={handleButtonClick}
style={{
padding: '10px 20px',
backgroundColor: '#4CAF50',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
fontSize: '16px',
transition: 'background-color 0.3s ease',
}}
>
Выбрать файл
</button>
{selectedFile && <p style={{fontSize: '14px', color: '#333'}}>Выбран файл: {selectedFile.name}</p>}
<button
onClick={handleUpload}
disabled={!selectedFile}
style={{
padding: '10px 20px',
backgroundColor: `${!selectedFile ? '#898f89' : '#4CAF50'}`,
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: `${!selectedFile ? 'no-drop' : 'pointer'}`,
fontSize: '16px',
transition: 'background-color 0.3s ease',
}}
>
Загрузить
</button>
</div>
</div>
);
};
2. Дополняем состояния "полосы загрузки", флажок загружен или нет. А так же пишем фейковый запрос.
- Доп состояния:
const [uploadProgress, setUploadProgress] = useState<number | null>(null);
const [isUploading, setIsUploading] = useState<boolean>(false);
- Пишем фейковый запрос:
const fakeUploadRequest = async (file: File) => {
const fileSizeInMB = file.size / (1024 * 1024); // Размер файла в мегабайтах
const uploadSpeed = 1; // Условная скорость "загрузки" (МБ/с)
const totalUploadTime = fileSizeInMB / uploadSpeed; // Общее время загрузки в секундах
const interval = 100; // Интервал обновления прогресса (мс)
const totalSteps = Math.ceil((totalUploadTime * 1000) / interval);
let currentStep = 0;
return new Promise<void>((resolve) => {
const intervalId = setInterval(() => {
currentStep++;
const progress = Math.min((currentStep / totalSteps) * 100, 100);
setUploadProgress(progress);
if (progress === 100) {
clearInterval(intervalId);
resolve();
}
}, interval);
});
};
- Дописываем существующие функции handleFileChange и handleUpload:
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setSelectedFile(event.target.files[0]);
setUploadProgress(null); // Сбрасываем прогресс при выборе нового файла
}
};
const handleUpload = async () => {
if (!selectedFile) {
alert('Please select a file to upload!');
return;
}
setIsUploading(true);
try {
await fakeUploadRequest(selectedFile);
alert('File uploaded successfully!');
} catch (error) {
console.error('Error during upload simulation:', error);
alert('Failed to upload file!');
} finally {
setIsUploading(false);
setSelectedFile(null);
}
};
3. Как может выглядеть реальный запрос:
Можем использовать отца всех современных инструментов для взаимодействия с серверной частью апки - XMLHttpRequest():
const uploadFileToServer = async (
file: File,
onProgress: (progress: number) => void,
) => {
return new Promise<void>((resolve, reject) => {
const formData = new FormData();
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://your-server-url.com/upload', true);
// Отслеживание прогресса загрузки
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100);
onProgress(progress); // Обновляем прогресс через callback
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
} else {
reject(new Error(`Upload failed: ${xhr.statusText}`));
}
};
xhr.onerror = () => reject(new Error('Network error'));
xhr.send(formData);
});
};
const handleUploadRequest = async (file: File) => {
await uploadFileToServer(file, setUploadProgress);
};
Ну или axios:
import axios from 'axios';
const uploadFileWithAxios = async (
file: File,
onProgress: (progress: number) => void
) => {
const formData = new FormData();
formData.append('file', file);
await axios.post('https://your-server-url.com/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (event) => {
if (event.total) {
const progress = Math.round((event.loaded / event.total) * 100);
onProgress(progress); // Обновляем прогресс
}
},
});
};
Ну или еще чего нибудь😎 Если совсем отчаянный можно использовать fetch. Но fetch не предоставляет прямой возможности отслеживания прогресса загрузки файла. Однако можно комбинировать fetch с низкоуровневым API ReadableStream для реализации аналогичного функционала. Ну и в таком случаем каждый чанк будет отправлять запрос, да и сервер придётся подготовить для подобных танцев с бубнами 🙂 Короч - разберёшься.
4. Дописываем оставшуюся разметку для полосочки загрузки и для цифровой индикации той самой загрузки:
{uploadProgress !== null && (
<div style={{marginTop: '20px'}}>
<div
style={{
width: '100%',
height: '20px',
backgroundColor: '#e0e0e0',
borderRadius: '5px',
overflow: 'hidden',
}}
>
<div
style={{
width: `${uploadProgress}%`,
height: '100%',
backgroundColor: '#76c7c0',
transition: 'width 0.1s ease',
}}
/>
</div>
<p style={{color: 'black'}}>{uploadProgress.toFixed(2)}%</p>
</div>
)}
5. Собираем всё вместе. Итоговый код:
import {ChangeEvent, useRef, useState} from 'react';
type Nullable<T> = null | T;
export const FileUpload = () => {
const [selectedFile, setSelectedFile] = useState<Nullable<File>>(null);
const [uploadProgress, setUploadProgress] = useState<Nullable<number>>(null);
const [isUploading, setIsUploading] = useState<boolean>(false);
const inputRef = useRef<Nullable<HTMLInputElement>>(null);
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setSelectedFile(event.target.files[0]);
setUploadProgress(null);
}
};
const fakeUploadRequest = async (file: File) => {
const fileSizeInMB = file.size / (1024 * 1024);
const uploadSpeed = 1;
const totalUploadTime = fileSizeInMB / uploadSpeed;
const interval = 100;
const totalSteps = Math.ceil((totalUploadTime * 1000) / interval);
let currentStep = 0;
return new Promise<void>((resolve) => {
const intervalId = setInterval(() => {
currentStep++;
const progress = Math.min((currentStep / totalSteps) * 100, 100);
setUploadProgress(progress);
if (progress === 100) {
clearInterval(intervalId);
resolve();
}
}, interval);
});
};
const handleUpload = async () => {
if (!selectedFile) {
alert('Please select a file to upload!');
return;
}
setIsUploading(true);
try {
await fakeUploadRequest(selectedFile);
alert('File uploaded successfully!');
} catch (error) {
console.error('Error during upload simulation:', error);
alert('Failed to upload file!');
} finally {
setIsUploading(false);
setSelectedFile(null);
}
};
const handleButtonClick = () => {
inputRef.current?.click();
};
return (
<div style={{width: '300px', margin: '0 auto', textAlign: 'center', height: '100%'}}>
<div style={{display: 'flex', flexDirection: 'column', gap: '10px'}}>
<input
type="file"
ref={inputRef}
onChange={handleFileChange}
style={{
display:'none',
}}
/>
<button
type="button"
onClick={handleButtonClick}
style={{
padding: '10px 20px',
backgroundColor: '#4CAF50',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
fontSize: '16px',
transition: 'background-color 0.3s ease',
}}
onMouseOver={(e) => (e.currentTarget.style.backgroundColor = '#45a049')}
onMouseOut={(e) => (e.currentTarget.style.backgroundColor = '#4CAF50')}
>
Выбрать файл
</button>
{selectedFile && <p style={{fontSize: '14px', color: '#333'}}>Выбран файл: {selectedFile.name}</p>}
<button
onClick={handleUpload}
disabled={isUploading || !selectedFile}
style={{
padding: '10px 20px',
backgroundColor: `${isUploading || !selectedFile ? '#898f89' : '#4CAF50'}`,
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: `${isUploading || !selectedFile ? 'no-drop' : 'pointer'}`,
fontSize: '16px',
transition: 'background-color 0.3s ease',
}}
>
{isUploading ? 'Загрузка...' : 'Загружен'}
</button>
</div>
{uploadProgress !== null && (
<div style={{marginTop: '20px'}}>
<div
style={{
width: '100%',
height: '20px',
backgroundColor: '#e0e0e0',
borderRadius: '5px',
overflow: 'hidden',
}}
>
<div
style={{
width: `${uploadProgress}%`,
height: '100%',
backgroundColor: '#76c7c0',
transition: 'width 0.1s ease',
}}
/>
</div>
<p style={{color: 'black'}}>{uploadProgress.toFixed(2)}%</p>
</div>
)}
</div>
);
};
Понятное дело, логику лучше вынести из компоненты, ну и в принципе для вариативности (использовании в нескольких местах - ну мало ли ≽^•༚• ྀི≼ ) пропсами закидывать асинк функшн, и uploadProgress со всеми вытекающими! Да и вообще мы молодцы 🧸!
Спасибо за внимание! До Новых Встреч!🤗🤗🤗