本文宣布于
我的博客
近来对团队内部 React 组件库(ne-rc)中的 Form 组件举行了重构,纪录一下思索的历程。
一些前置定义:
名词 | 定义 |
---|---|
表单 | Form 组件 |
子表单 | 嵌套在 Form 下面的相似 Input, Select 如许的子组件 |
起首我们看一下,我们的对 Form 组件的需求是什么。
猎取当前更改表单的状况
- 校验一切必填表单是不是填写完成
- 对外触发细致表单变化的要领
formFieldChange
暴露对外供应全部表单状况的要领
- 供应全部表单最新状况的要领 $Form.data
提交要领
- 校验表单是不是经由过程校验
- 对外触发
formSubmit
要领
接着我们从重构前和重构后,看怎样来处置惩罚这个题目。
Before
猎取当前更改表单的状况
怎样猎取更改的子表单
React 父子通讯须要经由过程 prop 通报要领,关于 Form 下面的相似与 Input 之类的子表单的变化想要关照到父级,假如不借助第三方的事宜通报要领,那末就只能经由过程由父级经由过程 props 向 Input 通报 formFieldChange
(假定就叫这个名字)要领,然后当子组件变化时去挪用 formFieldChange
来完成。
那末题目来了,什么时刻去通报这个要领呢?
不能在细致页面内里运用的时刻再去每条表单内里注册这个要领,那每一个用到表单组件的时刻就都须要给子表单举行如许的事宜绑定,如许太累了。
所以一开始,我挑选经由过程直接递归的遍历 Form 下面的 children,只需发明这个 children 是我想要的表单范例,那末就从新克隆一个带有 formFieldChange
的组件来替换掉本来的组件。
/**
* 猎取 form 下面每一个表单对象,注入属性,并网络起来
* @param children
* @returns {*}
*/
function getForms(children) {
return React.Children.map(children, (el, i) => {
if (!el) {
return null
}
switch (el.type) {
case Input:
Forms.push(el)
return React.cloneElement(
el,
{
key: i,
formFieldChange,
emptyInput
}
)
case Select:
Forms.push(el)
return React.cloneElement(
el,
{
key: i,
formFieldChange
}
)
case CheckBox:
Forms.push(el)
return React.cloneElement(
el,
{
key: i,
formFieldChange
}
)
default:
if (el.props && el.props.children instanceof Array) {
const children = getForms(el.props.children)
return React.cloneElement(
el,
{
key: i,
children
}
)
} else {
return el
}
}
})
}
如许,一切的特定子组件就都可以拿到被注册的要领。以 Input 为例,在 Input 的 onChange
要领内里去挪用从父级 props 传入的 formFieldChange
就可以关照到 Form 组件了。
网络更改表单的数据。
前一步完成后,这一步就比较简朴了,Input 在挪用 formFieldChange
的时刻把想要通报的数据作为参数传进去,在 Form 内里去对这个参数做处置惩罚,就可以拿到当前更改的表单状况数据了。
校验表单是不是填写完成
前面我们网络了每一条更改表单的数据。然则要推断当前 Form 下面的表单是不是填写完成,那末起首须要晓得我们有若干个须要填写的表单,然后在 formFieldChange
的时刻举行推断就可以了。怎样来提早晓得我们有若干须要填写的 Field 呢,之前我挑选的是经由过程在运用 Form 的时刻先初始化一个包括一切表单初始化状况的数据。
export default class Form extends React.Component {
constructor(props) {
super(props)
this.Forms = []
this.formState = Object.assign({}, {
isComplete: false,
isValidate: false,
errorMsg: '',
data: {}
}, this.props.formState)
}
static propTypes = {
onChange: PropTypes.func,
onSubmit: PropTypes.func,
formState: PropTypes.object
}
// 初始化一个相似如许的对象通报给 Form
formState: {
data: {
realName: {},
cityId: {},
email: {},
relativeName: {},
relativePhone: {},
companyName: {}
}
},
如许就很粗犷的处置惩罚了这个题目,然则这中心存在许多题目。
因为限制了特定的组件范例(Input,Select,CheckBox),致使不利于扩大,假如在开辟历程碰到其他范例的比方自定义的子表单,那末 Form 就没法对这个自定义子表单举行数据网络,处置惩罚起来比较贫苦。
所以就在斟酌另一个种完成体式格局, Form 只去网络一个特定前提下的组件,只需这个组件满足了这个前提,并完成了对应的接口,那末 Form 就都可以去网络处置惩罚。如许也就大大挺高了适用性。
暴露对外供应全部表单状况的要领
经由过程在外监听每次 Form 触发的 onChange
事宜来猎取全部 Form 的状况。
提交要领
磨练表单是不是经由过程校验
已有了全部 Form 的数据对象,做校验并不是什么难题。经由过程校验的时刻挪用 formSubmit
要领,没有经由过程校验的时刻对外把错误信息增加到 Form 的 state 上去。
对外触发 formSubmit 要领
当表单经由过程校验的时刻,对外触发 formSubmit
要领,把要提交的数据作为 formSubmit
的参数通报给表面。
After
前面是之前写的 Form 组件的一些思绪,在实际运用中也基础能满足营业需求。
然则全部 Form 的可拓展性比较差,没法很好的接入其他自定义的组件。所以萌生了重写的主意。
关于重写的这个 Form,我的主意是:起首一定要轻易运用,不须要一大堆的肇端事情;其次就是可拓展性要强,除了本身已供应的内涵 Input,Select 等可以接入 Form 外,关于其他的营业中的特别需求须要接入 Form 的时刻,只需这个组件完成了特定的接口就可以了很轻易的接入,而不须要大批的去修正组件内部的代码。
重构重要集合在上面需求 1 内里的内容,也就是:__猎取当前更改表单的状况__
猎取当前表单的状况剖析下来有一下几点:
- 猎取一切须要网络的子表单
formFields
- 初始化 Form
state
- 表单下体面表单数目或范例发生变化时更新 1 内里建立的
formFields
- 子表单内部状况发生变化时关照到父表单
猎取当前更改表单的状况
猎取一切须要的子表单
一样经由过程递归遍历 children 来猎取须要网络的子表单,经由过程子表单的 type.name 定名划定规矩是不是相符我们的定义来决议是不是要举行网络。
直接来看代码:
collectFormField = (children) => {
const handleFieldChange = this.handleFieldChange
// 简朴粗犷,在 Form 更新的时刻直接清空上一次保留的 formFields,全量更新,
// 防止 formFields 内容或许数目发生变化时 this.formFields 数据不正确的题目
const FormFields = this.formFields = []
function getChildList(children) {
return React.Children.map(children, (el, i) => {
// 只需 Name 以 _Field 开首,就认为是须要 From 治理的组件
if (!el || el === null) return null
const reg = /^_Field/
const childName = el.type && el.type.name
if (reg.test(childName)) {
FormFields.push(el)
return React.cloneElement(el, {
key: i,
handleFieldChange
})
} else {
if (el.props && el.props.children) {
const children = getChildList(el.props.children)
return React.cloneElement(el, {
key: i,
children
})
} else {
return el
}
}
})
}
只需组件的 class name 以 _Field 开首,就把它网络起来,并传入 handleFieldChange
要领,如许当一个自定义组件接入的时刻,只须要在表面包一层,并把 class 的定名为以 _Field 开首的花样就可以被 Form 网络治理了。
接入组件内里须要做的就是,在适宜的机遇挪用 handleFieldChange
要领,并把要通报的数据作为参数通报出来就可以了。
为何一定要死不悔改的运用遍历这类低效的体式格局去网络呢,实在都是为了组件上运用的轻易。如许就不须要每次在援用的时刻在对子表单做什么操纵了。
初始化 Form state
上一步拿到了一切的子表单,然后经由过程挪用 initialFormDataStructure
拿来初始化 Form 的 state.data
的构造,同时关照到表面 Form 发生了变化。
子表单数目或范例发生变化时
当 Form 下体面组件被增加或删除时,须要实时更新 Form Data 的构造。经由过程挪用 updateFormDataStructure
把新增的或许修正的子表单更新到最新,并关照到表面 Form 发生了变化。
子表单内部状况发生变化时
在第一步网络子表单的时刻就已把 handleFieldChange
注入到了子表单组件内里,所以子表单来决议挪用的机遇。当 handleFieldChange
被挪用的时刻,起首对 Form state
举行更新,然后外关照子表单发生了变化,同时关照表面 Form 发生了变化。
如许看起来全部流程就走通了,但实际上存在许多题目。
起首因为 setState
是一个异步的历程,只要在 render
后才猎取到最新的 state
. 这就致使,在一个生命周期轮回内假如我屡次挪用了 setState
,那末两次挪用之间对 state
的读取许多是不正确的。(有关生命周期的细致内容可以看这篇文章:https://www.w3ctech.com/topic…)
所以我建立了一个暂时变量 currentState
来寄存当前状况下最新的 state
,每次 setState
的时刻都对其举行更新。
另一个题目是当 Form 发生变化的时刻,updateFormDataStructure
挪用的过于频仍。实在只要在子表单的数目或许范例发生变化时才须要更新 Form state 的构造。而直接去对照子表单的范例是不是发生变化也是看法开支很大操纵,所以挑选另一种折衷体式格局。经由过程给 Form 当前的状况打标,将 Form 能够处于的状况都标识出来:
const STATUS = {
Init: 'Init',
Normal: 'Normal',
FieldChange: 'FieldChange',
UpdateFormDataStructure: 'UpdateFormDataStructure',
Submit: 'Submit'
}
如许,只要在 Form 的 STATUS
处于 Normal
的时刻才对其举行 updateFormDataStructure
操纵。如许就可以省去许屡次衬着以及无效的对外触发的 FormChange
事宜。
提交和对外暴露 Form 状况的要领和之前基础一致,如许全部对 Form 的重构就算完成了,细致项目中运用体验还不错 O(∩_∩)O
Form 组件地点: https://github.com/NE-LOAN-FED/NE-Component/tree/master/src/Form
末了,假如看文章的你有什么更好的主意,请告诉我?。