最近刚刚把重构推进一个段落,所以想整理一下这个项目里面用过的一些封装思路
这里主要指的是 axios,因为 axios 支持的比较好,所以一般项目利用的都是 axios 而不是 fetch,主要实现内容就是之前笔记里提过的:axios 的简易封装 和 封装一个 axios url encoding serialize util
我基本上就是沿用这个思路封装了 5 个函数:
基础函数
基础函数是 CRUD 调用的,大致实现方法如下:
export const request = async ({
apiMethod,
uri,
params = {},
data,
}: ApiProps): Promise<AxiosResponse<any, any>> => {
// 这里也会负责一些其他的操作,主要是对 headers 和 params 进行处理
// 这个对于所有的 CRUD 操作都是一样的,因此集中封装了一个函数进行处理
const headers = {};
// 封装的另一个原因也是在于 paramsSerializer 这一段不是很想重复好几遍
return await axios({
method: apiMethod,
url: uri,
data,
params,
headers,
paramsSerializer: (params) => {
return toQuery(params);
},
});
};
主要抽出来的原因就是三个:
Create
export const createSingleEntity = async (
state: RootState,
uri: string,
model: any,
params: any = {}
) => {
// 这里也是一个对 model 操作的地方
const data = processModel(model);
return await request({
apiMethod: ApiCallTypes.POST,
uri,
data,
params,
});
};
这里抽出来的主要原因是需要对 model 进行另外的处理,将 {attr1: sth, attr2: str}
这样一个 object 的数据转成后台需要的类型,以我们项目来说,大概是 {type: uri, data: model, id: 0}
这样的操作
建于所有的 entity 都需要同样的处理,与其放在调用的地方,不如集中到 createSingleEntity
中进行处理
Retrieve
这里主要处理是需要对数据进行一个 pagination 的操作,这同样也是所有 entity 共通的,因此放在这里集中操作
Update
Update 的处理更加麻烦一些,主要是我们的 Update 用的是 jsonpatch,并且操作可以包含 CUD 的操作,因此需要调用不同的 wrapper 以生成后端需要的数据格式
Delete
Delete 也是操作 deleteSingleEntity
,如果是 batch 操作的话,会转到 update
去操作
至于为什么不直接调用 update
呢……?
这个同样涉及到数据的处理操作,与其在每一个需要删除对象的地方,都额外包装一次数据,不如将其统一放到 delete
种操作,这样在处理每一个 endpoint 能够少写几行代码
TS 比较常用的是新建一个个性化对象,不过 Redux 要求的是一个可序列化对象——也就是基础的对象、数组类型,考虑过后这最终还是决定用一个 Type Definition+default object 的操作去做,如:
type ISomeObject = {
// type definition
};
const defaultSomeObject: ISomeObject = {
// initialize value
};
但是,如果没有 redux 这个 rehydrate 的需求,也可以直接创建一个 custom object:
export class SomeObject {
constructor(
public readonly x: number,
protected y: number,
private z: number
) {
// No body necessary
}
}
这样去进行操作,关于 parameter properties 的缩写部分,在 TypeScript 构造函数中解构参数属性 有提,包括找到的另一个操作方法。
另外关于模型,这里也做了一些其他的优化:
以前的数据会使用数据扁平化
也就是对象中包含对象的处理,之前遇到这种情况,会将对象中一些需要的数据抠出来进行单独存放,这一点因为 UI 方面也做了同步的升级,可以获取嵌套对象的数据,所以这一步就不需要了
移除了复合模型
这里同样也是 UI 方面做的优化,假设说当前页面的数据来源于 A
和 B
,现在直接使用 useMemo
,在 dependency array 中监听 A[]
和 B[]
的变化,然后自动完成数据拼接
之前的操作则是先调用 API 获得 A[]
和 B[]
,随后拼成组合模型 C[]
,在页面中直接渲染 C[]
。这样一些操作就非常的麻烦,因为需要同时更新 A[]
+C[]
/B[]
+C[]
/ A[]
+B[]
+C[]
这样的情况。这也说明 state 中出现了重复数据,才会出现既要更新单个模型(A/B),又要更新复合模型©的情况
React 项目结构小结 也提了一下 Redux 的基本实现,这里再细化一下 CRUD 相对的操作逻辑:
过滤方法
按照上面提到的,如果是用第一种,也就是用 type+default object 的方法,可以使用一个 lodash 的函数:pick
,语法为 _.pick(object, [paths])
,它会返回一个新的过滤好的对象:
var object = { a: 1, b: '2', c: 3 };
_.pick(object, ['a', 'c']);
// => { 'a': 1, 'c': 3 }
[paths]
可以使用 Object.keys(defaultObject)
获得
如果使用 custom class 的话,则可以用 spread operator 进行操作:
const newObj = new SomeObject(...data);
Create
这里主要是处理一下数据,将一些 UI 中多余的数据进行过滤——比如说应对一些验证可能会出现 isValid
这样的属性,这是后端不需要的。之后将处理过的对象还原成 API 所需要的格式,这点就交给封装好了的 createSingleEntity
去做
Retrieve
Retrieve 的操作基本上是一样的,在数据类型定义的非常明确的情况下,这一块甚至可以单独的抽成一个函数去调用
Update
操作与 Create 的 case 相似,不过这里需要将数据稍微整理一下成 UpdateEntities
需要的数据格式,我主要将数据格式定义成下面这样:
type IUpdatedEntities<T> = {
updateEntities: {
addEntity: T[];
updateEntity: Partial<T>[];
deleteEntity: T[];
};
uri: string;
};
delete 主要后端只需要接受 id,但是 UI 的选项是不会做到只返回选择的 id,而是返回选择的对象。在考虑了一下之后,建于所有的对象都需要从 T[]
转成 string[]
的操作,因此将其放到了 UpdatedEntities
,而非每一次的删除操作中
Delete
操作基本一致,如果只选中一个对象,那么直接调用 delete/entityName/id
如果选中多个数据,则就接收到的 T[]
传到对应的 deleteEntities
中
UI 这一块基本上跟业务有关,这里只是简单地提供一下思路和与我们项目相关的一些实现。
这里还是以我们的项目为例,每一个页面都有对应的 CRUD 的操作,R 已经实现完毕了,对于 CUD 的操作主要通过 表格(table) 和 表单(form) 去实现。表格的东西其实主要通过 UI 库实现的,主要就是写一些 custom validator、custom filter 之类的,onChangeHandler 也是表单内部实现的,我们主要就是拿到修改过的数据,因此这里没什么好说的
表单则是重复比较多的重灾区,分析一下原因主要有这么几个问题:
class based component 的案例如下:
// class based component
export class ExampleClass extends Component {
constructor(props) {
this.state = {
modalManager: {
isModalOpen: false,
modalID: '',
modalContent: emptyDiv,
headerText: emptyDiv,
toggleFn: emptyFunc,
submitFn: emptyFunc,
isDeleteModal: false,
},
};
}
// setModal & closeModal 是 class based component 重复的重灾区
setModal = ({
headerText,
modalContent,
toggleFn,
submitFn,
modalID = '',
isDeleteModal = false,
}) => {
this.setState({
modalManager: {
isModalOpen: true,
modalID,
modalContent,
headerText,
toggleFn,
submitFn,
isDeleteModal,
},
});
};
closeModal = () => {
this.setState({
modalManager: {
isModalOpen: false,
modalID: '',
modalContent: emptyDiv,
headerText: emptyDiv,
toggleFn: emptyFunc,
submitFn: emptyFunc,
isDeleteModal: false,
},
});
};
// CUD 这里就需要调用 setModal 3 次
render() {
return (
<>
{/* 省略其他的 render */}
{modalContent}
>
);
}
}
functional component 的案例如下:
// 省略 import
export const Example = () => {
const {
modalManager: {
modalID,
isModalOpen,
modalContent,
headerText,
toggleFn,
submitFn,
isDeleteModal,
},
setModal,
closeModal,
} = useModalManager();
const setAddModal = () => {
setModal({
headerText,
modalID: 'add',
toggleFn: closeModal,
modalContent: <>some component here>,
submitFn: addFunc,
isDeleteModal: false,
modalSize: 'md',
});
};
const setEditModal = () => {
setModal({
headerText,
modalID: 'edit',
toggleFn: closeModal,
modalContent: <>some component here>,
submitFn: editFunc,
isDeleteModal: false,
modalSize: 'md',
});
};
const setDeleteModal = () => {
setModal({
headerText,
modalID: 'delete',
toggleFn: closeModal,
modalContent: <>some component here>,
submitFn: deleteFunc,
isDeleteModal: true,
modalSize: 'md',
});
};
return (
<>
{/* 省略其他的 render */}
{modalContent}
>
);
};
functional component 虽然好了一些,但是重复次调用还是比较厉害的
这个实现有另外一个问题,是刚开始使用 hooks 没发现,但是挪到 redux 的时候就 gg 的问题——将 react component 存储到了 state 中去。之前使用 RTK 的时候就直接报错,说 mutate #object
之类的,后面一排查发现是 modalContent
的问题
最后在了解了一下整个项目后,做出了这样的修改:
创建一个新的 redux modal slice
重新定义了一个新的 hooks 去管理 CUD 的操作
type IUseSetModal = {
currentEntityName: string;
readOnly?: boolean;
addStructure?: Record;
addSubmitFn?: (args: T) => Promise;
editStructure?: Record;
editSubmitFn?: (args: T) => Promise;
selectedEntities?: Record>;
deleteSubmitFn?: (args: Record>) => Promise;
};
const useSetModal = ({}: // 所有上面定义的属性,并且为可选属性提供默认值
IUseSetModal) => {
const dispatch = useAppDispatch();
const handleAdd = () => {
dispatch(
setModal({
readOnly,
submitFn: addSubmitFn,
structure: addStructure,
currentEntityName,
modalOp: 'add',
})
);
};
const handleDelete = () => {
dispatch(
setModal({
readOnly,
submitFn: deleteSubmitFn,
currentEntityName,
modalOp: 'delete',
selectedEntities,
deselectAll,
})
);
};
const handleEdit = () => {
dispatch(
setModal({
readOnly,
submitFn: editSubmitFn,
structure: editStructure,
currentEntityName,
modalOp: 'edit',
selectedEntities: Object.values(selectedEntities!)[0],
})
);
};
return { handleAdd, handleDelete, handleEdit } as const;
};
export default useSetModal;
将 modal wrapper 的东西放到 App.js 下面去进行全局渲染
大致代码如下:
const GlobalModalWrapper = () => {
const {
isOpen,
submitFn,
modalOp,
deselectAll,
currentEntityName,
selectedEntities,
readOnly,
customValidator,
structure,
isLoading,
} = useAppSelector>((state: RootState) => state.modal);
const dispatch = useAppDispatch();
const [entity, setEntity] = useState>({});
const onChangeHandler = () => {
// do sth
};
useEffect(() => {
if (modalOp === 'add' || modalOp === 'edit') {
// do sth
return;
}
if (modalOp === 'delete') {
// do sth
}
}, [selectedEntities, modalOp]);
let headerText = '',
content = null;
const entityName = startCase(currentEntityName),
op = startCase(modalOp);
switch (modalOp) {
case 'add':
case 'edit':
headerText = `${op} a ${entityName}`;
content = sth;
break;
case 'delete':
headerText = `${op} ${entityName}(s)`;
content = sth;
}
return (
{
dispatch(closeModal());
}}
submitFn={async () => submitFn!(entity)}
deselectAll={deselectAll}
>
{content}
);
};
export default GlobalModalWrapper;
所以最终在组件里面的调用就是这样的:
export const Example = () => {
const { handleAdd, handleEdit, handleDelete } = useSetModal({
currentEntityName: '',
readOnly: some_bool,
addStructure: {},
addSubmitFn: add_function,
editStructure: {},
editSubmitFn: edit_func,
selectedEntities,
deleteSubmitFn: edit_func,
customValidator: some_func,
});
};
这样每个页面上的 modal 操作从 30+行 省略到了 10+ 行,需要写的瘦身差不多一半,class based component 的话省的要稍微多一些,不过主要也是因为重复声明两个函数造成的
总体上来说这次重构的效果还是挺好的,完成了:
测试代码的添加
虽然是从 redux 开始,不过我们终于开始写测试了……
代码更加的模块化
使用 redux 集中化数据
这一部分其实也减少了一些重复调用的代码,毕竟数据和页面也存在 1-to-many 的情况
单个页面开发速度提升 50%
之前简单的页面实现周期大概是一周左右(5-6 天),现在因为代码已经封装好了,所以实现周期提速到了 2-3 天
现有的代码量大概优化了 50%+
每个页面渲染本身优化了大概 40%左右
数据处理是个重灾区,之前每个 entity 都是独立处理的,现在可以通过通过调用封装后函数进行处理。平均上来说大概优化了 60-70%
总体平均一下,代码量对半砍是肯定有的