После более 600 уроков и лабораторных работ и 4 проектов портфолио, пришло время поговорить о моем последнем проекте портфолио. Когда я записывался на онлайн-курс по разработке программного обеспечения для самостоятельного изучения в Flatiron School, я знал, что мне нужно будет завершить пять портфельных проектов, чтобы получить высшее образование. Финальный проект всегда казался мне чем-то далеким, требующим столько навыков, которых у меня еще не было. Но вот он, готов к отправке на рассмотрение.

Для этого финального проекта портфолио цель состоит в том, чтобы создать стилизованное одностраничное приложение, состоящее из интерфейса React.js и Redux.js, которое выполняет вызовы Ruby on Rails API.

Одностраничное приложение

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

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

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

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

Построение структуры приложения

Приложение разделено на две части: внешнюю и внутреннюю. Внешний интерфейс - это часть, с которой взаимодействует пользователь: интерфейс. Серверная часть управляет подключениями между сервером и пользовательским интерфейсом. Есть два варианта сборки приложения:

  • Первый вариант состоит в создании одного репозитория кода - например, на Github - для внутреннего и внешнего интерфейса.
  • Второй вариант заключается в создании двух репозиториев кода: одного для серверной части, а другого - для интерфейса. У этого есть несколько преимуществ. Два из них заключаются в том, что, с одной стороны, он позволяет использовать серверную часть - API в нашем случае - повторно столько раз, сколько мы хотим, с таким количеством интерфейсов, сколько мы хотите, а с другой стороны, позволяет управлять небольшими каталогами в нашем текстовом редакторе.

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

Первый репозиторий, который я построил, был back-end. В моем терминале я использовал приведенную ниже команду для создания приложения Rails в качестве API без каких-либо представлений. Это похоже на создание обычного приложения Rails с дополнительным флагом.

rails new my_app_backend --api

Что касается внешнего интерфейса, я использовал генератор create-react-app, чтобы начать работу.

npx create-react-app my_app_frontend

Эти две команды дали мне все необходимые файлы для начала.

О типе компонента

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

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

Компоненты контейнера также известны как компоненты с сохранением состояния, а компоненты без состояния - также как компоненты представления. Разница между контейнерными и презентационными компонентами не строгая, а скорее произвольная, и каждый разработчик может организовать свой компонент по своему усмотрению. Но, вообще говоря, компонент-контейнер будет иметь состояние и сможет отслеживать свои изменения, а компонент представления не будет иметь state, он будет либо отображать содержимое, переданное через props, либо всегда отображать одно и то же.

В случае приложения, которое мы обсуждаем в этой статье, я выбрал базовое разделение. Я создал компонент контейнера для каждой модели, присутствующей в моем API. По мере продвижения вперед я удалил некоторые из них, в которых не было необходимости, и добавил другие. В основном мои компоненты с отслеживанием состояния - это формы. Лучшими примерами являются форма регистрации и форма входа. В форме в React для каждого ввода пользователя происходит изменение в состоянии, независимо от того, является ли это локальным состоянием или Redux store state (мы обсудим Redux немного позже). Примером компонента без состояния может быть список велосипедов, который находится в BicyclesList.js. Этот компонент получает как props список городов из компонента CitiesContainer и не имеет никакого отношения к состоянию. .

Маршрутизация в React

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

Как всегда, у нашей проблемы есть решение. А вот и библиотека response-router. Помимо прочего, это позволяет нам убедиться:

  • У нас есть URL, отображающий что делает пользователь, а не только основной корневой URL.
  • Пользователь может использовать кнопки перехода вперед и назад в браузере.
  • Пользователь может ввести URL-адрес в адресную строку и перейти для просмотра страницы.

Чтобы объяснить шаблон, который я использовал для своих маршрутов, я буду использовать в качестве примера CitiesContainer:

App.js
import React from 'react';
import { Route } from 'react-router-dom';
import CitiesContainer from './containers/CitiesContainer';
class CitiesContainer extends React.Component {
  render() {
    return (
      <Route path='/cities' component={CitiesContainer} />
    )
  }
}
export default App;

При использовании пути, а не точного пути будут учтены все пути, содержащие "/ cities". Внутри компонента CitiesContainer:

containers/CitiesContainer.js 
import React from 'react';
import { connect } from 'react-redux';
import { Route } from 'react-router-dom';
import { fetchCities } from '../actions/fetchCities';
import CitiesList from '../components/CitiesList';
import CityPage from '../components/CityPage';
import BicyclesList from '../components/BicyclesList';
class CitiesContainer extends React.Component {
  componentDidMount() {
    this.props.fetchCities()
  } 
  
  render() {
    return (
      <div>
        <Route exact path='/cities' render={() => <CitiesList 
         cities={this.props.cities} />} />
        <Route exact path='/cities/:id' render={(routerProps) => 
         <CityPage {...routerProps} cities={this.props.cities} 
         />}/>
        <Route path='/cities/:id/bicycles' render={(routerProps) => 
         <BicyclesList {...routerProps} cities={this.props.cities} 
         />}/>
      </div>
    )
  }
}
const mapStateToProps = state => {
  return {
    cities: state.cities
  }
}
export default connect(mapStateToProps, { fetchCities })(CitiesContainer)

routerProps предоставляется нам, разработчикам, пакетом response-router-dom. Они позволяют нам получать в props содержание URL как params. В нашем случае это позволяет нам получить в реквизитах id соответствующего города, с помощью которого мы можем фильтровать все города - заданные реквизитами - и изолируем тот, который нам нужен.

Использование Redux

Итак, мы наконец добрались до Redux. Что такое Redux, спросите вы. Я помню, как спрашивал себя - и Google - об одном и том же. Я нашел это видео на Youtube, когда набирал Что такое Redux? И нашел его очень понятным. Надеюсь, это сработает и для вас. В документации Redux говорится, что Redux - это контейнер предсказуемого состояния для приложений JavaScript. Документация настаивает на 4 аспектах Redux:

  • Предсказуемость: помогает писать приложения, которые единообразно работают в разных средах и которые легко тестировать.
  • Централизованный: централизация состояния и логики приложения позволяет использовать мощные возможности, такие как сохранение состояния.
  • Возможность отладки: инструменты разработчика Redux позволяют легко отслеживать, когда, где, почему и как изменилось состояние приложения.
  • Гибкость: Redux работает с любым уровнем пользовательского интерфейса.

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

  • Содержите содержимое состояния как props, но не должны ссылаться на хранилище в нашем компоненте, используя mapStateToProps () .
  • Или используйте mapDispatchToProps (), чтобы иметь возможность отправлять действия без ссылки на хранилище в нашем компоненте.

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

Во-первых, что такое промежуточное ПО? Википедия сообщает, что это «компьютерное программное обеспечение, которое предоставляет услуги программным приложениям, помимо тех, которые доступны в операционной системе. Его можно описать как « программный клей». Итак, thunk позволяет нам делать то, что иначе мы не смогли бы сделать. Функция thunk имеет аргумент dispatch, поэтому ее можно использовать внутри функции, в нашем случае внутри создателей действий. Поскольку dispatch может использоваться внутри функции, это позволяет нам dispatch только когда наш запрос fetch равен полный. Это то, что делается в приведенном ниже примере, где мы отправляем только после того, как запрос на выборку для получения всех городов был завершен:

actions/fetchCities.js
export const fetchCities = () => {
  return (dispatch) => {
    fetch('http://localhost:3000/api/v1/cities')
    .then(response => response.json())
    .then(cities => {
      dispatch({
        type: 'FETCH_CITIES',
        payload: cities
      })
    })
  }
}

Сохранение данных с "выборкой"

Используя выборку, мы можем ПОЛУЧАТЬ данные с сервера, а также данные POST на тот же сервер. Пример ПОЛУЧЕНИЯ данных с сервера можно найти в примере CitiesContainer, который я упоминал, говоря о маршрутах. Мы используем выборку в следующем контроллере действий, чтобы ПОЛУЧИТЬ все города:

actions/fetchCities.js
export const fetchCities = () => {
  return (dispatch) => {
    fetch('http://localhost:3000/api/v1/cities')
    .then(response => response.json())
    .then(cities => {
      dispatch({
        type: 'FETCH_CITIES',
        payload: cities
      })
    })
  }
}

В разделе аутентификации создатель действия входа в систему является хорошим примером запроса POST к серверу.

actions/auth.js
export const login = credentials => {
  return (dispatch) => {
    fetch('http://localhost:3000/api/v1/login', {
      credentials: 'include',
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(credentials)
    })
      .then(response => response.json())
      .then(user => {
        if (user.error) {
          alert(user.error)
        } else {
          dispatch(setCurrentUser(user))
          dispatch(resetLoginForm())
        }
      })
      .catch(console.log)
  }
}
export const setCurrentUser = user => {
  return {
    type: 'SET_CURRENT_USER',
    payload: user
  }
}
export const resetLoginForm = () => {
  return {
    type: 'RESET_LOGIN_FORM'
  }
}

Что касается стиля, я изначально планировал использовать Bootstrap для React, поскольку именно о нем я в основном слышал. Поскольку я был новичком в стилях и не нашел того, что искал с точки зрения элементов панели навигации, я поискал, какие другие фреймворки я мог бы использовать в React. Читая эту статью, я проверил компонент Menu в Semantic UI React и нашел настройку, которую искал изначально. Никогда не используя никакую другую структуру стилей, я не могу сравнить, насколько легко использовать Semantic UI по сравнению с другими, но я должен сказать, что к нему довольно легко привыкнуть и использовать.

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