【源码共读】| 简易实现antd v4-Form组件

Form表单组件是我们经常使用的一个组件。以antd库为例,我们通常会在登录场景中使用它。那么,如果要实现一个Form组件,应该怎么实现呢?

需求分析

Target: 实现一个简易的Form表单组件

在一个表单组件中,我们需要哪些功能呢?让我们参考antd v4的文档来看一下:
4x.ant.design/components/...

● 数据收集

● 数据校验

● 响应式

● 表单提交

让我们来看一下使用的代码,可以了解到以下内容:

  1. 在Form组件中包裹了Form.Item组件。
  2. 可以设置Form的初始值。
  3. 有校验成功和校验失败时的回调函数。
  4. Form.Item中包含了校验规则和提示信息。
tsx 复制代码
import { Button, Checkbox, Form, Input } from 'antd';
import React from 'react';

const App: React.FC = () => {
  const onFinish = (values: any) => {
    console.log('Success:', values);
  };

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

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

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

export default App;

我们可以打开到对应的仓库,看下源码实现:
github.com/ant-design/...

简易实现

Happy path

  • Form 使用一个form标签来包裹组件
tsx 复制代码
export default function Form(props) {
  const { children } = props
  return <form>{children}</form>
}
  • FormItem返回clone的节点
tsx 复制代码
import React, { Component } from 'react'

export default class FormItem extends Component {
  render() {
    const { children } = this.props
    const returnChildNode = React.cloneElement(children)
    return returnChildNode
  }
}

数据收集

当我们点击提交按钮时,可以获取到表单中的数据。那么如何实现数据的收集呢?

  • 每个子组件独立管理数据 ×
  • 父组件来统一收集管理 √
  • 第三方收集 √

再想深一层,如果交给父组件来的统一管理,比如存在Form表单的state中,每次数据更新setState会造成Form中的组件重新渲染。

所以,我们选用第三种方式,使用一个数据仓库来独立管理。

  • 构造一个FormStore
  • 使用useForm来存值,保持唯一性
tsx 复制代码
import { useRef } from 'react'

class FormStore {
  constructor() {
    this.store = {}
  }

  getFieldValue = (name) => {
    return this.store[name]
  }
  getFieldsValue = () => {
    return { ...this.store }
  }

  setFieldsValue = (newStore) => {
    this.store = {
      ...this.store,
      ...newStore
    }
  }

  submit = () => {
    console.log('submit')
  }

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

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

  if (!formRef.current) {
    const formStore = new FormStore()
    formRef.current = formStore.getForm()
  }
  return [formRef.current]
}
tsx 复制代码
// context
import React from "react";

const FieldContext = React.createContext();

export default FieldContext;
  • 在父组件使用Provider
tsx 复制代码
import FieldContext from './FieldContext'
import useForm from './useForm'

export default function Form(props) {
  const { children } = props
  const [formInstance] = useForm()

  const handleSubmit = (e) => {
    e.preventDefault()
    formInstance.submit()
  }

  return (
    <form onSubmit={handleSubmit}>
      <FieldContext.Provider value={formInstance}>
        {children}
      </FieldContext.Provider>
    </form>
  )
}
  • 子组件使用
tsx 复制代码
import React, { Component } from 'react'
import FieldContext from './FieldContext'

export default class FormItem extends Component {
  static contextType = FieldContext

  // 受控组件
  getControl = () => {
    const { getFieldValue, setFieldsValue } = this.context

    const { name } = this.props

    return {
      value: getFieldValue(name),
      onChange: (e) => {
        const newValue = e.target.value
        setFieldsValue({ [name]: newValue })
      }
    }
  }

  render() {
    const { children } = this.props
    const returnChildNode = React.cloneElement(children, this.getControl())
    return returnChildNode
  }
}

可以看到我们已经收集了组件的数据

数据响应式

然而,还存在一个问题:数据已经改变,但组件没有触发更新。因此,我们需要添加触发更新的逻辑。

tsx 复制代码
// useForm
class FormStore {
  // ... 省略代码
  constructor() {
    // 存放FromItem实例
    this.fieldEntities = []
  }

  registerFieldEntities = (entity) => {
    this.fieldEntities.push(entity)

    // unregister
    return () => {
      this.fieldEntities = this.fieldEntities.filter((item) => item !== entity)
      delete this.store[entity]
    }
  }
  // ... 省略代码
  getForm = () => {
    return {
      getFieldsValue: this.getFieldsValue,
      getFieldValue: this.getFieldValue,
      setFieldsValue: this.setFieldsValue,
      submit: this.submit,
      registerFieldEntities: this.registerFieldEntities
    }
  }
}
  • 在子组件挂载时注册,在卸载前注销
tsx 复制代码
export default class FormItem extends Component {
  // ... 省略代码
  componentDidMount() {
    const { registerFieldEntities } = this.context
    this.unregister = registerFieldEntities(this)
  }

  componentWillUnmount() {
    this.unregister()
  }

  onStoreChange = () => {
    this.forceUpdate()
  }
  // ... 省略代码
}
  • 当触发更新时,我们来刷新对应组件渲染
tsx 复制代码
// useForm
class FormStore {
  
  // ... 省略代码
  setFieldsValue = (newStore) => {
    this.store = {
      ...this.store,
      ...newStore
    }
    // 触发更新
    this.fieldEntities.forEach((entity) => {
      Object.keys(newStore).forEach((k) => {
        if (k === entity.props.name) {
          entity.onStoreChange()
        }
      })
    })
  }
  // ... 省略代码
}

到目前为止,我们已经成功收集和更新了表单数据。

表单校验

我们先在store中收集验证后的回调函数

tsx 复制代码
// useForm
class FormStore {
  // ...省略
  constructor() {
    this.callbacks = {}
  }

  setCallbacks = (callbacks) => {
    this.callbacks = {
      ...this.callbacks,
      ...callbacks
    }
  }
  // ...省略
  validate() {
    let err = []
    const fieldEntities = this.fieldEntities
    fieldEntities.forEach((entity) => {
      const { name, rules } = entity.props
      const value = this.getFieldValue(name)
      // warning: 只取了第一个校验规则
      let rule = rules[0]
      if (
        rule &&
        rule.required &&
        (value === undefined || value.trim() === '')
      ) {
        err.push({ [name]: rule.message, value })
      }
    })
    return err
  }

  submit = () => {
    console.log('submit')

    let err = this.validate()
    const { onFinish, onFinishFailed } = this.callbacks

    if (err.length === 0) {
      // 校验通过
      onFinish(this.getFieldsValue())
    } else {
      // 校验不通过
      onFinishFailed(err, this.getFieldsValue())
    }
  }


  getForm = () => {
    return {
      getFieldsValue: this.getFieldsValue,
      getFieldValue: this.getFieldValue,
      setFieldsValue: this.setFieldsValue,
      submit: this.submit,
      registerFieldEntities: this.registerFieldEntities,
      setCallbacks: this.setCallbacks
    }
  }

}
tsx 复制代码
// Form
export default function Form(props) {
  // ...省略
  const { children, onFinish, onFinishFailed } = props
  const [formInstance] = useForm()

  formInstance.setCallbacks({
    onFinish,
    onFinishFailed
  })
  // ...省略
}

目前为止,我们已经完成了表单的校验。

初始值的设定

首先,我们需要将form实例暴露出去

tsx 复制代码
// Form
export default function Form({ children, onFinish, onFinishFailed }, ref) {
  React.useImperativeHandle(ref, () => formInstance)
  // ...省略
}
tsx 复制代码
// Rc-form
export default class MyRCFieldForm extends Component {
  formRef = React.createRef()
  componentDidMount() {
    console.log('form', this.formRef.current)
    this.formRef.current.setFieldsValue({ username: 'default' })
  }
  
  render() {
    return (
      <div>
        <h3>MyRCFieldForm</h3>
        <Form
          ref={this.formRef}
          onFinish={this.onFinish}
          onFinishFailed={this.onFinishFailed}
        ></Form>
      </div>
    )
  }
}
tsx 复制代码
// useForm
export default function useForm(form) {
  // 存值,在组件卸载之前指向的都是同一个值
  const formRef = useRef()

  if (!formRef.current) {
    if (form) {
      formRef.current = form
    } else {
      const formStore = new FormStore()
      formRef.current = formStore.getForm()
    }
  }
  return [formRef.current]
}
tsx 复制代码
// rc-form index
import FormItem from './FormItem'

import _Form from './Form'
import React from 'react'
// 将form实例暴露出去
const Form = React.forwardRef(_Form)
Form.Item = FormItem

export { FormItem }
export default Form

至此,我们已经完成了简易的Form表单组件的实现

总结

看到这里,说明你已经成功实现了Form组件。让我们回顾一下涉及的内容:

  • 表单验证
  • 数据状态管理
  • 表单交互
  • 表单提交

通过以上代码,我们可以得知antd的form表单是通过Provider Context来传递数据的。同时,还需要建立一个独立的store来收集数据状态,并处理表单验证和提交。通过useForm的形式来保证了数据的唯一性。


参考来源:

  1. juejin.cn/post/694093...
  2. github.com/ant-design/...
相关推荐
中微子13 分钟前
React状态管理最佳实践
前端
烛阴23 分钟前
void 0 的奥秘:解锁 JavaScript 中 undefined 的正确打开方式
前端·javascript
中微子29 分钟前
JavaScript 事件与 React 合成事件完全指南:从入门到精通
前端
Hexene...38 分钟前
【前端Vue】如何实现echarts图表根据父元素宽度自适应大小
前端·vue.js·echarts
天天扭码1 小时前
《很全面的前端面试题》——HTML篇
前端·面试·html
xw51 小时前
我犯了错,我于是为我的uni-app项目引入环境标志
前端·uni-app
!win !1 小时前
被老板怼后,我为uni-app项目引入环境标志
前端·小程序·uni-app
Burt1 小时前
tsdown vs tsup, 豆包回答一坨屎,还是google AI厉害
前端
群联云防护小杜2 小时前
构建分布式高防架构实现业务零中断
前端·网络·分布式·tcp/ip·安全·游戏·架构
ohMyGod_1233 小时前
React16,17,18,19新特性更新对比
前端·javascript·react.js