如何将公共组件设计成使用更方便的命令式组件?
技术背景:
react+antd
需求背景:
在开发的过程中,随着项目越来越大,会出现越来越多的公共组件,如删除按钮、日志侧滑框等等,相信开发者一定会对这些组件进行封装,通过传入一些控制参数来进行组件调用。如题的问题,便是在封装之后的使用时产生,现在我们一步步遇到问题并解决他。
封装组件:
本文以项目中的日志组件为例。日志在一些存在复杂工作流、事项的系统中普遍存在,通常后端提供的接口只有一个,查询时根据不同的业务id和业务类型调取对应的操作日志或系统日志。
故我们封装出来的组件大抵如下:
import type { DrawerProps } from 'antd';
import { Drawer, Steps } from 'antd';
import { useEffect, useState } from 'react';
type Props = {
bizNo: string;
} & DrawerProps;
/**
* 调用日志的侧滑框,传入启闭开关和bizNo即可
* @visible: 开启开关
* @setLogVisible: 设置开启和关闭
* @bizNo: 查询日志参数,主键
*/
function LogDrawer({ bizNo, ...props }: Props) {
const [logList, setLogList] = useState<PROJECTAPI.ProjectLogItem[]>([]);
return (
<Drawer title="日志" {...props}>
{内容}
</Drawer>
);
}
export default LogDrawer;
该组件为日常工作中最容易想到的封装方式,不仅支持传入关键参数bizNo,还支持透传Drawer的所有属性,在调用时已经达到了比较方便的地步。
新问题的产生:
在组件的调用过程中发现,每一个需要调用的父组件都需要引入该组件并传参
import LogDrawer form '@/components/LogDrawer';
//省略其余部分
.....
<LogDrawer bizNo={...}/>
.....
当需要引用该组件的文件越来越多时,发现代码重复率越来越高。于是想将其再次封装
思路1:首先基本都是查看日志按钮才会调用,可以将按钮封装,又发现这样与LogDrawer本身无区别,遂否定;
思路2:想到了antd本身Modal中的Modal.confirm()的调用。便想模仿优秀的封装来进行此处封装;
目标:调用组件时
import LogDrawer form '@/components'
LogDrawer.show({bizNo:...,...props}) // ...props为DrawerProps,即antd的Drawer的自带属性
代码思路1:
import type { DrawerProps } from 'antd';
import { Drawer, Steps } from 'antd';
import { useEffect, useState } from 'react';
type Props = {
bizNo: string;
} & DrawerProps;
/**
* 调用日志的侧滑框,传入启闭开关和bizNo即可
* @visible: 开启开关
* @setLogVisible: 设置开启和关闭
* @bizNo: 查询日志参数,主键
*/
function LogDrawer({ bizNo, ...props }: Props) {
const [logList, setLogList] = useState<PROJECTAPI.ProjectLogItem[]>([]);
return (
<Drawer title="日志" {...props}>
{内容}
</Drawer>
);
}
// 不同点在这里
LogDrawer.show = ()=>{
// 操作1
}
export default LogDrawer;
需要在操作1中改变LogDrawer的open属性,可是在函数中怎么改变LogDrawer的open呢?首先想到的是状态管理,本项目使用的umi,再一构想发现无论useModel还是dva,都没办法完成要求,遂放弃状态管理的方案。
搜索后得到了本文的最终解:
命令式组件
函数中如何操作dom节点呢?当然是创建节点,插入节点,并且通过ReactDom.render来渲染节点了,故容易得到如下代码:
import type { DrawerProps } from 'antd';
import { Drawer, Steps } from 'antd';
import { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
type Props = {
bizNo: string;
} & DrawerProps;
/**
* 调用日志的侧滑框,传入启闭开关和bizNo即可
* @visible: 开启开关
* @setLogVisible: 设置开启和关闭
* @bizNo: 查询日志参数,主键
*/
function LogDrawer({ bizNo, ...props }: Props) {
const [logList, setLogList] = useState<PROJECTAPI.ProjectLogItem[]>([]);
return (
<Drawer title="日志" {...props}>
{内容}
</Drawer>
);
}
// 不同点在这里
LogDrawer.show = ()=>{
// 创建侧滑框的父节点
const DrawerBox = document.createElement('div');
document.body.appendChild(DrawerBox);
ReactDOM.render(
<LogDrawer
{...props}
open
// 将onClose函数卸载结构props的下方,避免props中的close覆盖关闭操作;
onClose={(e) => {
props?.onClose?.(e);
// 通过卸载渲染节点来关闭;
ReactDOM.unmountComponentAtNode(DrawerBox);
}}
/>,
DrawerBox,
);
}
export default LogDrawer;
至此,封装结束,在需要的组件中引用后,调用与预期一致,并且不影响之前的调用,可以渐进地替换为新调用方法。
ps:以日志只是作为示例,实际开发结合业务还需灵活拓展。
组件封装要遵循高内聚低耦合的原则,业务一定要与样式分离,定义清楚不同组件的边界,如错误消息提示和警告消息提示永远是一个组件的不同传参,边界的定义是消息提示,而不能是弹出内容的组件,否则Modal与message组件就要一起封装,遇到的问题就会很多了。封装让开发事半功倍,无效封装会增加开发风险。