Дипломная работа: Разработка сервиса для свободного обмена одеждой

Внимание! Если размещение файла нарушает Ваши авторские права, то обязательно сообщите нам

initialState,

compose(

applyMiddleware(thunk),

(window as any).__REDUX_DEVTOOLS_EXTENSION__ &&

(window as any).__REDUX_DEVTOOLS_EXTENSION__()

)

);

export default store;

Сначала задано начальное состояние приложения в виде пустого объекта «constinitialState = {}». Затем с помощью redux-функции «createStore()» сформированstore, который содержит корневой reducer (комбинация других reducer'ов приложения, см. Шаг 2), начальное состояние и store подключен к thunkдля создания асинхронной логики в работе со store, также здесь подключено расширение «REDUX_DEVTOOLS»для того, чтобы в браузере стала доступна отладка.

Последняя строка кода делает storeдоступным в приложении. Теперь storeможно подключить к любому компоненту как модуль с помощью конструкции «import<…>from<…>».

Чтобы приложение начало использовать store, необходимо подключить его в корневом компоненте. Для этого в компоненте «App.tsx», из библиотеки react-reduxимпортирован контейнер «Provider», который «оборачивает» весь код.Теперь, все дочерние компоненты, которые будут связаны с Redux, будут иметь доступ к глобальному store.

import * as React from 'react';

import { Provider } from 'react-redux';

import store from './redux/Stores/store';

const App: React.FC = () => {

return (

<Provider store={store}>

<Кодпрограммы>

</Provider>

);

};

export default App;

Шаг 2. Описание RootReducer и разделение кода на разные Reducer'ы

TypeScript требует четкой типизации во всем приложении. Код Redux- не исключение. Для того, чтобы описать reducer, сначала был описан интерфейс для этого reducer'а. Интерфейс для «ItemsReducer» будет выглядеть следующим образом:

export interface IItemsReducer {

loading: boolean;

items: IItem[];

error: any;

}

Где:

Поле «loading»- это логическая переменная, показывающая, загрузились ли все запрошенные товары на клиентскую часть. Поле «items» - это массив типаIItem, который описывает то, в каком виде представлены на клиентской части товары. И в случае, если ожидаемого результата не предвиделось, поле «error» типа«any». Как правило, устанавливать тип «any» считается плохой практикой в TypeScript. Однако, ошибки могут приходить в виде объектов, строк и даже массивов. Но здесь нам важно наличие или отсутствие ошибки, поэтому было принято решение не типизировать это поле. По умолчанию поле «error» имеет значение «null».

Теперь необходимо реализовать «itemsReducer» только что созданного типа.В reducer'eзадан «initialState» со значением пустого массива, затем реализована конструкция switch, где «ItemsReducer»в зависимости от пришедшего типа Action будет изменять состояние Store. Например, для типа «ITEM_LOADING», установлено соответствующее ему поле в значение «истина», а остальное состояние остается без изменений (оператор «…» (spread) помогает в этом). В любом ином случае (ветка «default»), «ItemsReducer»будет возвращать неизменное состояние.

import { IItemsReducer } from "./reducerTypes";

import { ItemsActions } from '../Actions/itemsActions';

const initialState: IItemsReducer = {

loading: false,

items: [],

error: null

};

export default (state = initialState, action: AppActionType): IItemsReducer => {

switch (action.type) {

case ItemsActions.GET_ITEMS:

return {

...state,

loading: false,

items: action.payload,

};

case ItemsActions.ITEM_LOADING:

return {

...state,

loading: true

};

case ItemsActions.ITEM_ERROR:

return {

...state,

loading: false,

error: action.payload

};

default:

return state;

}

}

Затем, с помощью функции combineReducers() в файле «RootReducer.ts» создается однаreducer-функция, объединяющая все имеющиеся. Импортированный «itemsReducer»передается в функцию и получает более краткое название для удобства («items»).

import { combineReducers } from 'redux';

import itemsReducer from './itemsReducer';

export default combineReducers({

items: itemsReducer,

<Другие reducer'ы>

});

Шаг 3. Описание Actions

Для корректной работы с TypeScript были описаны типы. Сначала задан общий тип «AppActionType», который описывает типы Аctions для всего приложения. «AppActionType» в свою очередь состоит из типов, которые будут принимать reducer'ы. В данном случае это будет тип «ItemTypes», который состоит из интерфейсов, соответствующих действиям в reducer'е:

interface IGetAllItems {

type: typeof ItemsActions.GET_ITEMS;

payload: IItem[];

}

interface IItemLoading {

type: typeof ItemsActions.ITEM_LOADING;

}

interface IItemError {

type: typeof ItemsActions.ITEM_ERROR;

payload: any;

}

type ItemTypes =

| IGetAllItems

| IItemLoading

| IItemError;

После того, как созданы типы для Actions, можно реализовать логику самих Actions (файл «itemsActions.ts»), где:

Описано в виде типа «enum»все существующие типы Actions для сущности «Товар». Это нужно для того, чтобы не допускать ошибок из-за опечаток и ускорить написание кода: TypeScript во время разработки будет подсказывать и автоматически дописывать типы, если они объявлены в «enum».

Затем реализован каждый Action. Ниже проиллюстрирован примерAction'а на удаление объекта. Внутри поля «payload» типа строка находится идентификатор того товара, который нужно удалить. Подобным образом реализованы все остальные Actions (всего шестьфункций). Важно отметить, чтоActionлишь сообщает что нужно сделать с состоянием, но не как. Само изменение состояния происходит внутри Reducer'a.

export const deleteItem = (payload: string): AppActionType => ({

type: ItemsActions.DEL_ITEM,

payload,

});

Шаг 4. Подключение Redux к функциональному компоненту

Нет необходимости связывать каждый компонент react-приложения с Redux. Так, например, глупые компоненты (от англ. «dummycomponents») нужны лишь для отображения данных, внутри них нет даже reactstate. Поэтому, следует подключать только те компоненты, от которых зависит изменение состояния. Так приложение получается более эффективным и быстрым.

В конце каждого компонента есть специальная конструкция, которая позволяет экспортировать этот компонент, и использовать его, например, в Router.

export default UserPage;

Для того, чтобы подключить компонент «UserPage» к reduxstore, необходимо положить его в специальный контейнер и по сути, экспортировать уже новый контейнер, внутри которого находится «UserPage». Для этого необходимо была импортирована функция «connect()» из библиотеки react-redux.

export default connect(mapStateToProps, mapDispatchToProps)(UserPage);

Внутри функции также находятся две функции: «mapStateToProps»и «mapDispatchToProps». Эти функции необходимы, чтобы связать компонент со store. Так, например, «mapStateToProps» получает часть состояния redux-state, а именно, объект «items» и присваивает его свойствам (props)react-компонента «UserPage». Для сохранения логики и читабельности кода этот propsкомпонента также назван «items».

Функция«mapDispatchToProps» позволяет отправлять Actions (dispatchactions) из компонента. В примере кода ниже видно, что в «mapDispatchToProps» выполняется привязка функции «getAllMine» к данном компоненту.

const mapStateToProps = (state: AppState) => ({

items: state.items.items,

});

const mapDispatchToProps = (

dispatch: ThunkDispatch<any, any, AppActionType>

) => ({

getAllMine: bindActionCreators(getAllMine, dispatch),

});

Шаг 5. Вызов функции и dispatch

В данном случае логика компонента такая: UserPageпоказывает сведения о пользователе и его товарах, здесь же пользователь может добавить, удалить, отредактировать товар и увидеть все свои товары. Нет необходимости каждый раз подгружать все товары пользователя, их можно хранить внутри состояния приложения, а отображать лишь при переходе на страницу. Поэтому функция «getAllMine()», которая получает все товары выбранного пользователя, будет срабатывать только тогда, когда меняется пользователь. (например, при выходе из приложения, и входе нового пользователя). Для этого используется reacthook«useEffect», который срабатывает после того, как изменилось состояние объекта «user». Благодаря функции «mapDispatchToProps», функция «getAllMine» стала доступна из свойств (props)компонента. В нее мы передаем идентификатор пользователя, товары которого мы должны получить.

useEffect(() => {

if (props.user._id) props.getAllMine(props.user._id);

}, [props.user]);

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

export const getAllMine = (userId: string) => (

dispatch: Dispatch<AppActionType>

) => {

dispatch(sendLoading());

axios

.get(`/api/items/${userId}`)

.then((res) => dispatch(sendItems(res.data)))

.catch((error) => dispatch(sendErrors(error.response.data)));

};

Отправка данных на сервер

Отправка данных на сервер реализуется с помощью GET, POST, PUT, DELETE запросов. Чтобы упростить обработку запросов на сервере был использован фреймворк Express.js, который представляет собой надстройку над node.js, упрощающую реализацию серверной части.

Для обработки запросов в Express определено ряд встроенных функций, и одной из таких является функция.get(). Она обрабатывает GET-запросы протокола HTTP и позволяет связать маршруты с определенными обработчиками. Для этого первым параметром передается маршрут, а вторым - обработчик, который будет вызываться, если запрос к серверу соответствует данному маршруту:

const express = require('express');

const router = express.Router();

const categories = require('../../controllers/categories.controller');

// api/categories

router.get('/', categories.getAllCategories);

module.exports = router;

В приведенном выше фрагменте кода, создается объект router из библиотеки Express, а затем используется функция, куда передан метод из подключенного контроллера («categories.controller»).

Для отправки запросов с клиентской части приложения используется библиотека Axios.

Axios -- это Java Script-библиотека для выполнения либо HTTP-запросов в Node.js, либо XMLHttpRequests в браузере. Библиотека поддерживает промисы, а значит и синтаксис ESMASCRIPT 2016. Также Axiosпозволяет автоматически преобразовывать JSON-данные.

Ниже пример использования библиотеки Axios. Для создания GET-запроса на получение категорий товаров, вызывается метод .get(), куда передается маршрут. Далее подписывается потребитель (метод .then()) первым аргументом является функция, выполняющаяся при успешном выполнении. Потребитель «.catch()»будет отлавливать любые пришедшие в запросе ошибки.

//получаем список категорий

export const getCategories = () => (dispatch: Dispatch<AppActionType>) => {

dispatch(sendLoading());

axios

.get('/api/categories')

.then((res) => dispatch(sendCategories(res.data)))

.catch((err) => dispatch(sendErrors(err.response.data)));

};

Визуальное решение

На основе разработанных макетов экранных форм в работе были реализованы экраны интерфейса. Однако разработка визуальных компонентов с нуля достаточно трудозатратна, поэтому было принято решение использовать готовые UI-библиотеки. Всего рассмотрено было четыре популярные библиотеки: ReactBootstrap, MaterialUI, MaterializeCSS, Grommet.

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

Для корректной работы с библиотекой Grommet[17]на языке typescript нет необходимости устанавливать дополнительные пакеты. В разрабатываемом сервисе использовалась библиотека компонентов Grommetи библиотека иконок для визуализации Grommet-icons.Grommet-icons позволяет добавлять SVG-изображения в виде компонента.

На рисунке 14 представлен пример реализации функционального компонента с радио-кнопками.

Рис. 14. Реализация визуального компонента из библиотеки grommet

На рисунке 15 представлен реализованный экран «Мои товары». Панель слева всегда остается видимой пользователю. В будущих версиях будет реализовано чтение местоположения пользователя, вкладка «История обмена». Справа расположена лента товаров пользователя. При нажатии на плитку «Добавить», открывается экран редактирования товара. При этом URLв адресной строке меняется: «/user/item/new». В случае же, если пользователь нажимает на плитку с товаром, то открывается страница редактирования или удаления товара, а URLв строке выглядит следующим образом: «/user/item/<идентификатор товара>».

Рис. 15. Экран приложения для свободного обмена одеждой. Экран «Мои товары»

Добавление и редактирование, удаление товаров происходит на одном и том же экране (см. Рисунок 16). Если открывается уже созданный товар, то текущий экран предзаполняется данными товара, а внизу появляются кнопки «Изменить товар» и «Удалить товар» соответственно.

Рис. 16. Экран приложения для свободного обмена одеждой. Экран «Добавить новый товар»

Лента товаров представляет собой «пачку» карточек, на которых изображены товары и дана краткая информация: категория, название, описание товара и теги, которые указал автор публикации (см. Рисунок 17). Перелистывать карточки можно, смахивая их влево или вправо либо воспользоваться кнопками, находящимися внизу. После того, как карточка свайпнута влево или вправо, внизу появляется уведомление о том, что действие завершилось. Когда вся лента закончится, появится карточка-заглушка, чтобы не допускать пустого экрана.

Рис. 17. Экран приложения для свободного обмена одеждой. Экран «Swipe»

Чтобы попасть на экран обмена (см. Рисунок 18), необходимо нажать кнопку «Узнать совпадения». После срабатывания запроса на совпадения, отразится информация. Если совпадений не было найдено, приложение предложит перейти на главную страницу или добавить еще товары - необходимо повысить вероятность совпадения.

На экране «Обмен» показан пользователь, которому понравились предметы гардероба, опубликованные текущим пользователем. В свою очередь, показаны предметы одежды этого пользователя, которые понравились. При нажатии на кнопку «Меняться» появится контактная информация у пользователя. Теперь оба пользователя могут связаться друг с другом. Можно выбрать предложенные предметы и сообщить пользователю об условиях обмена и передачи товара.