Создание шаблона современного веб-приложения на React

Содержание

В настоящее время набирают популярность одностраничные веб-приложения. Веб-сайты становятся веб-приложениями и напоминают обычные настольные (Desktop) приложения. Наиболее распространёнными инструментами разработки одностраничных веб-приложений являются:

В данной статье я хочу поделиться собственным опытом разработки одностраничного веб-приложения и дать советы начинающим React-разработчикам. Ранее на Портале магистров ДонНТУ тема разработки современных веб-приложений была освещена Жигаревым Михаилом Юрьевичем [1].

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

Одностраничные приложения (SPA)

Одностраничное приложение (Single Page Application) — это веб-сайт, который перерисовывает своё содержимое в ответ на действие пользователя (например, переход по ссылке) без осуществления запроса на сервер для получения новой HTML-страницы [2].

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

React

React — JavaScript-библиотека с открытым исходным кодом для разработки пользовательских интерфейсов [3]. С использованием React создавать интерактивные пользовательские интерфейсы становится значительно проще. Нужно всего лишь проектировать простые представления (view) для каждого состояния в веб-приложении и React будет эффективно обновлять и отрисовывать только те компоненты, данные которых изменились.

Главной особенностью React является высокая производительность, которая достигается благодаря концепции виртуального DOM. Как известно, одной из основных причин низкой производительности веб-приложений является частое применение дорогостоящих по времени операций над деревом DOM [4]. Виртуальный DOM (VDOM) хранит представление пользовательского интерфейса (UI) в памяти, накапливает изменения и синхронизирует их с настоящим DOM оптимальным образом с помощью библиотеки ReactDOM [5].

Redux

Redux — JavaScript-библиотека с открытым исходным кодом для управления состоянием приложения [6]. Redux позволяет значительно упростить обновление состояния в React-приложении, когда, например, нужно обмениваться некоторыми данными между глубоко вложенным дочерним компонентом и родительским компонентом первого уровня. На рисунке 1 показан поток данных веб-приложения, который организовывает Redux.

Однонаправленный поток данных Redux
Рисунок 1 – Однонаправленный поток данных Redux

Компонент (view, представление) отправляет (dispatch) действие (action). На определённое действие реагирует соответствующий редьюсер (reducer), который изменяет глобальное состояние (store). После изменения состояния подписанные на обновления компоненты реагируют на новые данные и перерисовывают себя.

Файловая структура и архитектура шаблона веб-приложения

Шаблон основан на коде, который генерирует CLI-инструмент Create React App v2 [7]. В шаблоне применяются следующие пакеты:

  • Material-UI — UI-фреймворк
  • axios — HTTP-клиент на основе Promise
  • node-sass — поддержка SASS
  • Normalize.css — устраняет различия в стандартных CSS-стилях между браузерами
  • normalizr — выполняет нормализацию вложенных JSON-объектов по заданной схеме
  • React Hot Loader — обеспечивает живую разработку (live-coding), после изменения кода в браузере автоматически обновляются только изменённые компоненты, причём состояние компонентов сохраняется
  • React Router — обеспечивает маршрутизацию
  • Redux — управляет состоянием приложения
  • Logger for Redux — обеспечивает логирование действий и состояния Redux
  • Redux Thunk — позволяет реализовать асинхронную отправку действий Redux
  • Reselect — позволяет применять мемоизацию для селекторов состояния Redux

На рисунке 2 показана файловая структура шаблона.

Файловая структура шаблона
Рисунок 2 – Файловая структура шаблона

В папке actions находятся действия Redux. Пример типичного действия, которое выполняет API-запрос:

							
export const login = (email, password) => {
  const request = () => ({
    type: ActionTypes.LOGIN__REQUEST,
    email,
  })
  const success = (email) => ({
    type: ActionTypes.LOGIN__SUCCESS,
    email,
  })
  const failure = (error) => ({
    type: ActionTypes.LOGIN__FAILURE,
    error,
  })

  return async (dispatch) => {
    dispatch(request())

    let result = null

    try {
      const response = await accountApi.login(email, password)
      result = response.data

      localStorage.setItem('email', email)

      dispatch(success(email))
    } catch (error) {
      dispatch(failure(error))
      dispatch(alertActions.error(error))
    }

    return result
  }
}
							
						

Каждое действие включает в себя отправку трёх простейших действий Redux: REQUEST, SUCCESS, FAILURE. Такое разделение обеспечивает гибкое поведение, при котором можно отдельно обработать момент начала запроса к серверу и успешного ответа или ошибки запроса.

В папке api содержатся функции отправки API-запросов, например:

							
export const login = (email, password) => {
  return axios.post(`/api/auth/login`, {
    email,
    password,
  })
}
							
						

В папке components находятся React-компоненты. Благодаря нативной поддержке языка SASS в Create React App v2, теперь можно подключать *.sass-файлы напрямую из *.jsx-файла компонента: import './MyComponent.sass'.

В папке reducers находятся функции-редьюсеры, которые изменяют данные состояния на основе данных, передаваемых действиями. Пример редьюсера entities.js, который содержит нормализованные объекты:

							
import * as ActionTypes from 'actions/ActionTypes'
import * as models from 'models'
import {normalize} from 'normalizr'
import merge from 'lodash/merge'


const initialState = {
  users: {},
}

export default (state = initialState, action) => {
  switch (action.type) {
    case ActionTypes.LIST_USER__SUCCESS: {
      const {entities} = normalize(action.data, {users: [models.user]})

      return merge({}, state, entities)
    }
    case ActionTypes.LOGOUT__SUCCESS:
      return initialState
    default:
      return state
  }
}
							
						

Благодаря нормализации объектов решается проблема обновления содержимого объекта. Без нормализации пришлось бы искать все вхождения данного объекта по всему состоянию приложения. С нормализацией же в части состояния state.entities хранится карта объектов (map), где ключом является id объекта, а значением является сам объект. В остальных частях состояния хранятся лишь ссылки на объекты, то есть id объекта.

Причём даже с учётом нормализации необходимо позаботиться о правильном обновлении состояния entities. Предположим, что в результате действий пользователя в state.entities.users[id] хранится объект пользователя user, а в его поле user.photos содержится список фото данного пользователя и список данных фото отображается на веб-странице. Если в данный момент времени выполняется запрос GET /user/:id и сервер вернёт набор полей, среди которых не будет поля photos. Объект сущности сохраняется в state.entities.users[id] и, получается, поле photos исчезает.

Для того, чтобы исправить этот недостаток, необходимо не просто перезаписывать объекты в entities, а выполнять глубокое слияние (deep merge). Именно для этого я использую функцию merge из библиотеки lodash.

В папке routers находятся такие же обычные Реакт-компоненты, но они содержат маршруты (Route), которые связывают пути URL с отображаемыми компонентами. Кроме того, файл routers/PrivateRoute.jsx представляет собой аналог обычного Route, но PrivateRoute выполняет закрывает целевой компонент от неаутентифицированного пользователя (гостя) и выполняет переадресацию на страницу входа. Причём делает это таким образом, что после успешной аутентификации выполняется переход на исходную страницу.

В последней папке selectors находятся селекторы данных состояния.

В файле models.js находятся определения схем, которые используются при нормализации и денормализации entities.

В файле store.js осуществляется настройка хранилища состояния Redux, назначаются мидлверы logger и thunk, а также настраивается горячая замена (hot module replacement) для редьюсеров.

В файле themes.js находятся определения тем дизайна пользовательского интерфейса для Material-UI.

В файле types.js содержатся определения типов для PropTypes.

Характерной особенностью данного шаблона также является использование абсолютных путей для импорта компонентов и файлов. Это избавляет от проблемы писать import MyComponent from '../../../../../../../../../../../../../../../../../../MyComponent' при достаточно глубокой вложенности компонентов. Вместо этого достаточно будет написать, например, import MyComponent from 'components/MyComponent'. Базовый путь задаётся в файле .env через переменную окружения NODE_PATH: NODE_PATH=src.

Также в шаблоне применяется кастомизация конфигурации Webpack без выполнения eject (отсоединения от Create React App). Это возможно благодаря использованию пакета react-app-rewired. При его использовании в файле config-overrides.js выполняется изменение текущей конфигурации Webpack. В данном шаблоне такой способ применяется для использования React Hot Loader.

На момент написания статьи последней версией данного шаблона является v1.0.0. Шаблон постоянно обновляется и совершенствуется, поэтому посмотреть исходники или скачать последнюю версию данного шаблона можно на GitHub [8].