本文主要讲述的内容是基于 React + Redux 的项目,应该如何设计状态管理器。用过 Redux 的同学应该都知道,状态管理器可以同时处理多个数据模型 Model, 并且每个 Model 中都会包含 Action、Reducer、InitialState、Effect。其中:
Effect表示异步请求的方法,一般都是封装的 http 请求;Action表示在 Effect 异步请求完成的回调任务中应该执行的具体动作;Reducer表示状态更新函数,它将根据Action.type去更新相应的数据,InitialState表示该数据模型的初始化状态;
Model 层的封装过程大致如下:
// src/models/main.js
const effects = {
queryUserInfo(query) {
return function(dispatch, getState) {
return axois.post('/v1.0/query/user-info', query)
.then(resp => {
const data = resp.data ?? {};
// 触发 Action
dispatch({ type: 'UserInfo', payload: data });
});
}
},
queryCount(query) {
return function(dispatch, getState) {
return axois.get('/v1.0/query/count', query)
.then(resp => {
const data = resp.data ?? {};
// 触发 Action
dispatch({ type: 'Count', payload: data });
});
}
}
};
const initState = {
userInfo: {
name: '',
sex: '',
age: 0,
},
count: 9999999,
}
const reducer1 = function(prevState = {}, action) {
switch (action.type) {
case 'UserInfo':
return { ...prevState, userInfo: action.payload };
case 'Count':
return { ...prevState, count: action.payload };
default:
return prevState;
}
}
创建 store
// src/store.js
const reducer = combineReducers({ main: reducer1, systemUi: reducer2 });
const store = configureStore({
reducer,
preloadedState,
});
以上便是我早前使用 Redux 时的封装过程,其中 initialState、reducer、effect 逻辑都是分开的,当一个 Model 中管理的状态特别多时,就逻辑就比较混乱。记不住哪个 effect 对应的 action.type 是什么。并且在使用时也很不方便:
// src/pages/home.jsx
import { effects } from '@/models/main';
export default function Page(props:any) {
const model = useSelector((state) => state['main']);
const dispatch = useDispatch();
const actions = useMemo(() => {
return bindActionCreators(effects, dispatch);
}, []);
// ...
}
优化
优化策略一
封装一个超类 Model,可自动生成 reducer 函数,每个数据模型在实现时继承 Model;
生成 reducer 函数时,根据每个 effect 方法的名称 + model.name 来生成 action.type;
优化策略二
封装两个钩子函数,分别对 useSelector 和 useDispatch 进行封装,简化使用方式,减少代码量。
策略一的实现
import type { Action, Dispatch } from '@reduxjs/toolkit';
export type ReduxAction = { payload: Function | object | null } & Action;
export type Effect = (query?: any) => (dispatch: Dispatch, getState?: any) => Promise<any>;
export default class Model {
// 生成 reducer 函数
reducer<S>(): Reducer<S, ReduxAction, S> {
// 子类的名称
const prefix = this.constructor.name;
// 子类原型对象上定义的方法
const keys = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
keys.splice(keys.indexOf('constructor'), 1);
// prettier-ignore
const initialState = Object.assign({}, this) as S;
const finalTypes = keys.map((key) => `${prefix}/${key}`);
// 当 finalTypes 集合中包含 action.type 时才会触发状态更新;
// 另外,如果 action.payload 为 null、undefined 时,将不会更新;
return function (initState: S = initialState, action: ReduxAction): S {
// eslint-disable-next-line
if (finalTypes.includes(`${prefix}/${action.type}`) && action.payload != null) {
const payload = action.payload;
return {
...initState,
...(typeof payload === 'function' ? payload(initState) : payload)
};
} else {
return initState;
}
};
}
// 从子类的 prototype 中提取 effects,要排除 constructor 属性。
effects<A>() {
const prototype = Object.getPrototypeOf(this);
const keys = Object.getOwnPropertyNames(prototype);
keys.shift();
const effects = {} as any;
keys.forEach((key) => effects[key] = prototype[key]);
return effects as A;
}
}
通过超类 Model 你会发现,所有的 Effect 方法只能定义在子类的原型对象上。
那么具体的数据模型应该如何定义:
import * as api from '@/api/main';
import Model from '@/common/model';
import { getLocalStorage } from '@/utils';
import type { Dispatch } from '@reduxjs/toolkit';
export default class MainModel extends Model {
regionList: Array<any> = [];
userInfo: { [key: string]: any } = getLocalStorage('USER_INFO') || {};
queryRegionList(query: any) {
return (dispatch: Dispatch) => {
// api 表示接口
return api.queryRegionList(query).then((response: any) => {
dispatch({
type: 'queryRegionList',
payload: { regionList: response?.data ?? [] }
});
return response;
});
};
}
queryUserInfo() {
return (dispatch: Dispatch) => {
return api.queryUserInfo().then((response: any) => {
// 如果你不希望更新 model,请将 payload 设置为 null。
dispatch({
type: 'queryUserInfo',
payload: { userInfo: response?.data ?? {} }
});
// 也可以不返回任何内容,返回的内容可以在 .then() 的回调函数中获取。
return response;
});
};
}
}
所有的 InitialState 都是定义在子类的实例对象上的,获取时直接通过实例对象进行访问即可。这样设计的目的是因为,开发中需要知道每个 Model 层中对应数据类型,这个时候直接借助 TypeScript 类型检查就可以知道这个Model 的数据结构。
如果将一个 Class 作为接口使用时,TS 只会检查它的实例属性和方法,不会检查它的静态属性/方法,以及原型对象上的属性和方法。
开发中还应该将所有的 models 全部导入 src/models/index.ts 中:
import MainModel from './main';
import WorkInfoModel from './workInfo';
import SystemRoleModel from './systemRole';
import MainModel from './main';
import WorkInfoModel from './workInfo';
import SystemRoleModel from './systemRole';
export const allModelState = {
mainModel: new MainModel(),
workInfoModel: new WorkInfoModel(),
systemRoleModel: new SystemRoleModel(),
};
export type AllModelStateType = typeof allModelState;
/**
* 注意
* 以下的内容可以定义,也可以不定义。
* 个人认为还是统一在这里定义并导出最好。业务逻辑中可直接导入使用
*/
// 全局共享的数据
export const mainReducer = allModelState.mainModel.reducer<typeof MainModel>();
export const mainActions = allModelState.mainModel.actions<typeof MainModel.prototype>();
// 作业信息
export const workInfoReducer = allModelState.workInfoModel.reducer<typeof WorkInfoModel>();
export const workInfoActions = allModelState.workInfoModel.actions<typeof WorkInfoModel.prototype>();
// 系统管理
export const systemRoleReducer = allModelState.systemRoleModel.reducer<typeof SystemRoleModel>();
export const systemRoleActions = allModelState.systemRoleModel.actions<typeof SystemRoleModel.prototype>();
策略二实现
import { useMemo } from 'react';
import type { AllModelStateType } from '@/models';
import { bindActionCreators } from '@reduxjs/toolkit';
import { useSelector, useDispatch } from 'react-redux';
// 获取函数的参数类型
type PT<T> = T extends (param: infer P) => any ? P : never;
// 获取函数的返回值类型
type RT<T> = T extends (...args: any) => infer R ? R : never;
// 如果函数的返回值仍然是一个函数,则返回内部函数的返回值类型
type RT2<T> = RT<RT<T>>;
/**
* 注意,
* 如果函数的参数是一个可选参数,那么 PT<T> 得到的结果中包含 undefined 类型
* 如果函数的参数为空,那么 PT<T> 得到的结果为 unknown。
* 并且 undefined extends unknown
*/
type Actions<A> = {
[K in keyof A]: undefined extends PT<A[K]>
? unknown extends PT<A[K]>
? () => RT2<A[K]>
: (query?: PT<A[K]>) => RT2<A[K]>
: (query: PT<A[K]>) => RT2<A[K]>;
};
export function useActions<A>(actions: A) {
const dispatch = useDispatch();
// 避免每次函数组件渲染时重新计算。
return useMemo(() => {
return bindActionCreators(actions as any, dispatch);
}, [dispatch, actions]) as Actions<A>;
}
export function useModel<T extends keyof AllModelStateType>(name: T) {
return useSelector<AllModelStateType>((state) => state[name]) as AllModelStateType[T];
}
上述 useActions 钩子将根据传入的 actions 参数通过 bindActionCreators 进行合成,并通过 useMemo() 避免函数组件每次渲染时都重新计算。另外 Actions<A> 将重新定义每个 action 方法的类型。否则,开发者拿到的 action 都被定义为 any。
useModel 钩子只需要传入一个 name 参数。就可以得到相应的数据和正确的类型。
使用
import React, { memo, useCallback, useMemo } from 'react';
import { useModel, useActions } from '@/redux';
import { workInfoActions } from '@/models';
function Page() {
const { regionList } = useModel('mainModel');
const { queryTableList } = useActions(workInfoActions);
//...
}
根据页面加载情况,按需导入
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import React, { useState, useEffect, memo } from 'react';
import { mainActions, mainReducer } from '@/models';
import type { AllModelStateType } from '@/models';
import type { Reducer, ReducersMapObject } from '@reduxjs/toolkit';
import { useModel, useActions } from '@/redux';
import { ReduxAction } from '@/common/model';
import { replaceReducer } from '@/redux';
import { Spin } from 'antd';
type Models = { [propName: string]: Reducer<any, ReduxAction> };
type ChildrenType = React.ReactElement | React.MemoExoticComponent<any>;
type Loader = () => Promise<{ default: ChildrenType }>;
export default function LazyLoader(loader: Loader, models?: Models): React.FunctionComponent {
return memo(() => {
const params = useParams();
const location = useLocation();
const navigate = useNavigate();
const mainModel = useModel('mainModel');
const actions = useActions(mainActions);
const [children, setChildren] = useState<ChildrenType | null>(null);
useEffect(() => {
loader().then((res) => {
// mainReducer 作为公共的数据模型,应保证在各个页面中都有。
// 封装的 store.replaceReducer();
replaceReducer({ mainModel: mainReducer, ...models } as any);
setChildren(() => res.default);
});
}, []);
return (
<Spin delay={500} spinning={!children} size="large">
<div style={{ height: 'calc(100vh - 131px)', display: !children ? 'block' : 'none' }} />
{children
? React.createElement(children.type, {
...mainModel,
...actions,
location,
navigate,
params,
})
: null}
</Spin>
);
});
}
上述代码中,定义了一个惰性加载的高阶组件,从而实现页面的按需加载。并且,在页面加载时,通过 replaceReducer() 函数完成状态管理器中的 model 的动态更新。
总结
读者如果有更好的想法、或建议都可以在下方给我留言,本人将在第一时间回复,多多交流学习,多多点赞。