Пришло время заставить React творить великие дела

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

Вот в чем дело: мы можем легко показать им ввод файла, позволить им выбрать файлы, используя элемент HTML по умолчанию, попросить их отправить файлы и закончить день. Но что происходит между ними? Что пользователи хотят видеть, когда что-то происходит? Интерфейс, который им ничего не говорит, или интерфейс, который сообщает им все?

Что, если у пользователя отключится интернет? Что делать, если сервер ничего не отвечает? Что, если файл 8 из 14 для них большой? Что, если пользователь ждал завершения процесса загрузки в течение 10 минут и хотел посмотреть, как далеко он продвинулся? Или какие файлы уже были загружены?

В предыдущем руководстве (вы можете найти его, если поискать в моих сообщениях) я рассмотрел создание логики для размещения этого API. Смысл этого поста был в том, чтобы научить логике. Вы можете остановиться на этом и создать на его основе свой собственный пользовательский интерфейс. Или вы можете создать логическую часть самостоятельно и прочитать этот фрагмент, чтобы узнать, как реализовать UX для любого компонента загрузки файлов. Эти сообщения были созданы по двум отдельным причинам, но полностью совместимы. Я просто собираюсь представить логику в этом посте, чтобы мы могли сосредоточиться на пользовательском интерфейсе. Решение за вами!

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

Я подумывал использовать свою любимую библиотеку CSS styled-components для создания этого руководства, однако решил не делать этого, чтобы продемонстрировать, как можно создать сложный пользовательский интерфейс без дополнительных инструментов. Инструменты - это просто удобство. Вам просто нужно немного изучить CSS, не инструменты.

И последнее, но не менее важное: вот предварительный просмотр того, что мы будем строить в этом посте:

Без лишних слов, приступим!

В этом руководстве мы собираемся быстро сгенерировать проект React с помощью create-react-app.

Идите вперед и создайте проект, используя команду ниже. В этом руководстве я назову наш проект upload-app:

npx create-react-app upload-app

Как только это будет сделано, перейдите в его каталог:

cd upload-app

Я пообещал просто предоставить логику реализации загрузки файлов, чтобы мы могли сразу приступить к созданию пользовательского интерфейса. Итак, вот настраиваемый хук, который мы будем использовать, под названием useApp.js:

src / useApp.js

Объяснение

Вот краткое изложение того, что здесь происходит.

Когда пользователи выбирают файлы, вызывается обработчик onChange. Аргумент e содержит нужные нам файлы, доступные e.target.files. Это будут файлы, которые будут отображаться один за другим в интерфейсе. Однако этот files объект не является массивом - это на самом деле FileList. Это проблема, потому что мы не можем просто нанести на карту это, иначе мы получим ошибку. Поэтому мы конвертируем его в массив и прикрепляем к state.files, позволяя пользовательскому интерфейсу отображать их строка за строкой в ​​пользовательском интерфейсе. Когда пользователь отправляет форму, вызывается обработчик onSubmit. Он отправляет действие, которое посылает сигнал одному или нескольким useEffects, что пора начинать. Их несколько useEffects, и каждому из них поставлены разные задачи и условия. Один используется для запуска потока, один используется для продолжения потока, а третий используется для завершения потока.

Что мы собираемся сделать дальше, так это открыть файл App.js и заменить код по умолчанию на:

А вот и наш стартовый файл CSS: src/styles.css

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

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

Когда вы нажимаете кнопку «Отправить», вы можете проверить сообщения консоли, чтобы убедиться, что они обрабатываются одно за другим:

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

Проблема в том, что пользователь не знает, что происходит - он может ждать 10 минут, а страница все равно останется прежней.

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

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

В настоящее время наш ввод файла выглядит так:

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

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

Создайте файл с именем FileUploader.js и поместите в него этот код:

Входной файл real является потомком root div element. triggerInput - это функция, которая позволяет нам подключаться к ссылке inputRef, прикрепленной к элементу file input. Мы скоро рассмотрим это в крючке.

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

Внутри нашего хука мы определили обработчик triggerInput внутри: src/useApp.js

const triggerInput = (e) => {
  e.persist()
  inputRef.current.click()
}

Вернуть его в конце, чтобы вызывающий абонент мог получить к нему доступ: src/useApp.js

return {
  ...state,
  onSubmit,
  onChange,
  triggerInput,
}

Большой! Теперь мы собираемся создать компонент, который будет маскироваться под реальный ввод файла. Это может быть что угодно, но для целей данного руководства это будет мини-«экран» для пользователя, который поможет ему загрузить свои файлы и перенести их на следующий экран с помощью графических и текстовых файлов. обновления. Поскольку мы выполняли рендеринг children в методе рендеринга FileUploader, мы можем отобразить этот экран как дочерний элемент FileUploader. Мы хотим, чтобы весь этот экран мог открывать файловый браузер, когда нам это нужно.

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

Создайте еще один файл с именем FileUploaderScreen.js:

Вот стили, которые я использовал для компонента:

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

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

Давайте теперь поместим эти FileUploader и FileUploaderScreen в наш App.js файл:

src / App.js

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

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

Как мы это делаем?

Здесь мы должны использовать свойство состояния status, которое мы определили ранее в нашем настраиваемом хуке:

const initialState = {
  files: [],
  pending: [],
  next: null,
  uploading: false,
  uploaded: {},
  status: IDLE,
}

Если вы посмотрите на наши useEffects и reducer, мы сделали действия отправки useEffects в зависимости от того, что происходило:

src / useApp.js

src / useApp.js

Вдобавок, если вы посмотрите на обработчик onChange, вы увидите, что отправляется один из следующих типов действий:

Поскольку мы знаем, что отправка 'load' обновит state.status до 'LOADED', мы можем использовать это в нашем FileUploaderScreen для изменения изображений всякий раз, когда state.status обновляется до 'LOADING'.

Итак, что мы сделаем, так это воспользуемся случаем переключения, чтобы назначить src свойству стиля backgroundImage в зависимости от значения state.status:

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

Каждый раз, когда пользователь что-то делает, изображение будет другим. Это сделано для того, чтобы мы не утомляли пользователя, чтобы он был постоянно занят. Делайте все, что хотите, чтобы они оставались на вашем сайте, а не уходили. Просто держите его в рейтинге G, конечно :).

В любом случае, если вы попытаетесь выбрать файлы прямо сейчас, экран не обновится. Это потому, что нам нужно передать свойство status в FileUploaderScreen:

src / App.js

Не знаю, как вы, но я действительно думаю, что теперь нужно заняться этими уродливыми, непропорциональными эскизами. Это уже не 90-е, у нас есть React!

Итак, что мы собираемся сделать, так это масштабировать их, чтобы они поместились в компоненты строки файла (список строк). В каждой строке эскиз будет иметь размер 50 пикселей в ширину и 50 пикселей в высоту. Это гарантирует, что у нас будет достаточно места справа, чтобы отображать имя файла и размеры файлов для пользователя в чистом и профессиональном виде.

Создайте новый файл с именем FileRow.js и добавьте его в:

Стили, которые я использовал:

Вот что происходит:

  1. Мы определили компонент FileRow, который получит необходимые реквизиты для рендеринга его дочерних компонентов. file, src, id и index поступают из массива state.files, установленного onChange внутри нашего useApp пользовательского хука.
  2. Цель isUploading - отобразить текст «Загрузка…» и индикатор загрузки прямо над ним во время загрузки файла.
  3. Цель isUploaded - затенять строки, когда их файловый объект находится внутри state.uploaded - сопоставлен по их идентификатору. (Вот почему у нас был state.uploaded, если вам интересно)
  4. Поскольку мы не хотим, чтобы каждая строка отображалась каждый раз при обновлении состояния, нам пришлось обернуть ее React.memo, чтобы запоминать свойства, чтобы они обновлялись только при index , isUploading или isUploaded изменения. Пока эти файлы загружаются, эти свойства никогда не изменятся, если только не произойдет что-то важное, поэтому можно безопасно применять эти условия.
  5. getReadableSizeFromBytes был предоставлен, чтобы мы отображали удобочитаемый размер файла. В противном случае пользователи будут читать числа вроде 83271328.
  6. Spinner - это счетчик загрузки.

Для этого урока я использовал react-md-spinner. Кроме того, я использовал пакет classnames для комбинирования / условного рендеринга имен классов для условного стиля для большей простоты управления.

Примечание: если вы решите продолжить с помощью response-md-spinner / classnames и получите эту ошибку:

Cannot find module babel-preset-react-app/node_modules/@babel/runtime

Тогда вам может потребоваться установить @babel/runtime.

src / Spinner.js

Стили, которые я использовал:

Теперь, если вы попытаетесь выбрать файлы, интерфейс выглядит намного более плавным, чем раньше:

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

Хитрость здесь в том, чтобы использовать мощное свойство state.status, как мы это сделали с поворотами изображения.

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

Перейдите к файлу FileUploaderScreen.js и начните с условного рендеринга компонента init / idle:

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

src / FileUploaderScreen.js

Теперь должно быть легче увидеть:

Используя ту же концепцию, что и ранее с компонентом Init, мы можем реализовать остальные компоненты таким же образом:

src / FileUploaderScreen.js

Вот все стили, использованные для них:

Компонент Загружен отображается, когда значение state.status равно "LOADED". Странно то, что кнопка «Загрузить еще» обернута FileUploader, который мы создали в начале. «Что это там делает?» вы можете спросить.

После того, как экран загрузки файла прошел начальный этап, мы больше не хотим, чтобы весь компонент запускал файловый браузер. Я очень скоро еще немного остановлюсь на этом.

Компонент Pending используется, чтобы показать, что загрузка в процессе, чтобы они знали, что что-то происходит, пока они ждут. Эта часть очень важна для наших пользователей!

Компонент Успех отображается сразу после завершения процесса загрузки.

И, наконец, компонент Ошибка отображается, когда при загрузке произошла ошибка. Это должно помочь пользователю понять текущую ситуацию, не заставляя его узнавать сами.

Следующее, что мы собираемся сделать, это обновить App.js:

src / App.js

Мы добавили новую функцию getFileUploaderProps в наш хук useApp:

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

Вот почему у нас есть это в компоненте App:

const initialFileUploaderProps = getFileUploaderProps({
  triggerInput: status === 'IDLE' ? triggerInput : undefined,
  onChange: status === 'IDLE' ? onChange : undefined,
})

И использовал его для распространения своих аргументов в FileUploader:

<FileUploader {...initialFileUploaderProps}>
  <FileUploaderScreen
    triggerInput={triggerInput}
    getFileUploaderProps={getFileUploaderProps}
    files={files}
    pending={pending}
    status={status}
  />
</FileUploader>

Теперь в FileUploader будут переданы все 4 аргумента, как обычно, но будут иметь значения undefined из props.triggerInput и props.onChange для остальных экранов. В реакции обработчики onClick не срабатывают, если они undefined. Это отключает обработчик кликов, поэтому вместо этого мы можем назначить кнопку Загрузить еще в качестве нового обработчика для выбора файлов.

Вот как выглядит приложение сейчас:

Все идет нормально. Но похоже, что счетчик загрузки в списке строк файлов неловко отодвигает вещи в сторону, когда их файл загружается.

Вы заметили, что к компоненту Spinner применено свойство flex-center?

Да, нам не хватает CSS. Так что давайте вставим это прямо в файл CSS:

Заключение

На этом мы подошли к концу этого урока! Если вы хотите увидеть бонус (зеленая стрелка, указывающая вниз на кнопку Следующая страница), вы можете увидеть реализацию в исходном коде на GitHub здесь.

Прошу прощения за поспешность в конце этого урока. Я не был уверен, становится ли он слишком длинным или слишком скучным :) Дайте мне знать, как этот урок прошел для вас!

Спасибо за чтение, и я надеюсь, что вы с нетерпением ждете новых качественных сообщений от меня в будущем!