React 项目结构小结

React 项目结构小结

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

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

使用的依赖

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

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 的使用其实之前也有写过,大概如下:

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

index.ts

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

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 需要重复提供类型问题,代码如下:

ts 复制代码
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 干净一些,大致代码如下:

ts 复制代码
import { example } from './slices/exampleSlice';

const reducers = {
  example,
};

export default reducers;

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

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

ts 复制代码
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 单独抽出来,如:

ts 复制代码
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

大致如下:

ts 复制代码
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 出去

相关推荐
霍先生的虚拟宇宙网络8 分钟前
webp 网页如何录屏?
开发语言·前端·javascript
jessezappy28 分钟前
jQuery-Word-Export 使用记录及完整修正文件下载 jquery.wordexport.js
前端·word·jquery·filesaver·word-export
旧林8431 小时前
第八章 利用CSS制作导航菜单
前端·css
yngsqq1 小时前
c#使用高版本8.0步骤
java·前端·c#
Myli_ing2 小时前
考研倒计时-配色+1
前端·javascript·考研
余道各努力,千里自同风2 小时前
前端 vue 如何区分开发环境
前端·javascript·vue.js
软件小伟2 小时前
Vue3+element-plus 实现中英文切换(Vue-i18n组件的使用)
前端·javascript·vue.js
醉の虾2 小时前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧2 小时前
TypeScript 的发展与基本语法
前端·javascript·typescript
hummhumm3 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j