React 项目结构小结

news/2024/7/15 18:27:35 标签: react.js, 前端, 前端框架

React 项目结构小结

简单的记录一下目前 React 项目用的依赖和实现

摸索了大半年了大概构建一套用起来还算轻松的体系……?基本上应该是说可以应对大部分的项目了

使用的依赖

目前项目还在 refactoring 的阶段,所以乱得很,这里是新建一个空的项目作为案例,package.json 中新增添的依赖如下:

{
  "dependencies": {
    "@reduxjs/toolkit": "^1.9.7",
    "@ui-framework/keycloak-auth": "^4.0.0",
    "axios": "^1.6.0",
    "dayjs": "^1.11.10",
    "lodash": "^4.17.21",
    "react-redux": "^8.1.3",
    "react-router-dom": "^6.18.0",
    "redux-logger": "^3.0.6",
    "styled-components": "^6.1.0",
    "uuid": "^9.0.1"
  }
}

版本不一定是最新的,这个取决于我们的 nexux 库更新的有多勤快

主要归类如下:

  • 状态

    • @reduxjs/toolkit

      这是主要的管理部分,RTK(Redux Toolkit) 是迭代后的版本,

    • react-redux

      RTK 的依赖

    • redux-logger(可选)

      用来查看 state 变化的插件

  • API 相关

    • axios
  • 路由

    其实可以的话是想找一下依赖,看看有没有把路由归并到 redux 管理的实现,之前看到的一些实现要么是 v5 的,v6 不支持,要么就是 v6 可以支持,但是比较 buggy,现在暂时没什么时间做这个,晚点再折腾

    • react-router-dom
  • 验证

    • keycloak-auth

      这是一个登录的依赖,token、profile 等会通过 keycloak-auth 返回

  • UI

    公司内部其实有实现自己的 UI 库,所以我就放两个底层用的依赖。其他关于 radio 之类的,都是项目内部实现的

    • styled-components

      CSS-in-JS

    • react-table

      react-table 本身是一个 headless 的库,需要具体实现 UI

    • react-select

      这个比较方便的一点在于可以使用 async dropdown,即通过点击 load more 或者 scroll 事件可以触发 API 调用

      搭配好 pagination 的 query 可以比较好的提升用户体验

  • util 相关

    • dayjs

      取代 momentjs

    • uuid

      主要用在 API 调用方面,有的情况后端返回的 id 是 UUID

    • lodash

底层用的还是 create-react-app,主要是因为脚手架一来确实方便,升级也可以直接通过升级 react-script 进行集成管理。二来,我们的项目需求并没有复杂到需要将 webpack 单独拆出来,做对应的优化等

基础结构

目前想要实现的结构如下:

在这里插入图片描述

  • components

    这里放置了一些封装好的 UI,也就是我们根据自己内部的业务需求实现的一些 wrapper,主要的类型有:button, modal, layout, table 等

  • constants

    这里是一些共用的逻辑,这个里面的分类是按照 model 进行的分类

    我们的项目是比较强类型的,而且是 2b 业务,所以主要就是表单+表格的功能,而每个表单/表格有需要有独立的 structure 传到 UI 库中形成对应的结构,因此每个 model 对应的 structure 可以保存在 constant 中

    另外一些可以保存的常量有 model 的类型,这一块目前放到 types 里,不过建于 types 对 TS 来说是生成 .d.ts 的文件的地方,所以这个迟早是要修改的

  • pages

    每一个渲染的页面

  • store

    redux store 的相关管理

  • types

    目前用来放 model 的 type,不过按照上面说的,想要移到 const

  • utils

    一些相关的 helpers,包括环境、date、entity(model) 之类的

一些实现

其实主要还是 redux 相关的部分的修改,其他部分要么就是之前已经写过笔记了,要么就是跟具体业务相关的,这里不太好记

react router dom

v5 的使用:[React 基础系列] React Router 的基本应用 和 v6 的升级:React Router DOM 升级到 v6 后的一些报错信息

就目前来说我们还是待在 v5 没有动,不过之后我需要对已经实现的 Router 进行一个重构,到时候会实现一下升级 v6,不过按照计划来说,这也是明年二/三月份之后的事情……

现在到明年二/三月份主要还是需要将所有的 page 转成 redux-based

redux

Redux 的使用其实之前也有写过,大概如下:

  • Redux Toolkit 调用 API 的四种方式
  • []async thunk 解决 API 调用的依赖问题(https://goldenaarcher.blog.csdn.net/article/details/129002505)
  • Redux 错误处理

这里不会太进入细节,简单的说一下每个 slice 里面的简单实现,以及 pages 中的 component 怎么调用的,基本结构大致如下:

在这里插入图片描述

index.ts

这里代码其实没什么好变的,基本上是一个万能模板了:

import {
  configureStore,
  EnhancedStore,
  ThunkDispatch,
  AnyAction,
  Store,
} from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import logger from 'redux-logger';
import reducers from './slices';

// reference: https://stackoverflow.com/questions/70143816/argument-of-type-asyncthunkactionany-void-is-not-assignable-to-paramete

// 1. Get the root state's type from reducers
export type RootState = ReturnType<typeof store.getState>;

// 2. Create a type for thunk dispatch
export type AppThunkDispatch = ThunkDispatch<RootState, any, AnyAction>;

// 3. Create a type for store using RootState and Thunk enabled dispatch
export type AppStore = Omit<Store<RootState, AnyAction>, 'dispatch'> & {
  dispatch: AppThunkDispatch;
};

export const store: EnhancedStore = configureStore({
  reducer: reducers,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false,
    }).concat(logger),
});

// you can also create some redux hooks using the above explicit types
export const useAppDispatch = () => useDispatch<AppThunkDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

useAppDispatchuseAppSelector 这个,因为类型检查的关系,使用 TS 的前提下几乎是必须的,我之前也有一篇笔记讨论过这个。

helper func

我们实现了一个 createAsyncThunk 的 wrapper,主要用来处理 AppThunkDispatch 需要重复提供类型问题,代码如下:

import { createAsyncThunk, createAction } from '@reduxjs/toolkit';
import { AppThunkDispatch, RootState } from '../';

export const createAppAsyncThunk = createAsyncThunk.withTypes<{
  state: RootState;
  dispatch: AppThunkDispatch;
  rejectValue: string;
  extra: { s: string; n: number };
}>();
slice/index.ts

slice/index.ts 主要就是用来集成一堆的 reducer 让 store 去使用,同时导出所有的 actions,这样让其他地方的 import 干净一些,大致代码如下:

import { example } from './slices/exampleSlice';

const reducers = {
  example,
};

export default reducers;

export * from './slices/exampleSlice';
slice

这里的 slice 主要是 API 的操作,代码大致如下:

export type IExampleSlice = {
  loading: boolean;
  data: any[];
  error: null | SerializedError;
};

const initialState: IExampleSlice = {
  loading: false,
  data: [],
  error: null,
};

const uri = '';

const exampleSlice = createSlice({
  name: 'modal',
  initialState,
  reducers: {
    clearState() {
      return initialState;
    },
  },
  extraReducers(builder) {
    builder.addCase(fetchExample.pending, () => {
      return {
        ...initialState,
        isLoading: true,
      };
    });
    builder.addCase(fetchExample.fulfilled, (state, { payload }) => {
      // process payload
    });
    builder.addCase(fetchExample.rejected, (state, action) => {
      console.error(action.error);
      state.error = action.error;
    });
  },
});

export const fetchExample = createAppAsyncThunk<
  any,
  { payload: Partial<any> },
  { state: RootState }
>(`${uri}/post`, async ({ payload }, { dispatch, getState }) => {
  const res = await wrappedFetchMethod();

  return res;
});

export const { clearState } = exampleSlice.actions;
export const example = exampleSlice.reducer;

其中 IExampleSlice 可以使用 generics 单独抽出来,如:

export type IGenericsSlice<T> = {
  loading: boolean;
  data: T[];
  error: null | SerializedError;
};

type ExampleModel = {
  id: string;
  name: string;
};

export type IExampleSlice = IGenericsSlice<ExampleModel>;

我们项目就是将 API 单独抽出来进行了封装,如果 slice 内有其他需要合并的属性,在定义 ISthSlice 的时候会使用 & 进行合并

其他 slice

其他的 slice 比较灵活,可以根据需求单独神明 type 并且进行返回。

每个 slice 有对应定义的 type 还是挺重要的,尤其是之后 Component 中调用的时候,可以比较方便的提供 intellisense

RTKQ

目前的项目因为数据量的关系(没有做 pagination,并且用户要求不做 pagination),所以决定不使用 RTKQ 在完成 CUD 操作后直接重新拉去数据

不过 RTK 可以 cache query,并且在对应的 query 中对应的功能

也就是一旦出发 CUD 操作,自动调用 retrieve 操作,不需要手动在 await 操作中实现。这一点对于原子性要求更高的 2c 项目中很有用,并且这个功能也取代了一些 redux-saga 可以实现的功能,这也是为什么我没有考虑引入 saga 的原因

组件调用 slice

大致如下:

import React, { useEffect } from 'react';

import { useAppDispatch, useAppSelector, RootState } from '../store';
// 所有导出都通过 slices/index,因此相对而言 import 可以稍微干净一些
import { IExampleSlice, fetchExample } from '../store/slices';

const Example = () => {
  const dispatch = useAppDispatch();

  const { data, error, loading } = useAppSelector<IExampleSlice>(
    (state: RootState) => state.example
  );

  const apiCall = async () => {
    console.log(data, error, loading);

    const res = await dispatch(
      fetchExample({
        payload: {
          id: '',
        },
      })
    );

    if (res.meta.requestStatus === 'fulfilled') {
      // do sth
    }
  };

  useEffect(() => {
    apiCall();
  }, []);

  return <div>Example</div>;
};

export default Example;
  1. 提供一些对于 payload 的检查还是有好处的,比如:

    在这里插入图片描述

    CRUD 中的操作主要都是对于对应的模型操作,因此将上面的 payload 定义改成 Example 的模型,TS 也可以自动对其进行静态检查:

    在这里插入图片描述

    在这里插入图片描述

  2. useAppSelector 中的类型

    主要也是为了 intellisense,如过不提供 <IExampleSlice>,返回值如下:

    在这里插入图片描述

    又或者是一些比较小的 typo,如果不提供类型,TS 是抓不出来的:

    在这里插入图片描述

    提供后:

    在这里插入图片描述

  3. 导出方式

    这里还是推荐使用 useAppSelector(state => state.someState) 的方式,这个是为了 performance,直接返回整个 state 可能会引起不必须的渲染:

    在这里插入图片描述

  4. 错误处理

    这个其实之前的笔记有提,这里再说一下好了

    如果是异步操作,通过 asnc/await 可以直接获取 request 的结果,这样就可以根据接过去具体更新 UI 了:

    如:

    在这里插入图片描述

    • 操作成功关闭 modal

    • 操作失败继续维持 modal 的开启状态

      同时根据返回的 error,在 UI 生成对应的错误信息

总结

其实大体上主要还是 redux 的配置比较多一些,其他部分都挺灵活的,而且和业务绑定的比较多,我也不方便说,说了也不一定有参考意义

项目的结构是一个比较风格化的东西,我用的比较喜欢的风格 有点变态的说已经把这个项目变成了我喜欢的样子……? 并不代表这一定是一门项目会用的风格,这是 react 的优点也是缺点

完成 redux 部分的 refactor 之后,下一步考虑的就是使用 lerna 对项目升级,做成一个 micro-frontend 的项目。这也是基于项目本身的特性决定的,我们的项目是一个多地区使用的网页应用,并且不同地区对于想要渲染的数据、显示的页面会有不同的需求。目前的实现就是一旦打包了,所有的东西全都打包 ship 出去,不过操作起来还是不太方便

我的构想是:

  • 将所有的 constant/util/components/slices 打包成一个共享的 module
  • 每个地区根据不同的需求
    • 调用不同的 reducer,生成只包含所需数据的 store
    • 实现对应的页面

最后形成一个像 venn diagram 的结构:

在这里插入图片描述

而不是将所有的代码打包到一起 ship 出去


http://www.niftyadmin.cn/n/5149194.html

相关文章

如何使用Selenium处理Cookie,今天彻底学会了

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

groovy-比较运算符

一、if的表达式为false的场景 0、0.0、0L、[]、[:]、null def aaa 0;if(aaa){println("a");}else {println("a2 ...");}//结果为a 二、""比较运算符不会出现类型转换的异常&#xff0c;因为groovy是弱类型语言 ""比较运算符相当于…

[AUTOSAR][诊断管理][ECU][$85] 设置DTC功能

文章目录 一、简介功能描述应用场景服务请求请求格式请求实例关闭DTC监控(OFF)开启DTC监控(ON)服务响应正响应格式正响应实例负响应NRC支持三、 示例代码85_ctl_dtc_set.c一、简介 功能描述 根据ISO14119-1标准中所述,诊断服务85服务主要用于开启或者停止DTC状态位的更新功能…

uniapp原生插件之安卓华为统一扫码HMS Scan Kit

插件介绍 华为统一扫码服务&#xff08;Scan Kit&#xff09;提供便捷的条形码和二维码扫描、解析、生成能力 插件地址 安卓华为统一扫码HMS Scan Kit - DCloud 插件市场 超级福利 uniapp 插件购买超级福利 详细使用文档 详细使用文档 插件申请权限 android.permi…

CodeWhisperer 的安装及体验

文章作者&#xff1a;Pony CodeWhisperer 是亚马逊出品的一款基于机器学习的通用代码生成器&#xff0c;可实时提供代码建议。类似 Cursor 和 Github Copilot 编码工具。 官网&#xff1a;https://aws.amazon.com/cn/codewhisperer/?trkcndc-detail 在编写代码时&#xff0c…

【VR开发】【Unity】【VRTK】3-VR项目设置

任何VR避不开的步骤 如何设置VR项目,无论是PC VR还是安卓VR,我在不同的系列教程中都说过了,不过作为任何一个VR开发教程都难以避免的一环,本篇作为VRTK的开发教程还是对VR项目设置交代一下。 准备好你的硬件 头盔必须是6DoF的,推荐Oculus Quest系列,Rift系列,HTC和Pi…

vue2的mixins和vue3的hooks

vue2的mixins和vue3的hooksvue2的mixins vue2 mixins 钩子函数vue2的Mixins是怎么使用的 vue3的hooks 什么是hooks如何使用 vue2的mixins和vue3的hooks Vue.js 2.x 中的 mixins 和 Vue.js 3.x 中的 Composition API (包括 hooks) 是 两种不同的代码组织和复用机制。 Vue.js 2…

C语言scanf()函数读取包含空格的字符串

scanf()函数读取输入时&#xff0c;遇到空格或者回车结束读取&#xff1b; 所以输入的字符串中有空格&#xff0c;到空格就中断了&#xff1b; 根据资料&#xff0c;使用 "%[^\n]" 这样的格式化符遇到空格继续读取&#xff0c;直到敲了回车&#xff1b; 看一下&am…