如何将公共组件设计成使用更方便的命令式组件?

293 阅读4分钟

如何将公共组件设计成使用更方便的命令式组件?

技术背景:

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组件就要一起封装,遇到的问题就会很多了。封装让开发事半功倍,无效封装会增加开发风险。