likes
comments
collection
share

封装一个Form,实现对AntD表单的监听

作者站长头像
站长
· 阅读数 60
  1. Form表单的初始化数据拿不到,需要再处理一下(还需要再插入代码)
  2. onValuesChange只能监听到用户行为触发的变化,对于setFiledsValues、setFeildValue、resetFieldsValue等API修改的表单数据是监听不到的(还需要处理一下)
  3. 最最最主要的还是:使用Babel插件在用户无感知的情况下对代码进行大量的转义,很可能造成编译错误、内容丢失、甚至导致线上问题,虽然写了很多单测,但是总和考量下来,这样做风险有点大。

于是,在Babel插件接近尾声的时候,我脑海中忽然闪现出一个想法,既然本质上都是对Form组件的一些API进行魔改,那为什么不干脆一点,直接在代码里面就把Form表单进行改写,一来这样难度更小,另外一方面,业务在使用的时候,也能比较清晰的感知到,这个Form是被封装过的,有魔法的Form。开始进入正题

我们需要做的几件事情

  1. 从源码中拷贝一份当前版本的Form组件
  2. 对源码中的Form进行包装
  3. 劫持onValuesChange事件,监听组件交互后的数据变化
  4. 劫持setFeildValue、setFeildsValue、resetFieldValue等通过API的方式改变Form数据的API
  5. 获取Form的初始化数据,给到window.WATCH_FORM_DATA_EXTENSIONS(因为我们最终的目的是要做一个能监听AntD 表单数据的浏览器插件)

从源码中拷贝一份当前版本的Form组件

这应该是这几步当中最简单的一步了,找到Form 4.x的源码,复制一份到components/super-form/index.tsx

  1. 把引入文件的路径修改一下
  2. FormInstance、FormProps这种导入,又导出的,直接删掉,使用的时候还是从antd导入
import warning from 'antd/lib/_util/warning';
import { FormProvider } from 'antd/lib/form/context';
import ErrorList from 'antd/lib/form/ErrorList';
import { useForm, useWatch } from 'antd/lib/form/Form';
import Item from 'antd/lib/form/FormItem';
import List from 'antd/lib/form/FormList';
import useFormInstance from 'antd/lib/form/hooks/useFormInstance';
// FormInstance、FormProps这种导入,又导出的,直接删掉,使用的时候还是从antd导入
// import InternalForm, { FormInstance, FormProps } from './Form';
import InternalForm from './Form';

type InternalFormType = typeof InternalForm;

type CompoundedComponent = InternalFormType & {
  useForm: typeof useForm;
  useFormInstance: typeof useFormInstance;
  useWatch: typeof useWatch;
  Item: typeof Item;
  List: typeof List;
  ErrorList: typeof ErrorList;
  Provider: typeof FormProvider;

  /** @deprecated Only for warning usage. Do not use. */
  create: () => void;
};

const Form = InternalForm as CompoundedComponent;

Form.Item = Item;
Form.List = List;
Form.ErrorList = ErrorList;
Form.useForm = useForm;
Form.useFormInstance = useFormInstance;
Form.useWatch = useWatch;
Form.Provider = FormProvider;
Form.create = () => {
  warning(
    false,
    'Form',
    'antd v4 removed `Form.create`. Please remove or use `@ant-design/compatible` instead.'
  );
};

export default Form;

对原本的Form组件进行包装

  1. 引入ant中的Form,对 InternalForm 进行重写
  2. 目前看到的components/super-form/index.tsx和AntD原生的能力是一模一样的
import React from 'react';
import warning from 'antd/lib/_util/warning';
import { FormProvider } from 'antd/lib/form/context';
import ErrorList from 'antd/lib/form/ErrorList';
import { useForm, useWatch } from 'antd/lib/form/Form';
import Item from 'antd/lib/form/FormItem';
import List from 'antd/lib/form/FormList';
import useFormInstance from 'antd/lib/form/hooks/useFormInstance';
// FormInstance、FormProps这种导入,又导出的,直接删掉,使用的时候还是从antd导入
// import InternalForm, { FormInstance, FormProps } from './Form';
import { Form as AntdForm } from 'antd';
import type { FormProps } from 'antd';

type InternalFormType = typeof InternalForm;

type CompoundedComponent = InternalFormType & {
  useForm: typeof useForm;
  useFormInstance: typeof useFormInstance;
  useWatch: typeof useWatch;
  Item: typeof Item;
  List: typeof List;
  ErrorList: typeof ErrorList;
  Provider: typeof FormProvider;

  /** @deprecated Only for warning usage. Do not use. */
  create: () => void;
};

// !!! 划重点,目前的Form组件和AntD原生的能力是一模一样的
const InternalForm: React.FunctionComponent<
  FormProps & {
    name: string;
  }
> = (props) => {
  const { form, children, ...resetProps } = props;
  return (
    <AntdForm form={form} {...resetProps}>
      {children as React.ReactNode}
    </AntdForm>
  );
};

const Form = InternalForm as CompoundedComponent;

Form.Item = Item;
Form.List = List;
Form.ErrorList = ErrorList;
Form.useForm = useForm;
Form.useFormInstance = useFormInstance;
Form.useWatch = useWatch;
Form.Provider = FormProvider;
Form.create = () => {
  warning(
    false,
    'Form',
    'antd v4 removed `Form.create`. Please remove or use `@ant-design/compatible` instead.'
  );
};

export default Form;

劫持onValuesChange事件,监听组件交互后的数据变化

  1. 给包装的<AntdForm />组件添加一个onValuesChange方法,这个方法处理执行正常的onValuesChange逻辑之外,还会额外注入一段脏代码用于上报数据给浏览器插件
// step1: 劫持onValuesChange事件,监听组件交互后的数据变化
// step2: 劫持setFieldValue,setFildsValue,监听js改变的数据
// step3: 获取Form的初始化数据给到 window.WATCH_FORM_DATA_EXTENSIONS
const InternalForm: React.FunctionComponent<IFormProps> = (props) => {
  const { children, form, onValuesChange, ...resetProps } = props;

  const innerOnValuesChange = (
    changedFields: FieldData[],
    values: FieldData[]
  ) => {
    // 在这里劫持onValuesChange事件
    window.WATCH_FORM_DATA_EXTENSIONS[`${props.name}_changedFields}`] =
      changedFields;
    window.WATCH_FORM_DATA_EXTENSIONS[`${props.name}`] = values;
    onValuesChange && onValuesChange(changedFields, values);
  };

  return (
    <AntdForm onValuesChange={innerOnValuesChange} form={form} {...resetProps}>
      {children as React.ReactNode}
    </AntdForm>
  );
};

劫持setFieldValue、setFieldsValue、resetFieldValue,监听js改变的数据

  1. 由于我们平时使用API的都是[form] = Form.useForm中的form提供的,所以我们需要找一下useForm,对useForm进行魔改
  2. 复制useForm,在FormInstance这个声明中,我们看到了我们想要改写的API:

封装一个Form,实现对AntD表单的监听

  1. 修改代码:对setFieldValuesetFieldsValue这两个函数进行重写,插入脏代码。对于resetFieldsValue,应该还需要在插件中保存一下Form的初始化数据,后面我们会说到
export default function useForm<Values = any>(
  form?: FormInstance<Values>
): [FormInstance<Values>] {
  const [rcForm] = useRcForm();
  const itemsRef = React.useRef<Record<string, React.ReactElement>>({});

  const wrapForm: FormInstance<Values> = React.useMemo(
    () =>
      form ?? {
        ...rcForm,
        setFieldValue: (name: NamePath, value: any) => {
          // 插入脏代码
          rcForm.setFieldValue(name, value);
        },
        setFieldsValue: (values: RecursivePartial<Values>) => {
          // 插入脏代码
          rcForm.setFieldsValue(values);
        },
        retFieldsValue: (values: RecursivePartial<Values>) => {
          // 插入脏代码
          rcForm.setFieldsValue(values);
        },
        __INTERNAL__: {
          itemRef: (name: InternalNamePath) => (node: React.ReactElement) => {
            const namePathStr = toNamePathStr(name);
            if (node) {
              itemsRef.current[namePathStr] = node;
            } else {
              delete itemsRef.current[namePathStr];
            }
          },
        },
        scrollToField: (name: NamePath, options: ScrollOptions = {}) => {
          const namePath = toArray(name);
          // eslint-disable-next-line no-underscore-dangle
          const fieldId = getFieldId(namePath, wrapForm.__INTERNAL__.name);
          const node: HTMLElement | null = fieldId
            ? document.getElementById(fieldId)
            : null;

          if (node) {
            scrollIntoView(node, {
              scrollMode: 'if-needed',
              block: 'nearest',
              ...options,
            } as any);
          }
        },
        getFieldInstance: (name: NamePath) => {
          const namePathStr = toNamePathStr(name);
          return itemsRef.current[namePathStr];
        },
      },
    [form, rcForm]
  );

  return [wrapForm];
}

获取Form表单的初始化数据

  1. 通过useEffect这个hooks,这个hooks是在页面渲染完成的时候触发的,所以我们能够通过form的API来获取表单的数据,并且把数据保存在插件中
  2. 上一步中的resetFieldsValue可以使用这里的数据
// step1: 劫持onValuesChange事件,监听组件交互后的数据变化
// step2: 劫持setFieldValue,setFildsValue,监听js改变的数据
// step3: 获取Form的初始化数据给到 window.WATCH_FORM_DATA_EXTENSIONS
const InternalForm: React.FunctionComponent<IFormProps> = (props) => {
  const { children, form, onValuesChange, ...resetProps } = props;
	// 通过useEffect这个hook 获取form.getFieldsValue的初始化数据
  useEffect(() => {
    window.WATCH_FORM_DATA_EXTENSIONS = {};
    const intialValue = form?.getFieldsValue();
    window.WATCH_FORM_DATA_EXTENSIONS[`${props.name}`] = intialValue;
  }, [form]);

  const innerOnValuesChange = (
    changedFields: FieldData[],
    values: FieldData[]
  ) => {
    window.WATCH_FORM_DATA_EXTENSIONS[`${props.name}_changedFields}`] =
      changedFields;
    window.WATCH_FORM_DATA_EXTENSIONS[`${props.name}`] = values;
    onValuesChange && onValuesChange(changedFields, values);
  };

  return (
    <AntdForm onValuesChange={innerOnValuesChange} form={form} {...resetProps}>
      {children as React.ReactNode}
    </AntdForm>
  );
};

这样做的不足之处

  1. 对于插件中的数据结构还需要再考虑一下,看看是否需要兼容<Form.Privider />这种存在多个表单的情况
  2. 需要对代码有侵入性,业务线的同学接入需要一定的成本,而且每次引用的话都需要从npm包,或者components/super-form下引入,有一定的心智负担
  3. AntD的某个版本对于Form组件有改动的话,那么这个super-form可能也需要跟着改,迭代起来有一点的成本,每次AntD进行升级的时候,都需要关注一个这个高阶组件是否还是兼容的,也有一定成本。

源代码地址

super-form

转载自:https://juejin.cn/post/7282691710072979493
评论
请登录