🚀还在手写后台管理系统的查询表单?来封装一个查询表单组件吧!

977 阅读5分钟

前言

开发这个组件的目的是能够更方便的通过配置来实现字段查询模块,如果你的功能有些复杂,比如说涉及到一些字段的联动逻辑。

其实通过配置也能做,但需要考虑的是,如果把一些复杂的工单做在组件里面,就会导致配置也变得复杂困难,那这个时候倒不如直接写代码了。

具体场景中,是用配置来写,还是通过开发代码来写,首先需要你花几分钟来思考一下。

schema

我们先来看看需要做的是什么,大致就是下面的这样一个组件,相信做过后台管理系统的同学都不会陌生。

image.png

那我们会怎么去使用这个组件呢,可以看一下下面的一个使用 demo

import { useRef, useState } from "react";
import Filter from "../../components/Filter";
import { COMPONENT_TYPE } from "../../components/Filter/constant";

const Config = () => {
  const ref = useRef(null);
  const [disabled, setDisabled] = useState(false);
  const fieldsConfig = [
    {
      name: "name",
      type: COMPONENT_TYPE.INPUT,
      label: "姓名",
      initialValue: "123,",
      componentProps: {
        disabled,
        placeholder: "请输入姓名",
      },
    },
    {
      name: "number",
      type: COMPONENT_TYPE.INPUT_NUMBER,
      label: "数字输入框",
    },
    {
      name: "date",
      type: COMPONENT_TYPE.DATE_PICKER,
      label: "日期",
    },
    {
      name: "status",
      type: COMPONENT_TYPE.SELECT,
      label: "状态",
      initialValue: 1,
      componentProps: {
        placeholder: "请选择状态",
        allowClear: true,
      },
      onChange: (e) => {
        console.log("e", e);
        setDisabled(e === 2);
      },
      data: [
        {
          label: "开启",
          value: 1,
        },
        {
          label: "关闭",
          value: 2,
        },
      ],
    },
  ];
  return <Filter ref={ref} fieldsConfig={fieldsConfig} />;
};

export default Config;

希望是可以通过配置化的形式去调用该组件,对于设计一个组件来说,定义好它的属性以及方法是很有必要的,所以我们先要定义一份 schema

fieldsConfig:[
  {
    // 表单项唯一key
    name:"",
    // 表单项label
    label:"",
    // 表单项组件类型:输入框、下拉、时间选择等
    type:"",
    // 表单项初始值
    initialValue:"",
    // 表单字段值变化时的回调,可以用做字段联动
    onChange:() => { },
    // 表单组件的候选值,比如select的下拉列表
    data:[],
    // 表单项的props,参考Form.Item组件
    formItemConfig: { },
    // 组件的props,参考具体的组件
    componentProps:{ }
  }
]
// 表单的props,参考Form组件
formConfig:{

}
// 一些其他的配置
extraConfig:{
  // 一行有多少个表单项
  columnCount:"",
  // 操作——查询、重置等
  actions:()=>{
  },
}

antd5封装

我们会分别对 antd3 以及 antd5 封装一份组件,但是对于使用方来说 schema 是相同的,所以会在封装过程中磨平这个差异。

image.png

首先我这里通过判断 Form 组件是否有 useForm 这个 hook 来判断 antd 的版本,这是一个运行时的判断方法,最后会介绍一种更通用的判断某个库是什么版本的方法。

然后实现一个 Filter 组件,作为入口:

image.png

先来看基于 antd5 的封装,

image.png

首先是整个表单的布局,会读取传入的配置中的这两个属性,分别表示一行多少个表单项、以及项目之间的间距是多少

image.png

然后分割成一个二维数组,通过 RowCol 这两个组件来实现布局

image.png

最后来到 renderField 中,这里就是主要渲染组件的逻辑,根据传入的不同 type ,来渲染不同的组件:

  const renderField = (field) => {
    let Component = <></>;
    const {
      type,
      name,
      formItemConfig,
      label,
      data,
      componentProps = {},
    } = field;
    switch (type) {
      case COMPONENT_TYPE.INPUT:
        Component = Input5;
        break;
      case COMPONENT_TYPE.SELECT:
        Component = Select5;
        break;
      case COMPONENT_TYPE.INPUT_NUMBER:
        Component = InputNumber;
        break;
      case COMPONENT_TYPE.DATE_PICKER:
        Component = DatePicker;
        break;
      default:
        break;
    }
    return (
      <Form.Item {...formItemConfig} name={name} label={label}>
        <Component {...{ ...componentProps, data }} />
      </Form.Item>
    );
  };

有一些组件需要分成 antd5antd3 版本的实现,有一些则不用,比如 input ,它是不用分开的

image.png

但比如 select ,它5与3版本之间有一个核心 api 差距比较大,所以需要分开,如果是其他透传的 props ,则无需分开,在组件调用的时候注意即可。

image.png

这个初始值的处理会是一个小技巧:

image.png

其实就是把一个数组转化成一个 map ,在开发过程中经常会遇到这个场景,如果不用 reduce 的话,常常会这样处理:

  const func = (list) => {
    const obj = {}
    list.map(item=>{
      obj[item.id] = item.label
    })
    return obj
  }

reduce 则会优雅一些,一行代码可以搞定。

然后,会通过 useImperativeHandle 这个 api ,把 form 对象暴露给父组件调用

image.png

最后就是一个字段联动,对于 antd5 来说,主要是监听 onValuesChange 事件,然后调用 schema 中传入的对应字段的 onChange 方法。

image.png

  const handleValueChange = (fields) => {
    Object.keys(fields).forEach((key) => {
      const onChange = fieldsConfig.find(
        (config) => config.name === key
      )?.onChange;
      if (onChange) {
        onChange(fields[key]);
      }
    });
  };

antd3封装

接下来来看 antd3 的封装,主要差异在于 form 表单组件的封装。一些 api 实现会与 antd5 版本差距比较大:

  • 通过 getFieldDecorator 绑定字段
  • 没有 onValuesChange api ,可以把 schema 中的 onChange 传入具体组件中来监听变更

image.png

  const renderField = (field) => {
    let Component = <></>;
    const {
      type,
      name,
      formItemConfig,
      label,
      data,
      initialValue,
      onChange,
      componentProps = {},
    } = field;
    switch (type) {
      case COMPONENT_TYPE.INPUT:
        Component = Input;
        break;
      case COMPONENT_TYPE.SELECT:
        Component = Select3;
        break;
      case COMPONENT_TYPE.INPUT_NUMBER:
        Component = InputNumber;
        break;
      case COMPONENT_TYPE.DATE_PICKER:
        Component = DatePicker;
        break;
      default:
        break;
    }
    return (
      <Form.Item
        {...{
          labelCol: { span: 6 },
          wrapperCol: { span: 18 },
        }}
        {...formItemConfig}
        label={label}
      >
        {form.getFieldDecorator(name, {
          ...formItemConfig,
          initialValue,
        })(<Component {...{ ...componentProps, data, onChange }} />)}
      </Form.Item>
    );
  };

对于调用方来说, antd3 版本的需要这样拿到 form 实例,有一点差别,但是感觉不大,就不打算抹平这个差异了。

image.png

更优雅的判断antd版本

const fs = require('fs');
const path = require('path');

// 定义宿主环境package.json路径
const packageJsonPath = path.resolve(process.cwd(), 'package.json');

// 读取并解析package.json文件
fs.readFile(packageJsonPath, 'utf8', (err, data) => {
    if (err) {
        console.error('Error reading package.json:', err);
        process.exit(1);
    }

    try {
        const packageJson = JSON.parse(data);
        
        if (packageJson.dependencies && packageJson.dependencies['antd']) {
            // 拿到antd版本
            console.log(packageJson.dependencies['antd'])
        }
        
    } catch (e) {
        console.error('Error parsing package.json:', e);
        process.exit(1);
    }
});
{
  "name": "your-npm-package",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "install": "node install.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

拿到 antd 版本之后,再往包里写入一些东西,来标识使用哪一个版本的组件。在这个场景下,如果做成 npm 包,下载的时候 npm 包不应该被编译,而是跟着宿主项目一起编译打包。

最后

为什么要重复造轮子而不是使用现成的一个库?看了一下 formRenderformily 的依赖,都是需要 antd4 及以上,而我们有几个项目都是 antd3 ,而且版本不会再升级。所以就自己造了一个。

但是,这种类似“低码”的都是都有一个通病——要么不够灵活,要么配置成本又很高,所以自己内心应该有一杆秤,什么时候手写,什么时候用这个配置化的东西,做需求之前花两分钟思考一下。

最后,封装这个东西的初衷也是为了方便我自己,至于它能不能造福别人,就看缘分了哈哈哈。