快来康康五年前端是怎么做增删改查的,基于Antd开箱即用的Modal封装,让新增与修改表单效率起飞!

916 阅读5分钟

此文暂且不聊表格展示和表格删除,主要针对了增加和修改表单场景

前言

最新又开始写CRUD了,功能如图:

image.png

image.png

image.png 写之前就在思考,马上都3202年了,CRUD的最优解是什么?做了一系列调研,大抵是三种方案

  1. 低代码系统生成CRUD页面

参考# 如何设计低代码平台快速构建页面 | (200+页面)阿里的formily

实现思路大概是:可视化操作生成一串JSON,封装好一个渲染组件,依照协议消费此JSON,完成渲染。

优点:
1. 在这种系统支持的业务模式下可快生产CRUD页面
2. 无需手工编码

缺点:
1. 强依赖于低代码平台以及相关的SDK,脱离平台或SDK无法工作
2. 二次需求开发困难
3. 复杂业务逻辑无法支撑
4. 构建一套完善的低代码系统需要不少的人力成本

2. 采用第三方封装好的库

目前并没有搜索到此场景下的完整封装

3. 自己动手、丰衣足食

基于时间成本与后期可维护性的考虑,还是摒弃了低代码系统,选择手动编码。

然后就基于AntdModal组件,将新增修改两个场景的弹出框利用高阶组件来进行了一层封装,基于真实场景沉淀开箱即用点击Demo体验

实现思路

流程图

withModal流程图.png

使用

1. 创建一个基于Antd-Form的常规表单组件UserForm,接受form和formProps参数(通过高阶组件传递过来的参数),将其传到Form组件上

import { IPropsFromWithModal } from "./withModal/type";
import { Form, Select, Input } from "antd";

export type IUserFormValue = {
  username: string;
  password: string;
};

const UserForm: React.FC<IPropsFromWithModal<IUserFormValue>> = ({
  form,
  formProps,
}) => {
  return (
    <Form {...formProps} form={form}>
      <Form.Item
        name="userName"
        label="用户名"
        rules={[
          {
            required: true,
            message: "请输入名称"
          }
        ]}
      >
        <Input placeholder="请输入"/>
      </Form.Item>
      <Form.Item
        name="password"
        label="密码"
        rules={[
          {
            required: true,
            message: "请输入"
          }
        ]}
      >
        <Input.Password placeholder="请输入" />
      </Form.Item>
    </Form>
  );
};

2. 引入withModal,用withModal包裹一下该表单组件,默认导出包裹后的组件

import { withModal } from '@/components/WithModal';

const UserFormModal = withModal<IUserFormValue, any>(UserForm);
export default UserFormModal;

包裹后组件的实例通过actionRef参数暴露了open/close两个方法

对应的参数如下:

export type IFormWrapperOpenProps<FormValues = any> = {
/** Modal的标题 */
title: React.ReactNode;

/** 表单初始值 */
formValues?: FormValues;

/** 请求函数 */
submitRuquest: (...args: any[]) => Promise<unknown>;

/** 请求额外的参数 */
submitExtraParams?: Record<string, any>;

/** 表单模式 */
mode?: IMode;
};



export type IFormWrapperRef<FormValues = any> = {
/** 表单弹窗打开接口 */
open: (openProps: IFormWrapperOpenProps<FormValues>) => void;

/** 表单弹窗关闭接口 */
close: () => void;
};

3.引入该组件即可直接使用

通过actionRef直接调用组件暴露的open与close方法,并传递相关参数以使用,具体例子请参考这里

import { useRef } from "react";
import { Button } from "antd";
import { createUser, editUser } from "./service";
import UserFormModal from "./Form";

export default function Demo() {
  const userFormModalRef = useRef<IFormWrapperRef>(null);

  return (
    <div>
      <Button
        onClick={() => {
          userFormModalRef.current?.open({
            title: "新增用户",
            submitRuquest: createUser,
            mode: "add"
          });
        }}
      >
        新增用户弹框
      </Button>
      <Button
        onClick={() => {
          userFormModalRef.current?.open({
            formValues: {
              userName: "用户01",
              password: "123456",
              userOrgan: "option2"
            },
            title: "编辑用户",
            submitRuquest: editUser,
            submitExtraParams: { id: 1 },
            mode: "edit"
          });
        }}
      >
        修改用户弹框
      </Button>
      <UserFormModal
        actionRef={userFormModalRef}
      />
    </div>
  );
}


代码

withModal/index.tsx

import { IFormWrappedModalProps, ISaveRef } from "./type";
import React, { useState, useImperativeHandle, FC } from "react";
import { useEffect, useRef } from "react";
import { Modal, Button, Form, Spin } from "antd";
import cloneDeep from "lodash/cloneDeep";

export function withModal<FormValues = any, RequestRes = any>(
  Component: React.FunctionComponent<any>
) {
  const WrappedComponent: FC<IFormWrappedModalProps<FormValues, RequestRes>> = (
    props
  ) => {
    const [form] = Form.useForm();
    const [visible, setVisible] = useState(false);
    const [btnLoading, setBtnLoading] = useState(false);
    const [formLoading, setFormLoading] = useState(false);
    const [value, setValue] = useState<FormValues | null>(null);
    const [title, setTitle] = useState<React.ReactNode>();
    const saveRef = useRef<ISaveRef>({
      submitRuquest: null,
      submitExtraParams: {},
      mode: "add"
    });

    const {
      actionRef,
      modalProps = {},
      formProps = { labelCol: { span: 6 }, wrapperCol: { span: 14 } },
      handleSubmitParams,
      successCallback,
      errorCallback,
      judegeRequestSuccess = (res) => (res as any).success
    } = props;

    const onCancel = () => {
      setVisible(false);
      setTimeout(() => {
        setValue(null);
      }, 100);
    };

    const onSubmit = () => {
      form.validateFields().then(async (value) => {
        setBtnLoading(true);
        let requstParmas = value;
        if (handleSubmitParams) {
          requstParmas = handleSubmitParams(cloneDeep(value));
        }
        try {
          const { submitRuquest, submitExtraParams } = saveRef.current;
          if (submitRuquest) {
            const res = (await submitRuquest({
              ...requstParmas,
              ...submitExtraParams
            })) as RequestRes;
            setBtnLoading(false);
            if (judegeRequestSuccess(res)) {
              onCancel();
              if (successCallback) {
                successCallback(res);
              }
            } else {
              if (errorCallback) {
                errorCallback(res);
              }
            }
          }
        } catch (error) {
          setBtnLoading(false);
        }
      });
    };

    // 实例挂载表单操作接口
    useImperativeHandle(actionRef, () => ({
      open: ({
        formValues = null,
        title,
        submitRuquest,
        submitExtraParams,
        mode = "add"
      }) => {
        setVisible(true);
        setValue(formValues);
        setTitle(title);
        saveRef.current.submitRuquest = submitRuquest;
        saveRef.current.mode = mode;
        if (submitExtraParams) {
          saveRef.current.submitExtraParams = submitExtraParams;
        }
      },
      close: onCancel
    }));

    useEffect(() => {
      if (value) {
        form.setFieldsValue(value);
      } else {
        form.resetFields();
      }
    }, [value]);

    const { mode } = saveRef.current;

    return (
      <Modal
        title={title}
        maskClosable={true}
        visible={visible}
        onCancel={onCancel}
        destroyOnClose={false}
        width={660}
        footer={[
          <Button type="ghost" key="cancel" onClick={onCancel}>
            取消
          </Button>,
          <Button
            type="primary"
            key="submit"
            onClick={onSubmit}
            loading={btnLoading}
          >
            确定
          </Button>
        ]}
        {...modalProps}
      >
        <Spin spinning={formLoading}>
          {React.createElement(Component, {
            form,
            setFormLoading,
            formProps,
            mode,
            ...props
          })}
        </Spin>
      </Modal>
    );
  };

  return WrappedComponent;
}


withModal/type.ts

import { ModalProps, FormProps, FormInstance } from "antd";
export type IModalProps = Omit<ModalProps, "onOk" | "onCancel">;

export type func = (...args: any[]) => void;

export enum EFormMode {
  add = "add",
  edit = "edit",
  readOnly = "readOnly"
}

export type IMode = "add" | "edit" | "readOnly";

export interface IFormWrappedModalProps<U, T> {
  /** 透传给Modal */
  modalProps?: IModalProps;
  /** 透传给Form */
  formProps?: FormProps;
  /** 暴露操作方法的ref */
  actionRef?: React.RefObject<IFormWrapperRef<U>>;
  /** 提交前处理参数 */
  handleSubmitParams?: (parmas: U) => void;
  /** 请求成功的判定条件 */
  judegeRequestSuccess?: (res: T) => boolean;
  /** 成功的回调 */
  successCallback?: (res: T) => void;
  /** 请求失败的回调 */
  errorCallback?: (res: T) => void;
  /** 允许透传其他值 */
  [index: string]: any;
}

export type IFormWrapperOpenProps<FormValues = any> = {
  title: React.ReactNode;
  /** 表单值 */
  formValues?: FormValues;
  /** 请求函数 */
  submitRuquest: (...args: any[]) => Promise<unknown>;
  /** 请求额外的参数 */
  submitExtraParams?: Record<string, any>;
  /** 表单模式 */
  mode?: IMode;
};

export type IFormWrapperRef<FormValues = any> = {
  /** 表单弹窗打开接口 */
  open: (openProps: IFormWrapperOpenProps<FormValues>) => void;
  /** 表单弹窗关闭接口 */
  close: () => void;
};

export type ISaveRef = {
  submitRuquest: IFormWrapperOpenProps["submitRuquest"] | null;
  submitExtraParams: IFormWrapperOpenProps["submitExtraParams"];
  mode: IFormWrapperOpenProps["mode"];
};
//  Pick<IFormWrapperOpenProps, 'submitRuquest' | 'submitExtraParams' | 'mode'>

export interface IPropsFromWithModal<T>
  extends IFormWrappedModalProps<any, any> {
  /** 表单的实例ref */
  form: FormInstance<T>;
  /** Modal的loading控制方法 */
  setFormLoading: func;
  /** 透传给Form */
  formProps: FormProps;
  /** 表单模式 */
  mode: IMode;
}

遇到的问题

封装的组件需要向外暴露openclose两个方法,首当其冲的思路便是使用React.forwardRefuseImperativeHandle结合,但在TS类型的书写上遇到了问题,在使用forwardRef时,组件接受参数除了通用已知的一些参数以外,还需要透传一些未知参数到真实的Form表单组件中,所以Props中需要有[index:string]:any,比如:

export interface IFormWrappedModalProps<U, T> {
  /** 透传给Modal */
  modalProps?: IModalProps;
  /** 透传给Form */
  formProps?: FormProps;
  /** ProTable的ref */
  tableRef?: MutableRefObject<ActionType | undefined>;
  /** 提交前处理参数 */
  handleSubmitParams?: (parmas: U) => void;
  /** 请求成功的判定条件 */
  judegeRequestSuccess?: (res: T) => boolean;
  /** 成功的回调 */
  successCallback?: (res: T) => void;
  /** 请求失败的回调 */
  errorCallback?: (res: T) => void;
  /** 允许透传其他值 */
  [index: string]: any;
}

但在@types/react中定义的forwardRef类型在增加[index:string]:any便丢失了已定义的所有属性的类型提示,比如modalProps等...,更详细的情况可查看这里,该情况已向@types/React提了issue,不知能否被解决 目前的解决方案就是不用forwardRef,直接接受一个属性actionRef,通过这个属性结合useImperativeHandle来向外暴露方法,具体实现可见代码

结语

如有谬误,欢迎大家指正。

对于CURD,你的最优解是什么呢?欢迎讨论~

6965ab4471bd4e9c98875b58f18d88e6.jpeg

参考文章

React通用解决方案——表单容器

在React中优雅地使用弹窗——useModal