前端造轮子【4】- 简易 antd-form

684 阅读4分钟

在日常的开发中,form 作为数据收集模块一定是不可避免会被使用到的,相信大家也对 form 的各种常规使用非常清楚,今天就从学习的角度分析和实现一个简单的 antd-form,帮助我们更加深入的了解其中的原理。

先来看一个没有 form 的场景:

import React, { useState } from 'react'
import { Input } from 'antd'

export default function NoFormPage() {
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')

  const setUtils = {
    username: setUsername,
    password: setPassword,
  }

  const onChange = (value, field) => {
    setUtils[field](value)
  }
  const onSubmit = () => {
    console.log('submit', username, password)
  }

  return (
    <div className="container">
      <h3 className="title">No Form</h3>
      <Input placeholder="username" onChange={(e) => onChange(e.target.value, 'username')} value={username}></Input>
      <Input placeholder="password" onChange={(e) => onChange(e.target.value, 'password')} value={password}></Input>
      <button onClick={onSubmit}>submit</button>
    </div>
  )
}

乍一看仿佛没什么问题,但实际上这里存在几个问题:

  • 每一个 Input 都需要手动配置数据,手动更改值
  • 没有表单验证

这里我们看看 antd 是如何使用的:

import { Form, Input, Button } from 'antd';

const Demo = () => {
  const onFinish = (values) => {
    console.log('Success:', values);
  };

  const onFinishFailed = (errorInfo) => {
    console.log('Failed:', errorInfo);
  };

  return (
    <Form
      name="basic"
      onFinish={onFinish}
      onFinishFailed={onFinishFailed}
    >
      <Form.Item
        label="Username"
        name="username"
        rules={[
          {
            required: true,
            message: 'Please input your username!',
          },
        ]}
      >
        **<Input />**
      </Form.Item>

      <Form.Item
        label="Password"
        name="password"
        rules={[
          {
            required: true,
            message: 'Please input your password!',
          },
        ]}
      >
        <Input.Password />
      </Form.Item>

      <Form.Item>
        <Button type="primary" htmlType="submit">
          Submit
        </Button>
      </Form.Item>
    </Form>
  );
};

从上面的代码可以看出,作为用户,我们需要关心的只有:

  • 整个表单的数据(而非每一条数据)
  • 每一条数据的验证规则

这里我们分析一下,先把大致的架子搭起来:

  • Form 关心的主要是 onFinishonFinishFailed 两个方法
  • Form.Item 则关心 namerules

这里我们把 Form.Item 提出来,于是架构如下:

// Form.js
import React from 'react'

export default function Form({ children }) {
  const onSubmit = (e) =>{
    e.preventDefault()
    /* do submit */
  }
  return <form onSubmit={(e)=>onSubmit(e)}>{children}</form>
}
// FormItem
import { cloneElement } from 'react'

export default function FormItem({ children }) {
  const getControlled = ({ name }) => {
    return {
      value: getFieldValue(name) /* get value */,
      onChange: () => {
        /* set value */
      },
    }
  }
  const returnNode = cloneElement(children, getControlled)

  return returnNode
}

接下来需要考虑的就是:

  • submit 的处理:这里肯定根据校验规则,通过则执行 onFinish 回调,否则执行 onFinishFailed 回调
  • 值的处理:因为我们关心的是整个 form 的值,显然这里可以单独抽离一个 store 类用于值得管理

数据处理

这里我们就利用自定义 hook 来完成逻辑的封装:

// useForm.js
class FormStore {
  constructor() {
    this.store = {}
    this.callbacks = {}
  }

  /**
   * get value
   * @memberof FormStore
   */
  getFieldValue() {}

  /**
   * set values
   * @memberof FormStore
   */
  setFieldsValue() {}

  /**
   * set callbacks: onFinish, onFinishFailed
   * @memberof FormStore
   */
  setCallbacks(callbacks) {
    this.callbacks = {
      ...this.callbacks,
      ...callbacks,
    }
  }

  /**
   * validate
   * @memberof FormStore
   */
  validate() {}

  /**
   * do submit
   * @memberof FormStore
   */
  submit() {
    let err = this.validate()
    const { onFinish, onFinishFailed } = this.callbacks
    if (err.length > 0) {
      onFinishFailed(err, this.store)
    } else {
      onFinish(null, this.store)
    }
  }

  getForm() {
    return {
      getFieldValue: this.getFieldValue,
      setFieldsValue: this.setFieldsValue,
      submit: this.submit,
    }
  }
}

export default function useForm() {
  const formStore = new FormStore()
  return [formStore.getForm()]
}

上面的逻辑,直接在 Form 中调用即可:

// Form.js
import React from 'react'
import useForm from './useForm'

export default function Form({ children, onFinish, onFinishFailed }) {
  const [form] = useForm()

  form.setCallbacks({
    onFinish,
    onFinishFailed,
  })

  const onSubmit = (e) => {
    e.preventDefault()
    form.submit()
  }

  return <form onSubmit={(e) => onSubmit(e)}>{children}</form>
}

这里存在一个小坑,那就是每次都会 new 一个新的 store,这显然不是我们想要的,那应该怎么办呢?

这里我们可以借助 react 提供的 useRef

export default function useForm(form) {
  const formRef = useRef()

  if (!formRef.current) {
    if (form) {
      formRef.current = form
    } else {
      const formStore = new FormStore()
      formRef.current = formStore
    }
  }

  return [formRef.current]
}

接下来就是处理数据了,不过在这之前,我们可以看到,在 FormItem 中同样需要调用 formInstance 中的方法,这里我们可以借助 context 来实现数据通信:

import { createContext } from 'react'

const FormContext = createContext()

export default FormContext

然后在 Form 中提供数据,就可以在 FormItem 中使用了:

<form onSubmit={(e) => onSubmit(e)}>
  <FormContext.Provider value={formInstance}>{children}</FormContext.Provider>
</form>
const getControlled = ({ name }) => {
  return {
    value: getFieldValue(name),
    onChange: (e) => {
      const newValue = e.target.value
      setFieldsValue({ [name]: newValue })
    },
  }
}
  • get 直接返回值即可
  • set 首先需要更新 store 中的值,然后要重新进行 render

处理如下:

/**
 * get value
 * @memberof FormStore
 */
getFieldValue(name) {
  const value = this.store[name]
  return value
}
/**
 * set values
 * @memberof FormStore
 */
setFieldsValue(newStore) {
  // set value
  this.store = {
    ...this.store,
    ...newStore,
  }
  // render
}

那么问题就来到了如何进行 render。

这里可以借助 react 提供的 forceUpdate,但 setFieldsValue 在 useForm 中,要如何执行对应 FormItem 的 forceUpdate 呢?

解决方案是注册:在 FormItem 挂载的时候将其注册到 useForm 中就能获取到它的实例了。(另外需要注意的是,有注册,那么一定要在对应的时候有注销)

那么问题又来了,如果是 class 组件,直接将 this 注册过来就好了,但我们使用的是函数组件,是没有 this 的,应该怎么办呢?

这里可以稍微转化一下思路,我们需要用到的东西只有两个:

  • name 值
  • render 方法

那么我们将这两个东西注册过来不就好了吗?

// FormItem.js
useEffect(() => {
  const unRegisterField = registerField({
    name,
    onValueChange,
  })
  return () => {
    unRegisterField()
  }
}, [onValueChange])
/**
 * set values
 * @memberof FormStore
 */
setFieldsValue = (newStore) => {
  // set value
  this.store = {
    ...this.store,
    ...newStore,
  }

  // render
  this.fieldInstances.forEach((filed) => {
    Object.keys(newStore).forEach((key) => {
      if (filed.name === key) {
        filed.onValueChange(newStore)
      }
    })
  })
}

接下来同样是 this 的问题:如果我们使用的是 class 组件,直接在 onValueChange 中调用 this.forceUpdate 就可以重新渲染了,但这里我们使用的是函数组件,是没有 this 的,应该怎么办呢?

这里我们可以借助 useReducer 返回值的第二个,具体参考官网:有类似 forceUpdate 的东西吗?

const [ignored, forceUpdate] = React.useReducer((x) => x + 1, 0)
useEffect(() => {
  const unRegisterField = registerField({
    name,
    rules,
- 	onValueChange,
+   onValueChange: forceUpdate,
  })
  return () => {
    if (unRegisterField) unRegisterField()
  }
}, [])

到这里,整个 store 的部分已经处理得差不多了,效果如下:

img-01

校验提交

这里其实没什么坑,值得一提的就是我们在注册的时候需要将 rules 也注册进来:

/**
 * validate
 * @memberof FormStore
 */
validate = () => {
  const err = []
  this.fieldInstances.forEach((field) => {
    const { name, rules } = field
    const value = this.getFieldValue(name)
    let rule = rules && rules[0]
    if (rule && rule.required && (value === undefined || value === '')) {
      err.push({
        [name]: rule.message,
        value,
      })
    }
  })
  return err
}

检验之后,根据检验结果调用不同的回调即可:

/**
 * do submit
 * @memberof FormStore
 */
submit = () => {
  let err = this.validate()
  const { onFinish, onFinishFailed } = this.callbacks
  if (err.length > 0) {
    onFinishFailed(err, this.store)
  } else {
    onFinish(null, this.store)
  }
}