Подробное пошаговое руководство по созданию загрузчика файлов с перетаскиванием и перетаскиванием, показывающего ход загрузки в режиме React

Что мы строим

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

Зачем это делать?

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

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

Требования

Прежде чем мы начнем, единственное, что вам нужно, это следующее, чтобы убедиться, что вы можете пройти через все.

  • NodeJS 10.16.1
  • Браузер, поддерживающий FileAPI с FileReader

Шаг 1. Загрузчик файлов Backend API

Прежде чем мы сможем даже создать наш интерфейс, нам понадобится серверная часть, которая принимает загрузку файлов, для этого мы создадим простой Backend API, который принимает файлы с конечной точкой POST в NodeJS.

Создать новый проект NodeJS

# create new folder
mkdir uploadapi;
  
# enter folder
cd uploadd api;
 
# create new file
touch index.js;
# create files directory
mkdir files; 
 
# init
yarn init -y; # or npm init -y;

Установить зависимости

Мы собираемся использовать express для конечных точек и multiparty для обработки данных формы.

yarn add express; # npm install express;
 
yarn add multiparty; # npm install multiparty; # to handle form data
 
yarn add cors; # npm install cors; # allow requests from other ports

Создать приложение

Давайте создадим наше приложение NodeJS:

Файл: /uploadapi/index.js

// Imports
const express = require('express');
const multiparty = require('multiparty');
const app = express();
const port = 5000;
const cors = require('cors');
const fs = require('fs');
const folder = 'files/';
// CORS configurations
app.use(cors());
// Endpoints
app.post('/upload', (req, res) => {
    // initiate multiparty
    const form = new multiparty.Form();
    
    // parse req form data
    return form.parse(req, (err, fields, files) => {
        // error handling
        if (err) {
             return res.status(400).send({error: err });
        }
  
        // path
        const { path } = files.file[0];
   
        // get the temp file name from the tmp folder
        let filename = path.split('/');
        filename = filename[filename.length - 1];
        
        // move file into folder
        return fs.rename(path, `${folder}${filename}`, error => {
             // error handling for moving
             if (error) {
                  return res.status(400).send({ error });
             }
             return res.status(200).send({ file: filename });
        });
    });
});
// Listen
app.listen(port, () => console.log(`Listening on port ${port}`));

Тестовая загрузка файла с помощью почтальона

Теперь, когда у нас есть файл, давайте запустим сервер и протестируем загрузку файла с помощью Postman.

Запустите наш сервер с помощью:

node index.js
# Expected output
# Listening on port 5000

Откройте Почтальон и убедитесь, что настройки установлены следующим образом:

POST http://localhost:5000/upload
Body: form-data
key: file
value: youfile.png

Шаг 2. Создайте приложение React

Теперь, когда у нас есть настройка бэкэнда, нам нужно построить наш интерфейс React с помощью create-react-app:

Создать новую папку проекта

# create new directory
mkdir react-uploader;
# scaffold out create-react-app
npx create-react-app react-uploader;

Удалить ненужные файлы

# Remove files
rm react-uploader/src/App.css;
rm react-uploader/src/App.js;
rm react-uploader/src/App.test.js;
rm react-uploader/src/logo.svg;
rm react-uploader/src/index.css;
# Create blank file
touch react-uploader/src/App.js; 
touch react-uploader/src/index.css;

Шаг 3 - Создание нашего пользовательского интерфейса

Мы собираемся создать наш пользовательский интерфейс так, чтобы у нас была область перетаскивания, которая будет принимать файл, показывать предварительный просмотр (если это изображение) и отображать процент выполнения посередине.

Если разбить это на компоненты, у нас должно получиться что-то вроде этого:

- App
    - ImagePreview 
    - DropArea
        - ImageStatus
        - Status

Создание нашего основного компонента

Наша основная структура для нашего App.js должна быть довольно простой:

Файл: /react-uploader/src/App.js

import React from 'react';
const App = () => {
    return (
        <div className="App">
        </div> 
    );
};
export default App;

Мы могли бы просто изменить наш index.css файл, чтобы настроить поля и отступы браузера по умолчанию:

Файл: /react-uploader/src/index.css

html, body {
    margin: 0;
    padding: 0;
    width: 100vw;
    height: 100vh;  
}

Мы также хотели убедиться, что наш дочерний div для App центрирован с помощью Flexbox:
File: /react-uploader/src/index.css

html, body {
    margin: 0;
    padding: 0;
    width: 100vw;
    height: 100vh;  
}
.App {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100vw;
    height: 100vh;
}

Создать DropArea & Status Component

Затем давайте создадим нашу область перетаскивания и сделаем ее немного меньше, чем основная App, и добавим к ней несколько прозрачных границ, чтобы мы могли учитывать их ширину, когда мы добавляем эффект наведения, чтобы сделать границу цветной.

Файл: /react-uploader/src/App.js

import React from 'react';
const App = () => {
    return (
        <div className="App">
            <div className="DropArea">
                 <div className="Status">Drop Here</div> 
            </div>  
        </div> 
    );
};
export default App;

Файл: /react-uploader/src/index.css

html, body {
    margin: 0;
    padding: 0;
    width: 100vw;
    height: 100vh;  
}
.App {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100vw;
    height: 100vh;
}
.DropArea {
    background: #efefef; 
    display: flex;
    align-items: center;
    justify-content: center; 
    width: calc(80vw - 80px);
    height: calc(80vh - 80px);
    border: solid 40px transparent;
    transition: all 250ms ease-in-out 0s;
    position: relative; 
}
.Status {
    background: transparent; 
    display: block;
    font-family: 'Helvetica', Arial, sans-serif;
    color: black;
    font-size: 60px;
    font-weight: bold;
    text-align: center;  
    line-height: calc(80vh - 80px);
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    transition: all 250ms ease-in-out 0s; 
}

Если мы посмотрим на наш проект до сих пор, он должен выглядеть так:

Компонент ImageStatus

Далее я хочу разместить изображение посередине, чтобы показать изображение, которое мы сейчас загружаем для предварительного просмотра. Мы собираемся использовать это изображение с Unsplash, потому что я люблю Монреаль. Мы поместим его в тот же каталог, что и наш проект React.

В нашем App.js мы создадим наш новый div с вложенным div для хранения изображения.

Файл: /react-uploader/src/App.js

import React from 'react';
import BgImage from './eva-blue-unsplash.jpg';
const App = () => {
    return (
        <div className="App">
            <div className="ImagePreview">
                <div style={{ backgroundImage: `url(${BgImage})` }} />
            </div>
            <div className="DropArea">
                 <div className="Status">Drop Here</div> 
            </div>
        </div> 
    );
};
export default App;

Затем мы изменим CSS, чтобы наше фоновое изображение покрывало весь фон и придало ему размытый вид:

Файл: /react-uploader/src/index.css

...
.App {
    background-size: cover;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100vw;
    height: 100vh;
}
.ImagePreview {
    display: block;
    left: 0;
    right: 0;
    bottom: 0;
    top: 0;
    position: absolute;
    overflow: hidden;
}
.ImagePreview > div {
    position: absolute;
    background-size: cover;
    filter: blur(20px);
    left: -40px;
    right: -40px;
    bottom: -40px;
    top: -40px;
}
...

У нас должно получиться следующее:

Я также хочу иметь возможность показывать прогресс загрузки изображения с его физическим размером, но с использованием маски. Для этого мы будем использовать два изображения, расположенные друг над другом.

Файл: /react-uploader/src/App.js

import React from 'react';
import BgImage from './eva-blue-unsplash.jpg';
const App = () => {
    return (
        <div className="App">
            <div className="ImagePreview">
                <div style={{ backgroundImage: `url(${BgImage})` }}></div>
            </div>
            <div className="DropArea">
                <div className="ImageProgress">
                    <div className="ImageProgressImage" style={{ backgroundImage: `url(${BgImage})` }}></div>
                    <div className="ImageProgressUploaded" style={{ backgroundImage: `url(${BgImage})` }}></div>
                </div> 
                <div className="Status">Drop Here</div> 
            </div>
        </div> 
    );
};
export default App;

В файле css мы будем моделировать прогресс с помощью clip-path.

Файл: /react-uploader/src/index.css

...
.DropArea {
    background: #efefef;
    display: flex;
    align-items: center;
    justify-content: center; 
    width: calc(80vw - 80px);
    height: calc(80vh - 80px);
    border: solid 40px transparent;
    transition: all 250ms ease-in-out 0s;
    position: relative;
}
.ImageProgress {
    display: block;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    position: absolute;
    overflow: hidden;
}
.ImageProgress > .ImageProgressImage {
    opacity: 0.3;
    position: absolute;
    background-position: center center;
    background-size: cover;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
}
.ImageProgress > .ImageProgressUploaded {
    position: absolute;
    background-position: center center;
    background-size: cover;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    clip-path: inset(50% 0 0 0);
}
...

Если мы сохраним нашу работу, у нас должно получиться что-то вроде следующего:

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

Файл: /react-uploader/src/index.js

...
.Status {
    background: rgba(0, 0, 0, 0.3); 
    display: block;
    font-family: 'Helvetica', Arial, sans-serif;
    color: white;
    font-size: 60px;
    font-weight: bold;
    text-align: center;  
    line-height: calc(80vh - 80px);
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    transition: all 250ms ease-in-out 0s;
}

Шаг 4 - предварительный просмотр файла

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

Временная очистка

Сначала нам нужно привести наши div в исходное состояние, поэтому пока мы просто прокомментируем их.

Файл: /react-uploader/src/App.js

import React from 'react';
const App = () => {
    return (
        <div className="App">
            {/* <div className="ImagePreview">
                <div style={{ backgroundImage: `url(${BgImage})` }}>  </div>
            </div> */}
            <div className="DropArea">
                {/* <div className="ImageProgress">
                    <div className="ImageProgressImage" style={{ backgroundImage: `url(${BgImage})` }}></div>
                    <div className="ImageProgressUploaded" style={{ backgroundImage: `url(${BgImage})` }}></div>
                </div>  */}
                <div className="Status">Drop Here</div> 
            </div>
        </div> 
    );
};
export default App;

Мы также хотим сбросить фон и цвет нашего статуса:

Файл: /react-uploader/src/index.css

.Status {
    background: transparent;
    display: block;
    font-family: 'Helvetica', Arial, sans-serif;
    color: black;
    font-size: 60px;
    font-weight: bold;
    text-align: center;  
    line-height: calc(80vh - 80px);
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    transition: all 250ms ease-in-out 0s;
}

Создание обработчиков перетаскивания

Затем мы создадим обработчики событий в главном блоке div className="App", чтобы он мог знать, когда файл был перетащен в окно.

Файл: /react-uploader/src/App.js

...
const App = () => {
    const onDragEnter = event => {
        console.log(event);
        event.preventDefault();
    }
return (
        <div className="App" onDragEnter={onDragEnter}>
...

Теперь, если мы перетащим файл в наше окно (НЕ УДАЛЯЙТЕ ЭТО), вы увидите, что событие запущено:

Затем мы будем использовать useState для управления сообщением о статусе, чтобы дать пользователю обратную связь, как если бы файл был обнаружен.

Файл: /react-uploader/src/App.js

import React, { useState } from 'react';
import BgImage from './eva-blue-unsplash.jpg';
const App = () => {
    const [status, setStatus] = useState('Drop Here');
    const onDragEnter = event => {
        console.log(event);
        setStatus('File Detected');
        event.preventDefault();
    }
...
                <div className="Status">{status}</div> 
            </div>
        </div> 
    );
};
export default App;

Также нам нужно обработать файл, перетаскивающий его за пределы окна:

Файл: /react-uploader/src/App.js

...
    const onDragEnter = event => {
        setStatus('File Detected');
        event.preventDefault();
    }
    const onDragLeave = event => {
        setStatus('Drop Here');
        event.preventDefault();
    }
return (
        <div className="App" onDragEnter={onDragEnter} onDragLeave={onDragLeave}>
...

Теперь мы добавим событие перетаскивания с изменением анимации CSS и событие перетаскивания, чтобы начать процесс чтения файла.

Файл: /react-uploader/src/App.js

...
    const onDragEnter = event => {
        setStatus('File Detected');
        event.preventDefault();
        event.stopPropagation();
    }
    const onDragLeave = event => {
        setStatus('Drop Here');
        event.preventDefault();
    }
    const onDragOver = event => {
        setStatus('Drop');
        event.preventDefault();
    }
    const onDrop = event => {
        console.log(event);
        event.preventDefault();
    }
...
<div className={`DropArea ${status === 'Drop' ? 'Over' : ''}`} onDragOver={onDragOver} onDrop={onDrop} onDragLeave={onDragEnter}>

и измените наш css:

Файл: /react-uploader/src/index.css

...
.DropArea {
    background: #efefef;
    display: flex;
    align-items: center;
    justify-content: center; 
    width: calc(80vw - 80px);
    height: calc(80vh - 80px);
    border: solid 40px transparent;
    transition: all 250ms ease-in-out 0s;
    position: relative;
}
.DropArea.Over {
    border: solid 40px rgba(0, 0, 0, 0.2);
}
...

Теперь при перетаскивании мы увидим следующие состояния:

Исправление события onDrop для компонента приложения

Вы заметите, что если мы слишком быстро перетащим файл в область App, он загрузит изображение. Мы исправим это с помощью event.preventDefault().

Файл: /react-uploader/src/App.js

...
const App = () => {
    const [status, setStatus] = useState('Drop Here');
    const doNothing = event => event.preventDefault();
...
<div className="App" onDragEnter={onDragEnter} onDragLeave={onDragLeave} onDragOver={doNothing} onDrop={onDragLeave}>

Не когда мы перетаскиваем файл в область className="App", ничего не происходит и он сбрасывается как следует.

Обнаружение поддерживаемого файла

Теперь мы можем прочитать файл, когда файл помещен в DropArea, прочитав event.

Файл: /react-uploader/src/App.js

const onDrop = event => {
     const supportedFilesTypes = ['image/jpeg', 'image/png'];
     const { type } = event.dataTransfer.files[0];
     if (supportedFilesTypes.indexOf(type) > -1) {
         // continue with code
     }
     event.preventDefault();
};

Чтение файла

Теперь, когда мы можем подтвердить, что файл является изображением, мы можем прочитать файл и установить для него новое состояние с именем preview.

Файл: /react-uploader/src/App.js

...
const App = () => {
    const [status, setStatus] = useState('Drop Here');
    const [preview, setPreview] = useState(null);
...
const onDrop = event => {
     const supportedFilesTypes = ['image/jpeg', 'image/png'];
     const { type } = event.dataTransfer.files[0];
     if (supportedFilesTypes.indexOf(type) > -1) {
           // Begin Reading File
           const reader = new FileReader();
           reader.onload = e => setPreview(e.target.result);
           reader.readAsDataURL(event.dataTransfer.files[0]);
     }
     event.preventDefault();
};
...

Отображение файла

Чтобы отобразить только что отброшенное изображение, мы будем использовать состояние preview в нашем компоненте.

Файл: /react-uploader/src/App.js

...
<div className="App" onDragEnter={onDragEnter} onDragLeave={onDragLeave} onDragOver={doNothing} onDrop={onDragLeave}>
     <div className={`ImagePreview ${preview ? 'Show' : ''}`}>
          <div style={{ backgroundImage: `url(${preview})` }}></div>
     </div>
     <div className={`DropArea ${status === 'Drop' ? 'Over' : ''}`} onDragOver={onDragOver} onDragLeave={onDragEnter} onDrop={onDrop}>
          <div className={`ImageProgress ${preview ? 'Show' : ''}`}>
               <div className="ImageProgressImage" style={{ backgroundImage: `url(${preview})` }}></div>
               <div className="ImageProgressUploaded" style={{ backgroundImage: `url(${preview})` }}></div>
           </div>
           <div className="Status">{status}</div> 
      </div>
</div>
...

Чтобы добавить немного более плавной анимации, мы изменим наш файл css, добавив transition.

Файл: /react-uploader/src/index.css

...
 
.ImagePreview {
    opacity: 0;
    display: block;
    left: 0;
    right: 0;
    bottom: 0;
    top: 0;
    position: absolute;
    overflow: hidden;
    transition: all 500ms ease-in-out 250ms;
}
.ImagePreview.Show {
    opacity: 1;
}
...
.ImageProgress {
    opacity: 0;
    display: block;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    position: absolute;
    overflow: hidden;
    transition: all 500ms ease-in-out 250ms;
}
.ImageProgress.Show {
    opacity: 1;
}
...

Когда вы его загрузите, у вас должно получиться плавное нарастание изображения.

Шаг 5 - загрузка файла

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

Почему не получить?

К сожалению, Fetch не имеет дескриптора события для прогресса загрузки, поэтому мы будем использовать вместо него XHR, который имеет больше обработчиков событий, то есть больше параметров.

Загрузка файла

Для начала мы просто загрузим файл в обычном режиме, чтобы убедиться, что он может взаимодействовать с нашим сервером.

Файл: /react-uploader/src/App.js

...
    const onDrop = event => {
        const supportedFilesTypes = ['image/jpeg', 'image/png'];
        const { type } = event.dataTransfer.files[0];
        if (supportedFilesTypes.indexOf(type) > -1) {
            // Begin Reading File
            const reader = new FileReader();
            reader.onload = e => setPreview(e.target.result);
            reader.readAsDataURL(event.dataTransfer.files[0]);
            // Create Form Data
            const payload = new FormData();
            payload.append('file', event.dataTransfer.files[0]);
            // XHR - New XHR Request
            const xhr = new XMLHttpRequest();
            // XHR - Make Request  
            xhr.open('POST', 'http://localhost:5000/upload');
            xhr.send(payload);
        }
        event.preventDefault();
    };
...

Теперь, если вы перетащите изображение в область перетаскивания, вы увидите, что файл успешно отправлен на ваш сервер.

Шаг 6 - События XHR

Далее мы воспользуемся событием, которое запускается по мере постепенной загрузки нашего файла. Это с .upload.onprogress. Мы воспользуемся этим как для отображения процента, так и для визуального отображения процента.

Файл: /react-uploader/src/App.js

...
const App = () => {
    const [status, setStatus] = useState('Drop Here');
    const [percentage, setPercentage] = useState(0);
...
    const onDrop = event => {
        const supportedFilesTypes = ['image/jpeg', 'image/png'];
        const { type } = event.dataTransfer.files[0];
        if (supportedFilesTypes.indexOf(type) > -1) {
            // Begin Reading File
            const reader = new FileReader();
            reader.onload = e => setPreview(e.target.result);
            reader.readAsDataURL(event.dataTransfer.files[0]);
            // Create Form Data
            const payload = new FormData();
            payload.append('file', event.dataTransfer.files[0]);
            // XHR - New XHR request
            const xhr = new XMLHttpRequest();
            // XHR - Upload Progress
            xhr.upload.onprogress = (e) => {
                const done = e.position || e.loaded
                const total = e.totalSize || e.total;
                const perc = (Math.floor(done/total*1000)/10);
                if (perc >= 100) {
                    setStatus('Done');
                } else {
                    setStatus(`${perc}%`);
                }
                setPercentage(perc); 
            };
            // XHR - Make Request
            xhr.open('POST', 'http://localhost:5000/upload');
            xhr.send(payload);
        }
        event.preventDefault();
    };
...

Мы также собираемся изменить изображение процесса загрузки, чтобы отразить это новое процентное состояние.

Файл: /react-uploader/src/App.js

...
<div className={`ImageProgress ${preview ? 'Show' : ''}`}>
     <div className="ImageProgressImage" style={{ backgroundImage: `url(${preview})` }}></div>
     <div className="ImageProgressUploaded" style={{ backgroundImage: `url(${preview})`, clipPath: `inset(${100 - Number(percentage)}% 0 0 0);` }}></div>
</div> 
...

и измените этот CSS из нашей таблицы стилей, чтобы установить начальное состояние и добавить переход для более плавного движения:

Файл: /react-uploader/src/index.css

...
.ImageProgress > .ImageProgressUploaded {
    position: absolute;
    background-position: center center;
    background-size: cover;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    clip-path: inset(0% 0 0 0);
    transition: all 250ms ease-in-out 0ms; 
}
...

Если вы попытаетесь перетащить файл, загрузка произойдет очень быстро, потому что сервер подключается к вашему компьютеру. Так что измените это, мы собираемся настроить скорость вращения дроссельной заслонки на Fast 3G в наших инструментах разработчика:

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

Вот и все. У вас есть загрузчик файла прогресса перетаскиванием. Следующие несколько шагов - это более эстетичная очистка и лучшая обработка состояния, но если вы получили от этого пользу, перейдите к шагу Отсюда, в противном случае перейдите к Шагу 7 - Очистка .

Шаг 7 - Очистка

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

Предотвращение дальнейшего перетаскивания при загрузке

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

Файл: /react-uploader/src/App.js

...
const App = () => {
    const [status, setStatus] = useState('Drop Here');
    const [percentage, setPercentage] = useState(0);
    const [preview, setPreview] = useState(null);
    const [enableDragDrop, setEnableDragDrop] = useState(true);
...
const onDragEnter = event => {
        if (enableDragDrop) {
            setStatus('File Detected');
        }
        event.stopPropagation();
        event.preventDefault();
    };
...
    const onDragLeave = event => {
        if (enableDragDrop) {
            setStatus('Drop Here');
        }
        event.preventDefault();
    };
...
    const onDragOver = event => {
        if (enableDragDrop) {
            setStatus('Drop');
        }
        event.preventDefault();
    };
...
    const onDrop = event => {
        const supportedFilesTypes = ['image/jpeg', 'image/png'];
        const { type } = event.dataTransfer.files[0];
        if (supportedFilesTypes.indexOf(type) > -1 && enableDragDrop) {
...
            // XHR - Upload Progress
            xhr.upload.onprogress = (e) => {
                const done = e.position || e.loaded
                const total = e.totalSize || e.total;
                const perc = (Math.floor(done/total*1000)/10);
                if (perc >= 100) {
                    setStatus('Done');
                    setEnableDragDrop(true);
                } else {
                    setStatus(`${perc}%`);
                }
                setPercentage(perc);
            };
...
            // XHR - Make Request
            xhr.open('POST', 'http://localhost:5000/upload');
            xhr.send(payload);
            setEnableDragDrop(false);
        }
        event.preventDefault();
    };

Сбросить по окончании загрузки

Файл: /react-uploader/src/App.js

...
            // XHR - Upload Progress
            xhr.upload.onprogress = (e) => {
                const done = e.position || e.loaded
                const total = e.totalSize || e.total;
                const perc = (Math.floor(done/total*1000)/10);
                if (perc >= 100) {
                    setStatus('Done');
                    // Delayed reset
                    setTimeout(() => {
                        setPreview(null);
                        setStatus('Drop Here');
                        setPercentage(0);
                        setEnableDragDrop(true);
                    }, 750); // To match the transition 500 / 250
                    
                } else {
                    setStatus(`${perc}%`);
                }
                setPercentage(perc);
            };
...

Изменить статус для лучшей видимости

Я хочу, чтобы наш процент и наш статус при загрузке были более заметными.

Файл: /react-uploader/src/App.js

...
<div className={`Status ${status.indexOf('%') > -1 || status === 'Done' ? 'Uploading' : ''}`}>{status}</div>
...

Файл: /react-uploader/src/index.css

...
.Status {
    background: transparent;
    display: block;
    font-family: 'Helvetica', Arial, sans-serif;
    color: black;
    font-size: 60px;
    font-weight: bold;
    text-align: center;  
    line-height: calc(80vh - 80px);
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    transition: all 250ms ease-in-out 0s;
}
.Status.Uploading {
    background: rgba(0, 0, 0, 0.3);
    color: white;
}
...

Скрывая наши белые границы

Последнее, что я хочу сделать, это скрыть наши белые границы после завершения процесса загрузки.

Файл: /react-uploader/src/App.js

...
<div className={`DropArea ${status === 'Drop' ? 'Over' : ''} ${status.indexOf('%') > -1 || status === 'Done' ? 'Uploading' : ''}`} onDragOver={onDragOver} onDragLeave={onDragEnter} onDrop={onDrop}>
    <div className={`ImageProgress ${preview ? 'Show' : ''}`}>
...

Файл: /react-uploader/src/index.css

...
.DropArea.Over {
    border: solid 40px rgba(0, 0, 0, 0.2);
}
.DropArea.Uploading {
    border-width: 0px;
}
...

Добавление кнопки прерывания загрузки

Я хочу убедиться, что у нас есть способ полностью прервать процесс загрузки и предоставить пользователю больше возможностей / действий при загрузке файлов.

Файл: /react-uploader/src/App.js

...
const [enableDragDrop, setEnableDragDrop] = useState(true);
const [stateXhr, setStateXhr] = useState(null);
...
            // XHR - Make Request
            xhr.open('POST', 'http://localhost:5000/upload');
            xhr.send(payload);
            setStateXhr(xhr);
            setEnableDragDrop(false);
        }
...
    const onAbortClick = () => {
        stateXhr.abort();
        setPreview(null);
        setStatus('Drop Here');
        setPercentage(0);
        setEnableDragDrop(true);
    };
...
                <div className={`Status ${status.indexOf('%') > -1 || status === 'Done' ? 'Uploading' : ''}`}>{status}</div>
 
                {status.indexOf('%') > -1 && <div className="Abort" onClick={onAbortClick}><span>&times;</span></div>}
            </div>
        </div> 
    );
};
export default App;

Файл: /react-uploader/src/index.css

...
.Abort {
    background: rgba(255, 0, 0, 0.5);
    display: block;
    position: absolute;
    top: 0;
    right: 0;
    width: 50px;
    height: 50px;
    clip-path: polygon(0 0, 100% 100%, 100% 0);
    transition: all 250ms ease-in-out 0s;
    cursor: pointer;
}
.Abort:hover {
    background: rgba(255, 0, 0, 1);
}
.Abort > span {
    color: white;
    font-family: 'Helvetica', Arial, sans-serif;
    font-weight: bold;
    font-size: 24px;
    height: 28px;
    width: 22px;
    line-height: 28px;
    position: absolute;
    top: 0;
    right: 0;
}

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

Отсюда

Мы делаем еще несколько вещей, чтобы улучшить ситуацию:

  • Ограничьте предварительный просмотр файлов - таким образом он перехватывает слишком большие файлы, чтобы они не загружали память браузера.
  • Обработка других данных, таких как xls и pdf. Файлы CSV можно читать в исходном формате, а файлы PDF можно предварительно просмотреть определенным образом в определенных браузерах.

Это было довольно долгое пошаговое руководство, так что поздравляю с прочтением до сих пор.

Если вы получили ценность от, пожалуйста, похвалите эту статью 👏❤️.️

Если вы думаете, что это можно улучшить, дайте мне знать в комментариях.

Пожалуйста, поделитесь им в Twitter 🐦 или других социальных сетях. Еще раз спасибо за чтение. 🙏

Также подпишитесь на меня в twitter: @codingwithmanny и instagram на @codingwithmanny.