Русский
Русский
English
Статистика
Реклама

Структура React REST API приложения TypeScript Styled-Components

Доброго %время_суток, хабровчане!

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

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

Предисловие

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

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

НО, были и хорошие, одной из которых я вдохновился. В ней говорилось о том, что всю логику хранить в папке "Core". Ссылку на эту статью, на момент публикации, к сожалению, найти не могу, но как найду - обязательно прикреплю, либо отредактирую пост (если такое будет возможно, то на месте этого абзаца будет ссылка на статью), либо оставлю ссылку в виде комментария.

(!) Прошу вас, на время прочтения этой статьи, держать в голове мысль, что то, что тут предлагаю - это всего-лишь одна из идей того, как можно организовать структуру своего приложения. Не стоит воспринимать мои слова, как догму (это относится к тем, кто только начал свое знакомство с React), ведь я и сам новичок.

Буду очень рад, если вы оставите предложения по улучшению данной структуры, как и вашей конструктивной критике.

Components

Начну, пожалуй, с компонентов.

Компоненты, в данной структуре, разделяются на:

  • Умные (Smart)

  • Обычные (Ordinary)

  • Простые (Simple)

  • UI (UI, как ни странно)

  • Контейнеры (Containers)

  • Страницы (Pages)

Первые четыре группы (Smart, Ordinary, Simple и UI) хранятся в папке Components.

Поговорим немного о них:

  • UI компоненты - это те компоненты, которые заменяют нативные (стандартные) компоненты по типу: button, input, textarea, select и так далее.

    • Данные компоненты не могут использовать свое локальное хранилище и обращаться к глобальному.

  • Simple компоненты - это те компоненты, которые являются простыми, иначе говоря компоненты, в которых нет какой-либо логики, которые просто что-то рендерят.

    • Не могут использовать локальное хранилище и обращаться к глобальному.

    • Не могут использовать хуки, кроме тех, что изначально поставляются с React (за исключением useState).

    • Могут использовать в своей реализации UI компоненты.

  • Ordinary компоненты - это те компоненты, которые могут иметь какую-то логику, для отображения чего-либо.

    • Не могу использовать локальное хранилище, как и обращаться к глобальному.

    • Не могут использовать хуки, кроме тех, что изначально поставляются с React (за исключением useState).

    • Могут использовать в своей реализации Simple и UI компоненты.

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

    • Могут использовать локальное хранилище, как и обращаться к глобальному (не изменяя его)

    • Могут использовать все доступные хуки, кроме тех, что взаимодействуют с сетью

    • Могут использовать в своей реализации Ordinary, Simple и UI компоненты.

Структура папки Componets:

. src/     components/        ordinary        simple        smart        ui     ...

Оставшиеся две группы (Containers и Pages) имеют отдельные папки в корне приложения (папка src).

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

  • Pages - это те компоненты, которые формируются благодаря контейнерам и компонентам из папки Components, если в этом есть необходимость. Могут, как и контейнеры, взаимодействовать с сервисами.

Структура корневой папки:

. src/     components/        ordinary        simple        smart        ui     containers     pages     ...

Сами компоненты должны иметь отдельные папки, есть 2 (это число не является константой) файла:

  • index.tsx - файл, в котором находится сам компонент

  • styled.ts - файл, в котором находятся стилизованные компоненты (его спокойно можно заменить на styles.sсss, либо же styles.css, в зависимости от того, чем вы пользуетесь для стилизации своих компонентов)

Пример компонента Align. Хотелось бы сказать, что этот компонент попадает под группу "Simple", так как он является глупым (не имеет нужды в локальном хранилище) и не заменяет никакой нативный, браузерный, UI компонент.

// index.tsximport React, { memo } from "react";import * as S from "./styled"; // Импортируем стилизованные компонентыconst Align = memo(({ children, axis, isAdaptable = false }: Readonly<Props>) => {return (<S.Align $axis={axis} $isAdaptable={isAdaptable}>{children}</S.Align>);});export { Align };export interface Props {axis: S.Axis;children?: React.ReactNode;isAdaptable?: boolean;}
// styled.tsimport styled, { css } from "styled-components";const notAdaptableMixin = css`width: 100%;height: 100%;max-height: 100%;max-width: 100%;`;const adaptableMixin = css<AlignProps>`width: ${(props) => !props.$axis.includes("x") && "100%"};height: ${(props) => !props.$axis.includes("y") && "100%"};min-width: ${(props) => props.$axis.includes("x") && "100%"};min-height: ${(props) => props.$axis.includes("y") && "100%"};`;export const Align = styled.div<AlignProps>`display: flex;flex-grow: 1;justify-content: ${(props) => (props.$axis.includes("x") ? "center" : "start")};align-items: ${(props) => (props.$axis.includes("y") ? "center" : "start")};${(props) => (props.$isAdaptable ? adaptableMixin : notAdaptableMixin)};`;export interface AlignProps {$axis: Axis;$isAdaptable?: boolean;}export type Axis = ("y" | "x")[] | "x" | "y";

Теперь, поговорим о самом сладком...

Core

Данная папка является "ядром" вашего приложения. В ней хранится все, для взаимодействия с сервером, глобальное хранилище, тема вашего приложения и т.д.

Эта папка содержит:

  • Config - в данной папке хранятся конфигурационные файлы приложения (например в ней можно хранить данные, необходимы для взаимодействия с бэкендом)

  • Constants - в данной папке находятся все константы, что используются в приложении (например сообщения об ошибках и предупреждениях)

  • Hooks - в данной папке хранятся все хуки кастомные хуки (хуки, что были сделаны вами).

  • Models - в данной папке хранятся модели, что приходят с бэкенда.

  • Schemes - в данной папке хранятся схемы форм, таблиц и т.д.

  • Services - в данной папке хранятся сами сервисы, благодаря которым и происходит общение с бэкендом.

  • Store - в данной папке хранятся схемы глобального хранилища (если Вы используете MobX), если же вы отдаете предпочтение Redux, то в данной папке могут хранится экшены, редьюсеры и т.д.

  • Theme (для Styled-Components) - в данной папке хранятся темы приложения.

  • Types - в данной папке хранятся вспомогательные типы, а также декларации модулей.

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

  • api.ts - в данном файле находится экземпляр HTTP клиента (например axios), который используют сервисы и который какой-то мутирует данные запросы (для передачи каких-либо заголовков, например).

Примеры содержимого папок
// config/api.config.tsexport const serverURI = "http://localhost:8080";export const routesPrefix = '/api/v1';// config/routes.config.tsimport { routesPrefix } from "./api.config";export const productBrowserRoutes = {getOne: (to: string = ":code") => `/product/${to}`,search: (param: string = ":search") => `/search/${param}`,};export const productAPIRoutes = {getOne: (code: string) => `${routesPrefix}/product/code/${code}`,search: () => `${routesPrefix}/product/search`,};

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

// constants/message.constants.tsexport const UNKNOWN_ERROR = "Неизвестная ошибка";
// hooks/useAPI.ts// Хук для взаимодействия с сервисами/* eslint-disable react-hooks/exhaustive-deps */import { useCallback, useEffect } from "react";import { useLocalObservable } from "mobx-react-lite";import type { API, Schema, Take } from "@core/types";function useAPI<F extends API.Service.Function<API.Response<any>>,R extends Take.FromServiceFunction.Response<F>,P extends Parameters<F>>(service: F, { isPendingAfterMount = false, isIgnoreHTTPErrors = false }: Options = {}) {const localStore = useLocalObservable<Store>(() => ({isPending: {value: isPendingAfterMount,set: function (value) {this.value = value;},},}));const call = useCallback(async (...params: P): Promise<R["result"]> => {localStore.isPending.set(true);try {const { data } = await service(...params);const { result } = data;localStore.isPending.set(false);return result;} catch (error) {if (isIgnoreHTTPErrors === false) {console.error(error);}localStore.isPending.set(false);throw error;}},[service, isIgnoreHTTPErrors]);const isPending = useCallback(() => {return localStore.isPending.value;}, []);useEffect(() => {localStore.isPending.set(isPendingAfterMount);}, [isPendingAfterMount]);return {call,isPending,};}export { useAPI };export interface Options {isPendingAfterMount?: boolean;isIgnoreHTTPErrors?: boolean;}type Store = Schema.Store<{ isPending: boolean }>;
// models/product.model.ts// Описание модели товараexport interface ProductModel {id: number;name: string;code: string;info: {description: string;note: string;};config: {isAllowedForPurchaseIfInStockZero: boolean;isInStock: boolean;};seo: {title: string;keywords: string;description: string;};}
// services/product.service.ts// Сервисы для взаимодействия с товарамиimport { api } from "../api";import { routesConfig } from "../config";import type { ProductModel } from "../models";import type { API } from "../types";export function getOne(code: string) {return api.get<API.Service.Response.GetOne<ProductModel>>(routesConfig.productAPIRoutes.getOne(code));}
// theme/index.ts// Тема приложенияimport { DefaultTheme } from "styled-components";export const theme: DefaultTheme = {colors: {primary: "#2648f1",intense: "#151e27",green: "#53d769",grey: "#626b73",red: "#f73d34",orange: "#fdb549",yellow: "#ffe243",white: "white",},};
// types/index.tsx// Вспомогательные типыimport type { AxiosResponse } from "axios";export namespace API {export namespace Service {export namespace Response {export type Upsert<T> = Response<T | null>;export type GetOne<T> = Response<T | null>;export type GetMany<T> = Response<{rows: T[];totalRowCount: number;totalPageCount: number;}>;}export type Function<T extends API.Response<any>, U extends any[] = any[]> = (...params: U) => Promise<AxiosResponse<T>>;}export type Response<T> = {status: number;result: T;};}
// utils/throttle.tsfunction throttle<P extends any[]>(func: (...params: P) => any, limit: number) {let inThrottle: boolean;return function (...params: P): any {if (!inThrottle) {inThrottle = true;func(...params);setTimeout(() => (inThrottle = false), limit);}};}export { throttle };
// store/index.tsximport { createContext } from "react";import { useLocalObservable } from "mobx-react-lite";import { app, App } from "./segments/app";import { layout, Layout } from "./segments/layout";import { counters, Counters } from "./segments/counters";export const combinedStore = { layout, app, counters };export const storeContext = createContext<StoreContext>(combinedStore);export function StoreProvider({ children }: { children: React.ReactNode }) {const store = useLocalObservable(() => combinedStore);return <storeContext.Provider value={store}>{children}</storeContext.Provider>;}export type StoreContext = {app: App;layout: Layout;counters: Counters;};
// api.ts// Экземпляр AXIOS для взаимодействия с серверомimport axios from "axios";import { apiConfig } from "./config";const api = axios.create({baseURL: apiConfig.serverURI,});api.interceptors.request.use((req) => {return {...req,baseURL: apiConfig.serverURI,};});export { api };

Ух ты! Как же много получилось.

И напоследок...

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

  • Assets - в данной папке хранятся все статичные файлы, такие как: иконки, изображения, шрифты и т.д. (их, конечно же, также стоит группировать и разделять на папки)

  • Routes - в данной папке (либо же файле, кому как больше нравится) хранятся все роуты приложения (пример будет ниже).

  • Styles - в данной папке хранятся все глобальные стили, которые применяются ко всем элементам и документу, в том числе.

// routes/index.tsximport { Switch, Route } from "react-router-dom";// Экспортируем страницыimport { Product } from "../pages/Product";...import { NotFound } from "../pages/NotFound";import { routesConfig } from "../core/config";const Routes = () => {return (<Switch><Route exact path={routesConfig.productBrowserRoutes.getOne()}><Product /></Route>{/* Объявляем как-то роуты */}<Route><NotFound /></Route></Switch>);};export { Routes };

Остается еще 2 файла:

  • app.tsx - компонент приложения

Примерно так он может выглядеть:

// app.tsximport React, { useEffect } from "react";// Импортирует роутыimport { Routes } from "./routes";const App = () => {return (<Routes />);};export { App };
  • index.tsx - входной файл вашего приложения

Он же может выглядеть примерно так:

import React from "react";import ReactDOM from "react-dom";import { BrowserRouter } from "react-router-dom";import { ThemeProvider } from "styled-components";// импортируем нашеimport { App } from "./app";// импортируем глобальные стилиimport { BodyStyles } from "./styles";import { StoreProvider } from "../core/store";// импортируем темуimport { theme } from "../core/theme";import reportWebVitals from "./reportWebVitals";const app = document.getElementById("app");ReactDOM.render(<React.StrictMode><ThemeProvider theme={theme}><BodyStyles /><BrowserRouter><StoreProvider><App /></StoreProvider></BrowserRouter></ThemeProvider></React.StrictMode>,app);reportWebVitals();

И на этом, я думаю, стоит закончить.

Итоговая структура выглядит вот так:

. src/     assets/        fonts        icons     components/        ordinary        simple        smart        ui     containers     core/        config        constants        hooks        models        schemes        services        store        theme        types        utils        api.ts     pages     routes     styles     app.tsx     index.tsx

Заключение

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

Ссылка на репозиторий (за такой скудный ридми, мне еще не по силам разговорный английский).

Всем удачи и огромное спасибо за внимание.

Источник: habr.com
К списку статей
Опубликовано: 09.03.2021 16:05:49
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Javascript

Reactjs

Typescript

React

Boilerplate

Template

Rest

Структура

Styled-components

Axios

React.js

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru