tiodot / tiodot.github.io

总结归纳--做的东西不是每一件都留到现在 但是能通过实践,收获了如何构建一套系统,一套工具的方法论
https://xchb.work
8 stars 0 forks source link

React表单验证之三:解析React组件验证规则 #18

Open tiodot opened 7 years ago

tiodot commented 7 years ago

在 #17 仿造async-validator实现了一个简易版的验证器。为了实现 #16 中提及的jQuery Form Validator类似的验证方式,一个验证器似乎是杯水车薪,路漫漫该如何走呢?

寻常路

提供工具函数

在antd提供的form表单组件,其将组件和验证规则关联需要通过一个getFieldDecorator来完成,其形式是:

import { Form, Icon, Input, Button, Checkbox } from 'antd';
const FormItem = Form.Item;

class NormalLoginForm extends React.Component {
  handleSubmit = (e) => {
    e.preventDefault();
    this.props.form.validateFields((err, values) => {
      if (!err) {
        console.log('Received values of form: ', values);
      }
    });
  }
  render() {
    const { getFieldDecorator } = this.props.form;
    return (
      <Form onSubmit={this.handleSubmit} className="login-form">
        <FormItem>
          {getFieldDecorator('userName', {
            rules: [{ required: true, message: 'Please input your username!' }],
          })(
            <Input prefix={<Icon type="user" style={{ fontSize: 13 }} />} placeholder="Username" />
          )}
        </FormItem>
        <FormItem>
          {getFieldDecorator('password', {
            rules: [{ required: true, message: 'Please input your Password!' }],
          })(
            <Input prefix={<Icon type="lock" style={{ fontSize: 13 }} />} type="password" placeholder="Password" />
          )}
        </FormItem>
          <Button type="primary" htmlType="submit" className="login-form-button">
            Log in
          </Button>
        </FormItem>
      </Form>
    );
  }
}

const WrappedNormalLoginForm = Form.create()(NormalLoginForm);
ReactDOM.render(<WrappedNormalLoginForm />, mountNode);

为了使用getFieldDecorator,最后还得将组件用Form.create()包一层,Form.create()会自动给被包裹的组件注入一个form到组件的props中。看起来就是那么不直观。

提供带验证组件

还有一种方式就是Lesha-spr/react-validation使用的提供支持验证的组件,附其一个github上的demo:

import React, {Component, PropTypes} from 'react';
import Validation from 'react-validation';
import validator from 'validator';
Object.assign(Validation.rules, {
    required: {
        rule: value => {
            return value.toString().trim();
        },
        hint: value => {
            return <span className='form-error is-visible'>Required</span>
        }
    },
    email: {
        rule: value => {
            return validator.isEmail(value);
        },
        hint: value => {
            return <span className='form-error is-visible'>{value} isnt an Email.</span>
        }
    }
});

export default class Registration extends Component {
    render() {
        return <Validation.components.Form>
            <h3>Registration</h3>
            <div>
                <label>
                    Email*
                    <Validation.components.Input value='email@email.com' name='email' validations={['required', 'email']}/>
                </label>
            </div>
            <div>
                <Validation.components.Button>Submit</Validation.components.Button>
            </div>
        </Validation.components.Form>;
    }
}

虽然看起来和jquery的form validator很像了,但一个不足就是input,select之类的都需要使用其提供的组件,不方便扩展,或者说限制较多。

进阶之路

在ES5时代的React推出了Mixin机制来提高代码复用率,然而ES6时代被HOCs无情的取代,虽然未被官方承认,但redux,flux等用的都是这种思路,像上面提及的antd中form组件的Form.create就使用到了HOCs。HOCs(High Order Components:高阶组件)类似于高级函数。在深入理解 React 高阶组件中提到一种高级组件的用途Inheritance Inversion,简直就是为实现表单验证的最佳实践。先看其最简单的一个示例:

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      return super.render()
    }
  }
}

可以看到,返回的 HOC 类(Enhancer)继承了 WrappedComponent,而不是 WrappedComponent 继承了 Enhancer。意味着Enhancer可以访问到 WrappedComponent中的state、props、组件生命周期方法和 render 方法。能否访问render方法,意味着就能访问其对应的虚拟DOM,虚拟DOM是啥,就是真实DOM的js的object表示。蓦然回首,答案已呼之欲出。

提供一个HOC类,需要完成以下几个功能:

  1. 获取WrappedComponent组件的虚拟DOM:
    const elementsTree = super.render(); // 组件的render方法返回就是虚拟DOM
  2. 递归解析DOM树,然后查找节点是否有data-validation属性
  3. 如果节点需要校验,则将校验规则扩展到验证器中,然后包装或者新增一个onChange事件,用于收集数据并校验数据
  4. 使用React提供的cloneElement将修改的属性扩展回虚拟DOM中
  5. 给WrappedComponent组件注入一个validate方法,用于验证表单数据

完整代码:

import React, {cloneElement} from 'react';
import Schema from '../verification';

export default function formWrapper(WrappedComponent) {
    return class Enhancer extends WrappedComponent {
        constructor(props) {
            super(props);
            this.schema = new Schema();
            this.store = {};
            super.validate = this.validate.bind(this); // 给组件注入validate方法
        }
        validate(callback) {
            const store = this.store;
            this.schema.validate(store, function (error) {
                console.log('result is: ', error);
                callback && callback(error, store);
            })
        }

        onChangeWithValidation(name, oldOnChange) {
            const schema = this.schema;
            const store = this.store;
            return function (e) {
                let error;
                const value = e.target.value;
                store[name] = value;
                console.log('wrapper change callback');
                schema.validate({[name]: value}, {keys: [name]}, function (err) {
                    console.log('validate result: ', err);
                    error = err;
                });
                oldOnChange && oldOnChange.call(oldOnChange, e, error, name);
            }
        }

        parseRule(elementsTree) { // 递归解析节点,根据是否有data-validation来判断是否需要校验
            const parseRule = this.parseRule.bind(this);
            if (!elementsTree.props) { // 文本节点
                return elementsTree;
            }
            let children = React.Children.map(elementsTree.props.children, function (child) {
                return parseRule(child);
            });
            let rules = elementsTree.props['data-validation'];
            let {name, onChange} = elementsTree.props;
            let schema = this.schema;
            let newProps = {};
            if (name && rules) {
                schema.extend({
                    [name]: rules
                });
                newProps.onChange = this.onChangeWithValidation(name, onChange);
            }
            const props = Object.assign({}, elementsTree.props, newProps);
            return cloneElement(elementsTree, props, children);
        }

        render() {
            const elementsTree = super.render();
            return this.parseRule(elementsTree);
        }
    }
}

HOC已完成,那如何使用?

import React, {Component} from 'react';
import './simple-form.less';
import FormWrapper from '../form/wrapper';

class SimpleForm extends Component {
    constructor(props) {
        super(props);
        this.state = {
            error: ''
        }
    }

    onChange(e, error, name) {
        console.log(e.target.value);
        console.log('error: ', JSON.stringify(error));
        this.setState({error: error ? error[0].message : ''});
    }

    render() {
        return (
            <form>
                <input name="name"
                       onChange={this.onChange.bind(this)}
                       type="text"
                       data-validation={[{required: true}]}/>
                <span style={{color: '#f00'}}>{this.state.error}</span>
            </form>
        );
    }
}
export default FormWrapper(SimpleForm); // 使用HOC类装饰

代码以及示例地址:react-validator 最后看看效果: simple-form

参考

深入理解 React 高阶组件