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}
      />
    );
  }
}

为了收集输入框的值,我们需要给表单组件 TextInputvalue 绑定到一个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是为空值的错误提示语,requiredhelp搭配,在submit方法调用时,会对该字段做简单的非空检验,如果字段为空值则会提示。

围绕这些特性,笔者细化了一下目标,作为一套表单组件,应该至少有这些功能:

  • 表单组件: 涵盖常用的表单如文本输入框等

  • 数据管理: 赋初始值,保存变化值

  • 表单检验: 简单非空检验,自定义规则检验

首先,实现基本的表单组件,这个是页面布局与功能的基础。依照需求,需要实现文本框,单选框,复选框,选择框等组件,并且定制UI,这一点无需赘述。在这一个过程,我们只是实现UI与单个组件操作逻辑,还没有统一管理数据逻辑。

其次,来看看数据管理,我们需要将数据统一交给Form组件管理,保持输入框值改变了之后能同步保存。这里就需要跨组件进行通信。利用context,可以无需声明props来进行父子组件的通讯,符合要求。

而对于表单检验,也是统一在Form组件里面处理,不过是否需要检验则需要看像FormTextInput这样的组件是否传了相关的条件如required

本文不谈UI层面上的基本表单组件的实现,主要谈谈表单组件数据相关的逻辑

架构

笔者本次实现的表单组件主要是三部分:Form组件,createInput高阶组件(HOC),createInput包装过的表单组件。

架构概览图如下:

form

基本能力

如下是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>
     );
   }

各个表单组件只需要 setValuevalue来保持数据同步。

总结

通过这些实践,笔者编写的业务代码十分清爽,达到了想要的效果。虽然实现的功能还有一些不全,但是当下还能满足业务需求。

在整个开发的过程中,笔者花的时间最多的不是在这些表单的数据逻辑抽象复用,而是在实现基本的定制化的表单组件上。这也说明了有些优化性的尝试不一定会花掉很多时间,为了提升代码体验和自身的代码能力,花的这些时间也是很有意义的。

题外

笔者在本次实践中使用了两个套路:

  • context组件通信

本次利用了context来进行组件通信,笔者也看到有开源方案用redux来进行数据存储与事件处理

  • HOC高阶组件

这是React推荐的组件复用方法,我们能在很多开源代码如react-redux源码里面看到

标签: none

仅有一条评论

  1. liumin liumin

    博主有demo吗? 刚从原生转过来,很多不太熟悉

添加新评论