看这篇文章以前,你须要掌握的知识:javascript
form 能够说是 web 开发中的最大的难题之一。跟普通的组件相比,form 具备如下几个特色:html
异步
获取数据的,那么连续两次用户输入拿到的数据也有可能存在 "后发先至" 的问题。正由于以上这些特色,使 form 的开发变得困难重重。在接下来的章节中,咱们会将 RxJS 和 Form 结合起来,帮助咱们更好的去解决这些问题。java
在实现咱们本身的 Form 组件以前,让咱们先来参考一下原生的 HTML Form。react
对于一个 Form 组件来讲,须要保存全部表单元素的信息(如 value, validity 等),HTML Form 也不例外。
那么,HTML Form 将表单状态保存在什么地方?如何才能获取表单元素信息?git
主要有如下几种方法:github
<form>
表单节点。document.forms[0].elements[0].value; // 获取第一个 form 中第一个表单元素的值 const form = document.querySelector("form"); form.elements[0].value; form.addEventListener('submit', function(event) { console.log(event.target.elements[0].value); });
表单校验的类型通常分为两种:web
novalidate
属性能够关闭浏览器的自动校验。<form novalidate> <input name='username' required/> <input name='password' type='password' required minlength="6" maxlength="6"/> <input name='email' type='email'/> <input type='submit' value='submit'/> </form>
var $form = document.querySelector('form'); function getFormValues(form) { var values = {}; var elements = form.elements; // elemtns is an array-like object for (var i = 0; i < elements.length; i++) { var input = elements[i]; if (input.name) { switch (input.type.toLowerCase()) { case 'checkbox': if (input.checked) { values[input.name] = input.checked; } break; case 'select-multiple': values[input.name] = values[input.name] || []; for (var j = 0; j < input.length; j++) { if (input[j].selected) { values[input.name].push(input[j].value); } } break; default: values[input.name] = input.value; break; } } } return values; } $form.addEventListener('submit', function(event) { event.preventDefault(); getFormValues(event.target); console.log(event.target.elements); console.log(getFormValues(event.target)); });
感兴趣的同窗能够先去看一下源码 https://github.com/reeli/reac...typescript
RxJS 是一个很是强大的数据管理工具,但它并不具有用户界面渲染的功能,而 React 却特别擅长处理界面。那何不将它们的长处结合起来?用 React 和 RxJS 来解决咱们的 Form 难题。既然知道了它们各自的长处,因此分工也就比较明确了: 浏览器
RxJS 负责管理状态,React 负责渲染界面。网络
与 Redux Form 不一样的是,咱们不会将 form 的状态存储在 store 中,而是直接保存在 <Form/>
组件中。而后利用 RxJS 将数据通知给每个 <Field/>
,而后 <Field/>
组件会根据数据去决定本身是否须要更新 UI,须要更新则调用 setState
,不然什么也不作。
举个例子,假设在一个 Form 中有三个 Field (以下),当只有 FieldA 的 value 发生变化时, 为了避免让 <Form/> 和
其子组件也 re-render,Redux Form 内部须要经过 shouldComponentUpdate()
去限制。
// 伪代码 <Form> <FieldA/> <FieldB/> <FieldC/> </Form>
而 RxJS 能把组件更新的粒度控制到最小,换句话说,就是让真正须要 re-render 的 <Field/>
re-render,而不须要 re-render 的组件不从新渲染 。
从上面的设计思路能够总结出如下两个问题:
第一个问题,须要的是一个 Observable 的功能,并且是可以支持多播的 Observable。第二个问题须要的是一个 Observer 的功能。在 RxJS 中,既是 Observable 又是 Observer,并且还能实现多播的,不就是 Subject 么!所以,在实现 Form 时,会大量用到 Subject。
Form 组件中也须要一个 State,用来保存全部 Field 的状态,这个 State 就是 formState。
那么 formState 的结构应该如何定义呢?
在最先的版本中,formState
的结构是长下面这个样子的:
interface IFormState { [fieldName: string]: { dirty?: boolean; touched?: boolean; visited?: boolean; error?: TError; value: string; }; }
formState 是一个对象,它以 fieldName
为 key,以一个 保存了 Field 状态的对象做为它的 value。
看起来没毛病对吧?
可是。。。。。
最后 formState 的结构却变成了下面这样:
interface IFormState { fields: { [fieldName: string]: { dirty?: boolean; touched?: boolean; visited?: boolean; error?: string | undefined; }; }; values: { [fieldName: string]: any; }; }
Note: fields 中不包含 filed value,只有 field 的一些状态信息。values 中只有 field values。
为何呢???
其实在实现最基本的 Form 和 Field 组件时,以上两种数据结构均可行。
那问题到底出在哪儿?
这里先买个关子,目前你只须要知道 formState 的数据结构长什么样就能够了。
为了更好的理解数据流,让咱们来看一个简单的例子。咱们有一个 Form 组件,它的内部包含了一个 Field 组件,在 Field 组件内部又包含了一个 Text Input。数据流多是像下面这样的:
首先,咱们须要建立两个基本组件,一个 Field 组件,一个 Form 组件。
Field 组件是链接 Form 组件和表单元素的中间层。它的做用是让 Input 组件的职责更单一。有了它以后,Input 只须要作显示就能够了,不须要再关心其余复杂逻辑(validate/normalize等)。何况,对于 Input 组件来讲,不只能够用在 Form 组件中,也能够用在 Form 组件以外的地方(有些地方可能并不须要 validate 等逻辑),因此 Field 这一层的抽象仍是很是重要的。
利用 RxJS 的特性来控制 Field 组件的更新,减小没必要要的 rerender。与 Form 进行通讯。 当 Field 状态发生变化时,须要通知 Form。在 Form 中改变了某个 Field 的状态,也须要通知给 Field。
通知 Field 每一次 Form State 的变化。 在 Form 中会建立一个 formSubject$,每一次 Form State 的变化都会向 formSubject$ 上发送一个数据,每个 Field 都会注册成为 formSubject$ 的观察者。也就是说 Field 知道 Form State 的每一次变化,所以能够决定在适当的时候进行更新。
当 FormAction 发生变化时,通知给 Field。 好比 startSubmit 的时候。
Context 主要用于跨级组件通讯。在实际开发中,Form 和 Field 之间可能会跨级,所以咱们须要用 Context 来保证 Form 和 Field 的通讯。Form 经过 context 将其 instance 方法和 formState 提供给 Field。
Form 组件会向 Field 组件提供一个 d__ispatch__ 方法,用于 Field 和 Form 进行通讯。全部 Field 的状态和值都由 Form 统一管理。若是指望更新某个 Field 的状态或值,必须 dispatch 相应的 action。
表单元素和 Field 通讯主要是经过回调函数。Field 会向表单元素提供 onChange,onBlur 等回调函数。
对于接口的设计来讲,简单清晰是很重要的。因此 Field 只保留了必要的属性,没有将表单元素须要的其余属性经过 Field 透传下去,而是交给表单元素本身去定义。
经过 Child Render,将对应的状态和方法提供给子组件,结构和层级更加清晰了。
Field:
type TValidator = (value: string | boolean) => string | undefined; interface IFieldProps { children: (props: IFieldInnerProps)=> React.ReactNode; name: string; defaultValue?: any; validate?: TValidator | TValidator[]; }
Form:
interface IRxFormProps { children: (props: IRxFormInnerProps) => React.ReactNode; initialValues?: { [fieldName: string]: any; } }
到这里,一个最最基本的 Form 就完成了。接下来咱们会在它的基础上进行一些扩展,以知足更多复杂的业务场景。
FieldArray 主要用于渲染多组 Fields。
回到咱们以前的那个问题,为何要把 formState 的结构分为 fileds 和 values?
其实问题就出在 FieldArray,
经过 RxJS,咱们将 Field 更新的粒度控制到了最小,也就是说若是一个 Field 的 Value 发生变化,不会致使 Form 组件和其余 Feild 组件 rerender。
既然 Field 只能感知本身的 value 变化,那么问题就来了,如何实现 Field 之间的联动?
因而 FormValues 组件就应运而生了。
每当 formValues 发生变化,FormValues 组件会就把新的 formValues 通知给子组件。也就是说若是你使用了 FormValues 组件,那么每一次 formValues 的变化都会致使 FormValues 组件以及它的子组件 rerender,所以不建议大范围使用,不然可能带来性能问题。
总之,在使用 FormValues 的时候,最好把它放到一个影响范围最小的地方。也就是说,当 formValues 发生变化时,让尽量少的组件 rerender。
在下面的代码中,FieldB 的显示与否须要根据 FieldA 的 value 来判断,那么你只须要将 FormValues 做用于 FIeldA 和 FieldB 就能够了。
<FormValues> {({ formValues, updateFormValues }) => ( <> <FieldA name="A" /> {!!formValues.A && <FieldB name="B" />} </> )} </FormValues>
FormSection 主要是用于将一组 Fields group 起来,以便在复用在多个 form 中复用。主要是经过给 name
添加前缀来实现的。
那么怎样给 Field 和 FieldArray 的 name 添加前缀呢?
我首先想到的是经过 React.Children 拿到子组件的 name,再和 FormSection 的 name 拼接起来。
可是,FormSection 和 Field 有可能不是父子关系!由于 Field 组件还能够被抽成一个独立的组件。所以,存在跨级组件通讯的问题。
没错!跨级组件通讯咱们仍是会用到 context。不过这里咱们须要先从 FormConsumer 中拿到对应的 context value,再经过 Provider 将 prefix 提供给 Consumer。这时 Field/FieldArray 经过 Consumer 拿到的就是 FormSection 中的 Provider 提供的值,而再也不是由 Form 组件的 Provider 所提供。由于 Consumer 会消费离本身最近的那个 Provider 提供的值。
<FormConsumer> {(formContextValue) => { return ( <FormProvider value={{ ...formContextValue, fieldPrefix: `${formContextValue.fieldPrefix || ""}${name}.`, }} > {children} </FormProvider> ); }} </FormConsumer>
主要用于工具类方法。
主要用于 Field,FieldArray 等组件。由于它们不能脱离 Form 独立存在,因此没法对其使用单元测试。
Note: 在测试中,没法直接修改 instance 上的某一个属性,觉得 React 将 props 上面的节点都设置成了 readonly (经过 Object.defineProperty 方法)。 可是能够经过总体设置 props 绕过。
instance.props = { ...instance.props, subscribeFormAction: mockSubscribeFormAction, dispatch: mockDispatch, };
若是项目中的表单过多,那么对于 QA 测试来讲无疑是一个负担。这个时候咱们但愿可以有一个自动填表单的工具,来帮助咱们提升测试的效率。
在写这个工具的时候,咱们须要模拟 Input 事件。
input.value = 'v'; const event = new Event('input', {bubbles: true}); input.dispatchEvent(event);
咱们的指望是,经过上面的代码去模拟 DOM 的 input 事件,而后触发 React 的 onChange 事件。可是 React 的 onChange 事件却没有被触发。所以没法给 input 元素设置 value。
由于 ReactDOM 在模拟 onChange 事件的时候有一个逻辑:只有当 input 的 value 改变,ReactDOM 才会产生 onChange 事件。
React 16+ 会覆写 input value setter,具体能够参考 ReactDOM 的 inputValueTracking。所以咱们只须要拿到原始的 value setter,call 调用就好了。
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; nativeInputValueSetter.call(input, "v"); const event = new Event("input", { bubbles: true}); input.dispatchEvent(event);
在 Dev 环境中,能够经过 Log 来进行 Debug。目前在 Dev 环境下会自动打印 Log,其余环境则不会打印 Log。
Log 的信息主要包括: prevState, action, nextState。
Note: 因为 prevState, action, nextState 都是 Object,因此别忘了在打印的时候调用 cloneDeep,不然没法保证最后打印出来的值的正确性,也就是说最后获得的结果可能不是打印的那一时刻的值。
这篇文章只讲了关于 React Rx Form 的思路以及一些核心技术,你们也能够按照这个思路本身去实现一版。固然,也能够参考一下源码,欢迎来提建议和 issue。Github 地址: https://github.com/reeli/reac...