封装一个Form,实现对AntD表单的监听
- Form表单的初始化数据拿不到,需要再处理一下(还需要再插入代码)
- onValuesChange只能监听到用户行为触发的变化,对于setFiledsValues、setFeildValue、resetFieldsValue等API修改的表单数据是监听不到的(还需要处理一下)
- 最最最主要的还是:使用Babel插件在用户无感知的情况下对代码进行大量的转义,很可能造成编译错误、内容丢失、甚至导致线上问题,虽然写了很多单测,但是总和考量下来,这样做风险有点大。
于是,在Babel插件接近尾声的时候,我脑海中忽然闪现出一个想法,既然本质上都是对Form组件的一些API进行魔改,那为什么不干脆一点,直接在代码里面就把Form表单进行改写,一来这样难度更小,另外一方面,业务在使用的时候,也能比较清晰的感知到,这个Form是被封装过的,有魔法
的Form。开始进入正题
我们需要做的几件事情
- 从源码中拷贝一份当前版本的Form组件
- 对源码中的Form进行包装
- 劫持onValuesChange事件,监听组件交互后的数据变化
- 劫持setFeildValue、setFeildsValue、resetFieldValue等通过API的方式改变Form数据的API
- 获取Form的初始化数据,给到window.WATCH_FORM_DATA_EXTENSIONS(因为我们最终的目的是要做一个能监听AntD 表单数据的浏览器插件)
从源码中拷贝一份当前版本的Form组件
这应该是这几步当中最简单的一步了,找到Form 4.x的源码,复制一份到components/super-form/index.tsx
中
- 把引入文件的路径修改一下
- 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组件进行包装
- 引入ant中的Form,对 InternalForm 进行重写
- 目前看到的
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事件,监听组件交互后的数据变化
- 给包装的
<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改变的数据
- 由于我们平时使用API的都是
[form] = Form.useForm
中的form
提供的,所以我们需要找一下useForm
,对useForm
进行魔改 - 复制
useForm
,在FormInstance
这个声明中,我们看到了我们想要改写的API:
- 修改代码:对
setFieldValue
、setFieldsValue
这两个函数进行重写,插入脏代码
。对于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表单的初始化数据
- 通过useEffect这个hooks,这个hooks是在页面渲染完成的时候触发的,所以我们能够通过form的API来获取表单的数据,并且把数据保存在插件中
- 上一步中的
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>
);
};
这样做的不足之处
- 对于插件中的数据结构还需要再考虑一下,看看是否需要兼容
<Form.Privider />
这种存在多个表单的情况 - 需要对代码有侵入性,业务线的同学接入需要一定的成本,而且每次引用的话都需要从npm包,或者
components/super-form
下引入,有一定的心智负担 - AntD的某个版本对于Form组件有改动的话,那么这个
super-form
可能也需要跟着改,迭代起来有一点的成本,每次AntD进行升级的时候,都需要关注一个这个高阶组件是否还是兼容的,也有一定成本。
源代码地址
转载自:https://juejin.cn/post/7282691710072979493