本文发布于 个人博客
最近对团队内部 React 组件库(ne-rc)中的 Form 组件进行了重构,记录一下思考的过程。react
一些前置定义:git
名词 | 定义 |
---|---|
表单 | Form 组件 |
子表单 | 嵌套在 Form 下面的相似 Input, Select 这样的子组件 |
首先咱们看一下,咱们的对 Form 组件的需求是什么。github
获取当前变更表单的状态异步
formFieldChange
暴露对外提供整个表单状态的方法this
提交方法code
formSubmit
方法接着咱们从重构前和重构后,看如何来解决这个问题。orm
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
的参数传递给外面。
前面是以前写的 Form 组件的一些思路,在实际使用中也基本能知足业务需求。
可是整个 Form 的可拓展性比较差,没法很好的接入其余自定义的组件。因此萌生了重写的想法。
对于重写的这个 Form,个人想法是:首先必定要方便使用,不须要一大堆的起始工做;其次就是可拓展性要强,除了本身已经提供的内在 Input,Select 等可以接入 Form 外,对于其余的业务中的特殊需求须要接入 Form 的时候,只要这个组件实现了特定的接口就能够了很方便的接入,而不须要大量的去修改组件内部的代码。
重构主要集中在上面需求 1 里面的内容,也就是:__获取当前变更表单的状态__
获取当前表单的状态分解下来有一下几点:
formFields
state
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
方法,并把要传递的数据做为参数传递出来就能够了。
为何必定要执迷不悟的使用遍历这种低效的方式去收集呢,其实都是为了组件上使用的方便。这样就不须要每次在引用的时候在对子表单作什么操做了。
上一步拿到了全部的子表单,而后经过调用 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
最后,若是看文章的你有什么更好的想法,请告诉我?。