React-Native 里怎么写表单业务代码
笔者最近处理了一些大表单的业务需求,本次结合自己的一些实践经验,来谈谈对表单业务代码的一些感悟
引子
我们先看看 React-Native
里面怎么写一个文本输入框:
export default class UselessTextInput extends Component {
constructor(props) {
super(props);
this.state = { text: 'Useless Placeholder' };
}
render() {
return (
<TextInput
onChangeText={(text) => this.setState({text})}
value={this.state.text}
/>
);
}
}
为了收集输入框的值,我们需要给表单组件 TextInput
的 value
绑定到一个state
,并添加 onChangeText
事件来更改对应的 state
,这样TextInput
输入的值,就能被收集到state
里面。
在输入框比较多的情况,我们需要一个一个给输入框加上这样的逻辑。在某些情况下,我们可以用一个共用的事件处理方法来处理。如:
handleChange = (type, value) => {
this.setState({
[type]: value
})
}
render() {
const { name, age } = this.state
return (
<View>
<TextInput
onChangeText={(text) => this.setState({name: text})}
value={name}
/>
<TextInput
onChangeText={(text) => this.setState({age: text})}
value={age}
/>
</View>
)
}
这样处理没什么很大的问题。
但是,如果我们的业务需求有很多大表单,需要文本输入框,单选框,复选框,弹出选择框等各种类型的表单组件,我们还需要表单数据收集,常规检验,与错误提示等等。在这些复杂的场景下,我们只会在上面再堆一大堆逻辑,这个时候可以想一下是不是可以做点优化什么的?
笔者在工作中正好遇到了这样的复杂场景,本着组件复用,逻辑复用的原则,笔者觉得应该需要封装一下这些表单组件与逻辑。
目标
在对表单组件做封装之前,我们先来确认一下希望达成的目标:
定制化风格表单组件
基本表单组件,自定义表单组件的封装,能实现页面布局,组件复用
抽象表单数据收集
onChangeText
等处理组件输入数据的同步逻辑可以抽象出来,使其通用抽象表单检验与错误处理
能够提供通用常规检验处理与自定义检验方法
目标确认之后,笔者在使用第三方库与自己动手实现,这两个方面进行了思考,最后决定自己实现。理由如下:
- 即使用了第三方的表单组件,但是
UI
还是需要定制 - 第三方库没有非常规的表单组件,但是这种组件也需要将数据收集到表单
- 核心的表单数据收集逻辑,并不复杂
- 自己动手,可轻可重,细节可控,便于维护
实现
在编码之前,笔者参考了一些抽象化的表单的使用场景,最终希望实现的代码应该是长成下面的样子:
submit(){
const values = this.form.getValues();
if (values) {
// 提交表单数据
}
}
render() {
return (
<Form ref={(ref) => {
this.form = ref;
}}>
<FormTextInput
required
help={"请输入姓名!"}
name={"name"}
placeholder={"请输入姓名"}
defaultValue={""}
/>
<FormTextInput
required
help={"请输入年龄!"}
name={"age"}
placeholder={"请输入年龄"}
defaultValue={""}
/>
</Form>
)
}
组件 Form
是表单内数据统一收集检验的载体。
FormTextInput
是对TextInput
做了封装的表单组件,需要放到Form
里面使用,在UI
上符合我们的定制化需求。
对FormTextInput
接受到的props
,传入的name
作为字段名,defaultValue
是初始值,help
是为空值的错误提示语,required
和 help
搭配,在submit
方法调用时,会对该字段做简单的非空检验,如果字段为空值则会提示。
围绕这些特性,笔者细化了一下目标,作为一套表单组件,应该至少有这些功能:
表单组件: 涵盖常用的表单如文本输入框等
数据管理: 赋初始值,保存变化值
表单检验: 简单非空检验,自定义规则检验
首先,实现基本的表单组件,这个是页面布局与功能的基础。依照需求,需要实现文本框,单选框,复选框,选择框等组件,并且定制UI
,这一点无需赘述。在这一个过程,我们只是实现UI
与单个组件操作逻辑,还没有统一管理数据逻辑。
其次,来看看数据管理,我们需要将数据统一交给Form
组件管理,保持输入框值改变了之后能同步保存。这里就需要跨组件进行通信。利用context
,可以无需声明props
来进行父子组件的通讯,符合要求。
而对于表单检验,也是统一在Form
组件里面处理,不过是否需要检验则需要看像FormTextInput
这样的组件是否传了相关的条件如required
。
本文不谈UI
层面上的基本表单组件的实现,主要谈谈表单组件数据相关的逻辑
架构
笔者本次实现的表单组件主要是三部分:Form
组件,createInput
高阶组件(HOC
),createInput
包装过的表单组件。
架构概览图如下:
基本能力
如下是Form
组件的实现:
// @flow
// 表单组件抽象,利用 context 与表单内输入框做数据绑定
// 统一管理输入框值,基础检验,自定义检验等
import React, { PureComponent } from "react";
import { View } from "react-native";
import propTypes from "prop-types";
import Toast from "components/toast";
import type { formValueType, formValidationType } from "./flowTypes";
export default class Form extends PureComponent<{
children: any
}> {
static childContextTypes = {
inForm: propTypes.bool,
setValue: propTypes.func,
getValue: propTypes.func,
setValidation: propTypes.func,
clear: propTypes.func
};
_values: formValueType = {};
_validation: formValidationType = {};
getChildContext() {
return {
inForm: true,
setValue: this.setValue,
getValue: this.getValue,
setValidation: this.setValidation,
clear: this.clear
};
}
getValues(isValidate: boolean = true) {
// 获取数据
if (isValidate) {
return this.validate() ? this._values : null;
}
return this._values;
}
validate() {
// 表单检验
return Object.keys(this._values).every(name => {
const validation = this._validation[name];
const value = this._values[name];
if (typeof validation === "function") {
return validation(value);
}
if (validation && !value) {
Toast.show(validation);
return false;
}
return true;
});
}
removeValue(name: string) {
delete this._values[name];
}
removeValidation(name: string) {
delete this._validation[name];
}
clear = (name: string) => {
this.removeValidation(name);
this.removeValue(name);
};
setValidation = (name: string, help: any) => {
this._validation[name] = help;
};
getValue = (name: string) => this._values[name];
setValue = (name: string, value: any) => {
this._values[name] = value;
};
render() {
return <View>{this.props.children}</View>;
}
}
getChildContext
为子组件提供了一些属性和方法,其中,setValue
getValue
方法提供了对字段值的读写操作,使得表单组件能够同步数据到Form
。
getChildContext() {
return {
inForm: true,
setValue: this.setValue,
getValue: this.getValue,
setValidation: this.setValidation,
clear: this.clear
};
}
高级能力
对于Form
里面的表单组件,都需要使用 setValue
getValue
,这是每个表单组件的必要逻辑,我们可以抽象这些逻辑。
React
强调组合优于继承的理念,官方推荐的做法是用HOC
高阶组件的方式来复用小组件来构建大组件。使用高阶组件,可以将任何组件进行数据绑定到表单,既能满足常用组件,也能满足自定义组件。
// @flow
// 将输入框绑定到 Form 表单进行管理,支持自定义组件拓展
import React, { PureComponent } from "react";
import propTypes from "prop-types";
export default function createInput(WrappedComponent: any): any {
return class BaseFormInput extends PureComponent<any> {
static contextTypes = {
inForm: propTypes.bool,
inCollapse: propTypes.bool,
setValue: propTypes.func,
getValue: propTypes.func,
setValidation: propTypes.func,
clear: propTypes.func
};
state = {
value: this.getInitValue()
};
constructor(props: any, context: any) {
super(props, context);
if (this.context.inForm !== undefined) {
this.init();
}
}
init() {
const { name, validate, required, help } = this.props;
const { value } = this.state;
if (name) {
if (validate) {
this.context.setValidation(name, validate);
} else if (required && help) {
this.context.setValidation(name, help);
}
this.context.setValue(name, value);
}
}
getInitValue() {
const { name, defaultValue } = this.props;
return defaultValue || (name ? this.context.getValue(name) || "" : "");
}
setValue = (value: any) => {
this.setState({ value });
const { name } = this.props;
if (this.context.inForm && name) {
this.context.setValue(name, value);
}
};
render() {
return (
<WrappedComponent
{...this.props}
setValue={this.setValue}
value={this.state.value}
/>
);
}
};
}
利用createInput
函数,可以创造各种表单组件,如下是一个文本输入表单组件示例:
export default createInput(
class ItemTextInput extends PureComponent<any> {
onChangeText = (value: string) => {
const { onChange, setValue } = this.props;
setValue(value);
onChange && onChange(value);
};
render() {
return (
<View style={styles.rawInput}>
<TextInput
placeholderTextColor={"#999999"}
onChangeText={this.onChangeText}
{...otherProps}
/>
</View>
);
}
各个表单组件只需要 setValue
与value
来保持数据同步。
总结
通过这些实践,笔者编写的业务代码十分清爽,达到了想要的效果。虽然实现的功能还有一些不全,但是当下还能满足业务需求。
在整个开发的过程中,笔者花的时间最多的不是在这些表单的数据逻辑抽象复用,而是在实现基本的定制化的表单组件上。这也说明了有些优化性的尝试不一定会花掉很多时间,为了提升代码体验和自身的代码能力,花的这些时间也是很有意义的。
题外
笔者在本次实践中使用了两个套路:
context
组件通信
本次利用了context
来进行组件通信,笔者也看到有开源方案用redux
来进行数据存储与事件处理
HOC
高阶组件
这是React
推荐的组件复用方法,我们能在很多开源代码如react-redux
源码里面看到