Strukturování aplikace React pro měřítko (část I)

Jedním z důležitých aspektů psaní udržovatelného kódu je správné nastavení kódu. Pokud není organizace kódu provedena správně, může to do značné míry vést k chybám a ovlivnit efektivitu vývoje.

Proč bychom měli uvažovat o uspořádání kódu?

U vývojářů pocházejících z různých zásobníků a jazyků to může být vnímáno velmi odlišně a neexistuje žádný definitivní způsob, ale zkusme definovat, proč to může být dobré

  • Čitelnost
  • Předvídatelnost
  • Konzistence
  • Snazší ladění
  • Snazší přijímání nových vývojářů

V tomto článku bych se rád podělil o jeden způsob organizace projektu reakce, který fungoval pro středně/velké aplikace. Způsob, jakým to budeme strukturovat, je takový, že aplikaci rozdělíme na menší části (funkce) a každá část bude dále rozdělena na

  • data:zabývá se správou stavu aplikace
  • Uživatelské rozhraní:zabývá se znázorněním stavu dat

To nám pomůže snadno udržovat celou aplikaci na atomární úrovni.

V této dvoudílné sérii definujeme strukturu od začátku. Budete také potřebovat základní znalosti:

  • Základy React
  • Háčky reakce
  • Redux pro státní správu
  • Sada nástrojů Redux pro správu Redux
  • Redux-saga pro řešení vedlejších účinků (např. volání API)

Ačkoli tento vzor funguje pro malé projekty, může to být přehnané, ale hej, všechno začíná v malém, že? Struktura definovaná v tomto článku bude tvořit základ aplikace, kterou vytvoříme v dalším článku této série.

Inicializovat projekt

Začněme inicializací projektu reakce (ve strojopisu) pomocí create-react-app spuštěním následujícího příkazu v terminálu

npx create-react-app my-app --template typescript

Po inicializaci skončíme s výše uvedenou strukturou. Veškerá obchodní logika půjde do /src složka.

Nastavení Redux

Pro správu stavu budeme používat redux a redux-saga . Budeme také používat RTK @reduxjs/toolkit (redux toolkit), což je oficiálně doporučený přístup pro psaní logiky Redux. Aby redux-saga mohla naslouchat odeslané redux akci, musíme při vytváření reduktoru vložit ságy, pro to redux-injectors bude použito.

POZNÁMKA:Můžeme také použít další možnosti správy stavu, jako je RxJS, kontextové API atd.

yarn add @reduxjs/toolkit react-redux redux-saga @types/react-redux redux-injectors

Pojďme nakonfigurovat obchod Redux vytvořením /src/reducer.ts , /src/saga.ts a /src/store.ts

// /src/reducer.ts
import { combineReducers } from "@reduxjs/toolkit";

const reducers = {
  // ...reducers 
};

function createRootReducer() {
    const rootReducer = combineReducers({
      ...reducers
    });

    return rootReducer;
};

export { createRootReducer };
// /src/saga.ts
import { all, fork } from "redux-saga/effects";

function* rootSaga() {
    yield all([
        // fork(saga1), fork(saga2)
    ]);
};

export { rootSaga };
// /src/store.ts
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import createSagaMiddleware from 'redux-saga';
import { createInjectorsEnhancer } from 'redux-injectors';
import { createRootReducer } from './reducer';
import { rootSaga } from './saga';

export type ApplicationState = {
  // will hold state for each chunk/feature 
};

function configureAppStore(initialState: ApplicationState) {
  const reduxSagaMonitorOptions = {};
  const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions);

  const { run: runSaga } = sagaMiddleware;

  // sagaMiddleware: Makes redux saga works
  const middlewares = [sagaMiddleware];

  const enhancers = [
    createInjectorsEnhancer({
      createReducer: createRootReducer,
      runSaga
    })
  ];

  const store = configureStore({
    reducer: createRootReducer(),
    middleware: [...getDefaultMiddleware(), ...middlewares],
    preloadedState: initialState,
    devTools: process.env.NODE_ENV !== 'production',
    enhancers
  });

  sagaMiddleware.run(rootSaga);
  return store;
}

export { configureAppStore };

Nyní do aplikace přidáme redux store pomocí komponenta v /src/App.tsx

// /src/App.tsx
import React from 'react';
import logo from './logo.svg';
import './App.css';
import { Provider } from 'react-redux';
import store from './store';

function App() {
  return (
    <Provider store={store}>
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
      </div>
    </Provider>
  );
}

export default App;

Uložte a spusťte aplikaci pomocí npm start zkontrolovat, zda vše běží v pořádku. Chcete-li zkontrolovat, zda byl redux správně integrován, můžete otevřít Redux DevTools v prohlížeči.

Nastavení základny

Než začneme, pojďme definovat základní analogii toho, jak budeme strukturovat náš projekt

  • config: konfigurace související s aplikací, jako je koncový bod API, výčty (konstanty) atd
  • komponenty: vlastní komponenty, které se používají na více místech
  • kontejnery: obsahuje funkce nebo moduly, kde jsou komponenty připojeny k obchodu Redux
  • navigátor: logika související se směrováním je zde
  • služby: moduly, které se propojují s vnějším světem, jako jsou všechna rozhraní API, Analytics atd
  • utils: pomocné metody, jako jsou pomocníci API, pomocníci s datem atd

Pojďme vyčistit src/App.tsx a odstraňte všechny standardní kódy.

// src/App.tsx
import React from 'react';
import { Provider } from 'react-redux';
import { ApplicationState, configureAppStore } from './store';

const initialState: ApplicationState = {
  // ... initial state of each chunk/feature
};

const store = configureAppStore(initialState);

function App() {
  return (
    <Provider store={store}>
      <div>Hello world</div>
    </Provider>
  );
}

export default App;

Nastavení routeru

Pro zpracování logiky směrování aplikace přidáme react-router-dom do projektu a vytvořte komponentu s názvem Navigátor v /src/navigator/

yarn add react-router-dom 
yarn add --dev @types/react-router-dom
// src/navigator/Navigator.tsx
import React, { FC } from "react";
import { Switch, Route, BrowserRouter as Router } from "react-router-dom";

type Props = {};

const Navigator: FC<Props> = () => {
  return (
    <Router>
      <Switch>
        <Route
            path="/"
            render={() => <div>Hello world</div>} />
      </Switch>
    </Router>
  );
};

export { Navigator };

a importovat komponenta v /src/App.tsx

// /src/App.tsx
import React from "react";
import { Provider } from "react-redux";
import { ApplicationState, configureAppStore } from "./store";
import { Navigator } from "./navigator/Navigator";

const initialState: ApplicationState = {
  // ... initial state of each chunk/feature
};

const store = configureAppStore(initialState);

function App() {
  return (
    <Provider store={store}>
      <Navigator />
    </Provider>
  );
}

export default App;

klikněte na uložit a měli byste vidět Ahoj světe text.

Nastavení konfigurace

Tato složka bude obsahovat veškerou konfiguraci související s aplikací. Pro základní nastavení přidáme následující soubory

  • /.env :Obsahuje všechny proměnné prostředí pro aplikaci, jako je koncový bod API. Pokud je složka vytvořena pomocí create-react-app , proměnné mající REACT_APP jako prefix bude automaticky načten konfigurací webpacku, pro více informací se můžete podívat na oficiální příručku. Pokud máte vlastní konfiguraci webového balíčku, můžete tyto proměnné env předat z CLI nebo můžete použít balíčky jako cross-env.
// .env 
// NOTE: This file is added at the root of the project
REACT_APP_PRODUCTION_API_ENDPOINT = "production_url"
REACT_APP_DEVELOPMENT_API_ENDPOINT = "development_url"
  • src/config/app.ts : Obsahuje všechny přístupové klíče a koncové body, které aplikace vyžaduje. Všechny tyto konfigurace budou načteny z výše definovaných proměnných prostředí. Pro tuto chvíli, abychom to zjednodušili, budeme mít dvě prostředí, konkrétně produkční a vývojové.
// src/config/app.ts
type Config = {
  isProd: boolean;
  production: {
    api_endpoint: string;
  };
  development: {
    api_endpoint: string;
  };
};

const config: Config = {
  isProd: process.env.NODE_ENV === "production",
  production: {
    api_endpoint: process.env.REACT_APP_PRODUCTION_API_ENDPOINT || "",
  },
  development: {
    api_endpoint: process.env.REACT_APP_DEVELOPMENT_API_ENDPOINT || "",
  },
};

export default config;
  • src/config/enums.ts :Obsahuje všechny výčty (konstanty) na globální úrovni. Pro tuto chvíli to deklarujme.
// src/config/enums.ts
enum enums { 
    // GLOBAL_ENV = 'GLOBAL_ENV'
}

export default enums;
  • src/config/request.ts :Obsahuje výchozí konfiguraci požadavku, kterou později použijeme při volání API. Zde můžeme nastavit některé konfigurace požadavků API na úrovni aplikace, jako je časový limit, maxContentLength, responseType atd.
// src/config/request.ts
type RequestConfig = {
  url: string,
  method: "get" | "GET" | "delete" | "DELETE" | "head" | "HEAD" | "options" | "OPTIONS" | "post" | "POST" | "put" | "PUT" | "patch" | "PATCH" | undefined,
  baseURL: string,
  transformRequest: any[],
  transformResponse: any[],
  headers: any,
  params: any,
  timeout: number,
  withCredentials: boolean,
  responseType: "json" | "arraybuffer" | "blob" | "document" | "text" | "stream" | undefined,
  maxContentLength: number,
  validateStatus: (status: number) => boolean,
  maxRedirects: number,
}

const requestConfig: RequestConfig = {
  url: '',
  method: 'get', // default
  baseURL: '',
  transformRequest: [
    function transformRequest(data: any) {
      // Do whatever you want to transform the data
      return data;
    }
  ],
  transformResponse: [
    function transformResponse(data: any) {
      // Do whatever you want to transform the data
      return data;
    }
  ],
  headers: {},
  params: {},
  timeout: 330000,
  withCredentials: false, // default
  responseType: 'json', // default
  maxContentLength: 50000,
  validateStatus(status) {
    return status >= 200 && status < 300; // default
  },
  maxRedirects: 5, // default
};

export default requestConfig;

Aktuální struktura složek s přidáním následujících souborů:

  • /src/config/app.ts
  • /src/config/enums.ts
  • /src/config/requests.ts
  • /.env

Nastavení služby API

V této části nastavíme některé pomocné metody pro volání API. Za tímto účelem použijeme Axios a napíšeme obal pro běžné místní úložiště a metody API GET POST PUT PATCH DELETE . Následující obálka s některými drobnými úpravami bude dokonce fungovat s rozhraním API pro načítání nebo XMLHTTPRequest který je snadno dostupný bez jakékoli externí knihovny. Tento kousek lze přeskočit, ale trocha abstrakce může zajistit lepší konzistenci a čistý a čitelný kód.

Nejprve do projektu přidáme balíček Axios.

yarn add axios

Nyní vytvoříme soubor s názvem api-helper.ts v /src/utils a přidejte do souboru následující obsah.

// /src/utils/api-helper.ts
import axios from "axios";
import requestConfig from "../config/request";

export type CustomError = {
  code?: number
  message: string
};

export const getCustomError = (err: any) => {
  let error: CustomError = {
    message:  "An unknown error occured" 
  };

  if (err.response
    && err.response.data
    && err.response.data.error
    && err.response.data.message) {
    error.code = err.response.data.error;
    error.message = err.response.data.message;
  } else if (!err.response && err.message) {
    error.message = err.message;
  }

  return error;
};

export const getFromLocalStorage = async (key: string) => {
  try {
    const serializedState = await localStorage.getItem(key);
    if (serializedState === null) {
      return undefined;
    }
    return JSON.parse(serializedState);
  } catch (err) {
    return undefined;
  }
};

export const saveToLocalStorage = async (key: string, value: any) => {
  try {
    const serializedState = JSON.stringify(value);
    await localStorage.setItem(key, serializedState);
  } catch (err) {
    // Ignoring write error as of now
  }
};

export const clearFromLocalStorage = async (key: string) => {
  try {
    await localStorage.removeItem(key);
    return true;
  } catch (err) {
    return false;
  }
};

async function getRequestConfig(apiConfig?: any) {
  let config = Object.assign({}, requestConfig);
  const session = await getFromLocalStorage("user");
  if (apiConfig) {
    config = Object.assign({}, requestConfig, apiConfig);
  }
  if (session) {
    config.headers["Authorization"] = `${JSON.parse(session).token}`;
  }
  return config;
}

export const get = async (url: string, params?: string, apiConfig?: any) => {
  const config = await getRequestConfig(apiConfig);
  config.params = params;
  const request = axios.get(url, config);
  return request;
};

export const post = async (url: string, data: any, apiConfig?: any) => {
  const config = await getRequestConfig(apiConfig);
  let postData = {};
  if (
    apiConfig &&
    apiConfig.headers &&
    apiConfig.headers["Content-Type"] &&
    apiConfig.headers["Content-Type"] !== "application/json"
  ) {
    postData = data;
    axios.defaults.headers.post["Content-Type"] =
      apiConfig.headers["Content-Type"];
  } else {
    postData = JSON.stringify(data);
    axios.defaults.headers.post["Content-Type"] = "application/json";
  }
  const request = axios.post(url, postData, config);
  return request;
};

export const put = async (url: string, data: any) => {
  const config = await getRequestConfig();
  config.headers["Content-Type"] = "application/json";
  const request = axios.put(url, JSON.stringify(data), config);
  return request;
};

export const patch = async (url: string, data: any) => {
  const config = await getRequestConfig();
  config.headers["Content-Type"] = "application/json";
  const request = axios.patch(url, JSON.stringify(data), config);
  return request;
};

export const deleteResource = async (url: string) => {
  const config = await getRequestConfig();
  const request = axios.delete(url, config);
  return request;
};

getCustomError zpracovat chybu do vlastního typu CustomError a getRequestConfig se stará o přidání autorizace k požadavku API, pokud je uživatel autorizován. Tento pomocný nástroj API lze upravit podle logiky používané back-endem.

Pokračujme nastavením /src/services/Api.ts kde deklarujeme všechna naše volání API. Vše, co vyžaduje interakci s vnějším světem, bude spadat pod /src/services , jako jsou volání API, analýzy atd.

// /src/services/Api.ts
import config from "../config/app";
import * as API from "../utils/api-helper";

const { isProd } = config;

const API_ENDPOINT = isProd
  ? config.production.api_endpoint
  : config.development.api_endpoint;

// example GET API request
/** 
    export const getAPIExample = (params: APIRequestParams) => {
        const { param1, param2 } = params;
        const url = `${API_ENDPOINT}/get_request?param1=${param1}&param2=${param2}`;

        return API.get(url);
    }
*/

Aktuální struktura složek s následující změnou bude vypadat takto:

  • /src/utils/api-helper.ts
  • /src/services/Api.ts

Další kroky

Lidi! to je v podstatě vše pro tuto část, i když jedna hlavní část, kde definujeme veškerou obchodní logiku aplikace, tj. containers &components zbývá, kterému se budeme věnovat v další části vytvořením malého klienta Reddit pro načítání výsledků pro konkrétní téma.

Dávám také odkaz na toto úložiště GitHub, neváhejte jej použít pro vaši referenci a pokud se vám líbí, propagujte toto úložiště, abyste maximalizovali jeho viditelnost.

anishkargaonkar / reagovat-reddit-client

Klient Reddit pro zobrazování nejlepších výsledků pro daná klíčová slova

Děkuji vám za přečtení tohoto článku, doufám, že to bylo zajímavé čtení! Rád bych slyšel vaše myšlenky. Uvidíme se v dalším díle. Dobrý den!