24-实践篇-表单验证下
课程
1
写给想要进阶的你
已学完
学习时长: 4分28秒
2
基础篇-认识 jsx
已学完
学习时长: 40分51秒
3
基础篇-起源 Component
已学完
学习时长: 32分50秒
4
基础篇-玄学 state
已学完
学习时长: 33分37秒
5
基础篇-深入 props
已学完
学习时长: 43分58秒
6
基础篇-理解 lifeCycle
学习时长: 72分46秒
7
基础篇-多功能 Ref
已学完
学习时长: 71分17秒
8
基础篇-提供者 context
已学完
学习时长: 53分16秒
9
基础篇-模块化 css
学习时长: 35分20秒
10
基础篇-高阶组件
学习时长: 43分29秒
11
优化篇-渲染控制
学习时长: 50分4秒
12
优化篇-渲染调优
学习时长: 35分8秒
13
优化篇-处理海量数据
学习时长: 28分1秒
14
优化篇-细节处理(持续)
学习时长: 32分19秒
15
原理篇-事件原理
学习时长: 30分48秒
16
原理篇-调度与时间片
学习时长: 23分47秒
17
原理篇- 调和与 fiber
学习时长: 35分54秒
18
原理篇-Hooks 原理
已学完
学习时长: 34分34秒
19
生态篇-React-router
学习时长: 50分9秒
20
生态篇-React-redux
学习时长: 50分34秒
21
生态篇-React-mobx
学习时长: 65分3秒
22
实践篇-实现mini-Router
学习时长: 37分19秒
23
实践篇-表单验证上
学习时长: 41分49秒
24
实践篇-表单验证下
学习时长: 46分31秒
25
实践篇-自定义弹窗
学习时长: 35分4秒
26
实践篇-自定义 Hooks 设计(持续)
学习时长: 32分13秒
27
实践篇-自定义 Hooks 实践(持续)
学习时长: 58分47秒
28
总结篇-如何有效阅读源码
学习时长: 7分18秒
29
原理篇-Context原理
学习时长: 28分27秒
30
原理篇-beginWork和render全流程
学习时长: 38分16秒
31
V18特性篇-useMutableSource(已被取缔)
学习时长: 28分37秒
32
V18特性篇-transition
学习时长: 38分15秒
33
原理篇-更新流程:进入调度任务
学习时长: 30分20秒
34
v18特性篇-concurrent 下的 state更新流程
学习时长: 29分27秒
35
v18特性篇-订阅外部数据源
学习时长: 16分53秒
36
原理篇-v18commit全流程
学习时长: 45分40秒
37
v18新特性-Offscreen
学习时长: 1秒
38
实践篇-设计并实现 keepalive 功能
学习时长: 71分13秒
juejin_logo copyCreated with Sketch.

一 前言

上一章节主要讲了 Form 表单的设计原则,以及状态管理 FormStore 和自定义 hooks useForm 的编写,本章节将继续上一章节没有讲完的部分。

通过本章节的学习,你将收获以下知识点:

  • Form 设计及其编写。
  • FormItem 设计及其编写。

二 Form 编写

1 属性分析

属性设定

属性名称作用类型
form传入useForm 创建的 FormStore实例FormStore 实例对象
onFinish表单提交成功调用function ,一个参数,为表单的数据层
onFinishFailed表单提交失败调用function ,一个参数,为表单的数据层
initialValues设置表单初始化的值object

细节问题

  • Form 接收类似 onFinishonFinishFailed 监听回调函数。

  • Form 可以被 ref 标记,ref 可以获取 FormStore 核心方法。

  • Form 要保留原生的 form 属性,当 submit 或者 reset 触发,自动校验/重置。

2 代码实现

创建 context 保存 FormStore 核心 Api

import {  createContext  } from 'react'
/* 创建一个 FormContext */
const  FormContext = createContext()

export default FormContext
  • 创建一个 context 用来保存 FormStore 的核心 API 。

接下来就是重点 Form 编写

function Form ({
    form,
    onFinish,
    onFinishFailed,
    initialValues,
    children
},ref){
    /* 创建 form 状态管理实例 */
    const formInstance = useForm(form,initialValues)
    /* 抽离属性 -> 抽离 dispatch | setCallback 这两个方法不能对外提供。  */
    const { setCallback, dispatch  ,...providerFormInstance } = formInstance

    /* 向 form 中注册回调函数 */
    setCallback({
        onFinish,
        onFinishFailed
    })

    /* Form 能够被 ref 标记,并操作实例。 */
    useImperativeHandle(ref,() => providerFormInstance , [])
    /* 传递 */
    const RenderChildren = <FormContext.Provider value={formInstance} > {children} </FormContext.Provider>

    return <form
        onReset={(e)=>{
            e.preventDefault()
            e.stopPropagation()
            formInstance.resetFields() /* 重置表单 */
        }}
        onSubmit={(e)=>{
            e.preventDefault()
            e.stopPropagation()
            formInstance.submit()      /* 提交表单 */
        }}
           >
           {RenderChildren}
        </form>
}

export default forwardRef(Form)

Form 实现细节分析:

  • 首先通过 useForm 创建一个 formInstance ,里面保存着操纵表单状态的方法,比如 getFieldValuesetFieldsValue 等。

  • formInstance 抽离出 setCallback ,dispatch 等方法,得到 providerFormInstance ,因为这些 api 不期望直接给开发者使用。通过 forwardRef + useImperativeHandle 来转发 ref, 将 providerFormInstance 赋值给 ref , 开发者通过 ref 标记 Form ,本质上就是获取的 providerFormInstance 对象。

  • 通过 Context.Provider 将 formInstance 传递下去,提供给 FormItem 使用。

  • 创建原生 form 标签,绑定 React 事件 —— onResetonSubmit,在事件内部分别调用, 重置表单状态的 resetFields 和提交表单的 onSubmit方法。

三 FormItem 编写

接下来就是 FormItem 的具体实现细节。

1 属性分析

相比 antd 中的 FormItem ,属性要精简的多,这里我保留了一些核心的属性。

属性名称作用类型
name (重要属性)证明表单单元项的键 namestring
label表单标签属性string
height表单单元项高度number
labelWidthlable 宽度number
required是否必填boolean
trigger收集字段值变更的方法string , 默认为 onChange
validateTrigger验证校验触发的方法string,默认为 onChange
rules验证信息里面包括验证方法 rule 和 验证失败提示文案 message

2 代码实现

接下来就是 FormItem 的代码实现。

function FormItem ({
    name,
    children,
    label,
    height = 50 ,
    labelWidth,
    required = false ,
    rules = {},
    trigger = 'onChange',
    validateTrigger = 'onChange'
}){
    const formInstance  = useContext(FormContext)
    const { registerValidateFields , dispatch , unRegisterValidate } = formInstance
    const [ , forceUpdate ] = useState({})
    const onStoreChange = useMemo(()=>{
        /* 管理层改变 => 通知表单项 */
        const onStoreChange = {
            changeValue(){
                forceUpdate({})
            }
         }
        return onStoreChange

    },[ formInstance ])
    useEffect(()=>{
         /* 注册表单 */
        name && registerValidateFields(name,onStoreChange,{ ...rules , required })
        return function(){
            /* 卸载表单 */
           name &&  unRegisterValidate(name)
        }
    },[ onStoreChange ])
     /* 使表单控件变成可控制的 */
    const getControlled = (child)=> {
        const mergeChildrenProps = { ...child.props }
        if(!name) return mergeChildrenProps
         /* 改变表单单元项的值 */
        const handleChange  = (e)=> {
             const value = e.target.value
              /* 设置表单的值 */
             dispatch({ type:'setFieldsValue' },name ,value)
         }
        mergeChildrenProps[trigger] = handleChange
        if(required || rules ){
             /* 验证表单单元项的值 */
            mergeChildrenProps[validateTrigger] = (e) => {
                 /* 当改变值和验证表单,用统一一个事件 */
                if(validateTrigger === trigger){
                    handleChange(e)
                }
                /* 触发表单验证 */
                dispatch({ type:'validateFieldValue' },name)
            }
        }
        /* 获取 value */
        mergeChildrenProps.value = dispatch({ type:'getFieldValue' }, name) || ''
        return mergeChildrenProps
    }
    let renderChildren
    if(isValidElement(children)){
        /* 获取 | 合并 | 转发 | =>  props  */
        renderChildren = cloneElement(children, getControlled(children))
    }else{
        renderChildren = children
    }
    return <Label
        height={height}
        label={label}
        labelWidth={labelWidth}
        required={required}
           >
         {renderChildren}
         <Message
             name={name}
             {...dispatch({ type :'getFieldModel'},name)}
         />
     </Label>
}

FormItem 的流程比较复杂,接下来我将一一讲解其流程。

  • 第一步: FormItem 会通过 useContext 获取到表单实例下的方法。
  • 第二步: 创建一个 useState 作为 FormItem 的更新函数 onStoreChange。
  • 第三步: 在 useEffect 中调用 registerValidateFields 注册表单项。此时的 FormItem 的更新函数 onStoreChange 会传入到 FormStore 中,上一章节讲到过,更新方法最终会注册到 FormStore 的 control 属性下,这样 FormStore 就可以选择性的让对应的 FormItem 更新。在 useEffect 销毁函数中,解绑表单项。
  • 第四步: 让 FormItem 包裹的表单控件变成受控的, 通过 cloneElement 向表单控件( 比如 Input ) props 中,注册监听值变化的方法,默认为 onChange ,以及表单验证触发的方法 ,默认也是 onChange ,比如如下例子🌰:
   <FormItem
        label="请输入小册名称"
        labelWidth={150}
        name="name"
        required
        rules={{
            rule:/^[a-zA-Z0-9_\u4e00-\u9fa5]{4,32}$/,
            message:'名称仅支持中文、英文字母、数字和下划线,长度限制4~32个字'
        }}
        trigger="onChange"
        validateTrigger="onBlur"
    >
        <Input
            placeholder="小册名称"
        />
    </FormItem>

如上,向 FormItem 中, 绑定监听变化的事件为 onChange,表单验证的事件为 onBlur

更新流程 :那么整个流程,当组件值改变的时候,会触发 onChange 事件,本质上被上面的 getControlled 拦截,实质用 dispatch 触发 setFieldsValue ,改变 FormStore 表单的值,然后 FormStore 会用 onStoreChange 下的 changeValue 通知当前 FormItem 更新,FormItem 更新通过 dispatch 调用 getFieldValue 获取表单的最新值,并渲染视图。这样完成整个受控组件状态更新流程。

验证流程: 当触发 onBlur 本质上用 dispatch 调用 validateFieldValue 事件,验证表单,然后 FormStore 会下发验证状态(是否验证通过)。

完成更新/验证流程。

  • 第五步:渲染 LabelMessage UI 视图。

四 Index文件及其他组件

还有一些负责 UI 渲染的组件,以及表单控件,这里就简单介绍一下:

Label

function Label({ children , label ,labelWidth , required ,height}){
    return <div className="form-label"
        style={{ height:height + 'px'  }}
           >
       <div
           className="form-label-name"
           style={{ width : `${labelWidth}px` }}
       >
           {required ? <span style={{ color:'red' }} >*</span> : null}
           {label}:
        </div>  {children}
    </div>
}
  • Label 的作用就是渲染表单的标签。

Message

function Message(props){
    const { status , message , required , name , value } = props
    let showMessage = ''
    let color = '#fff'
    if(required && !value && status === 'reject'  ){
        showMessage = `${name} 为必填项`
        color = 'red'
    }else if(status === 'reject'){
        showMessage = message
        color = 'red'
    }else if(status === 'pendding'  ){
        showMessage = null
    }else if( status === 'resolve' ){
        showMessage = '校验通过'
        color = 'green'
    }
    return <div className="form-message" >
       <span style={{ color  }}  >{showMessage}</span>
    </div>
}
  • message 显示表单验证的状态,比如失败时候的提示文案等,成功时候的提示文案。

Input

const Input = (props) => {
    return <input
        className="form-input"
        {...props}
           />
}
  • Input 本质上就是 input 标签。

Select 组件

function Select({ children,...props }){
    return <select {...props}
        className="form-input"
           >
        <option label={props.placeholder}
            value={null}
        >{props.placeholder}</option>
        {children}
    </select>
}
/* 绑定静态属性   */
Select.Option = function ( props ){
    return <option {...props}
        className=""
        label={props.children}
           ></option>
}

export default Select

Index文件

Index 文件对组件整理,并暴露给开发者使用。

import Form from './component/Form'
import FormItem from './component/FormItem'
import Input from './component/Input'
import Select from './component/Select'

Form.FormItem = FormItem

export {
    Form,
    Select,
    Input,
    FormItem
}

export default Form

五 验证功能

验证 demo 编写

import React , { useRef , useEffect } from 'react'

import Form , { Input , Select } from './form'

const FormItem = Form.FormItem
const Option = Select.Option

function Index(){
    const form = useRef(null)
    useEffect(()=>{
        console.log(form.current,'form.current')
    },[])
    const handleClick = () => {
         form.current.submit((res)=>{
             console.log(res)
         })
    }
    const handleGetValue = ()=>{
        console.log( form.current , 'form.current ' )
    }
    return <div style={{ marginTop:'50px' }} >
        <Form  initialValues={{ author : '我不是外星人' }}
            ref={form}
        >
            <FormItem
                label="请输入小册名称"
                labelWidth={150}
                name="name"
                required
                rules={{
                    rule:/^[a-zA-Z0-9_\u4e00-\u9fa5]{4,32}$/,
                    message:'名称仅支持中文、英文字母、数字和下划线,长度限制4~32个字'
                }}
                validateTrigger="onBlur"
            >
                 <Input
                     placeholder="小册名称"
                 />
            </FormItem>
            <FormItem
                label="作者"
                labelWidth={150}
                name="author"
                required
                validateTrigger="onBlur"
            >
                 <Input
                     placeholder="请输入作者"
                 />
            </FormItem>
            <FormItem label="邮箱"
                labelWidth={150}
                name="email"
                rules={{ rule: /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/ ,message:'邮箱格式错误!'  }}
                validateTrigger="onBlur"
            >
                <Input
                    placeholder="请输入邮箱"
                />
            </FormItem>
            <FormItem label="手机"
                labelWidth={150}
                name="phone"
                rules={{ rule: /^1[3-9]\d{9}$/ ,message:'手机格式错误!'  }}
                validateTrigger="onBlur"
            >
                <Input
                    placeholder="请输入邮箱"
                />
            </FormItem>
            <FormItem label="简介"
                labelWidth={150}
                name="des"
                rules={{ rule: (value='') => value.length < 5   ,message:'简介不超过五个字符'  }}
                validateTrigger="onBlur"
            >
                <Input placeholder="输入简介"  />
            </FormItem>
            <FormItem label="你最喜欢的前端框架"
                labelWidth={150}
                name="likes"
                required
            >
                <Select  defaultValue={null}
                    placeholder="请选择"
                    width={120}
                >
                    <Option
                        value={1}
                    > React.js </Option>
                    <Option value={2} > Vue.js </Option>
                    <Option value={3} > Angular.js </Option>
                </Select>
            </FormItem>
            <button className="searchbtn"
                onClick={handleClick}
                type="button"
            >提交</button>
            <button className="concellbtn"
                type="reset"
            >重置</button>
        </Form>
       <div style={{ marginTop:'20px' }} >
            <span>验证表单功能</span>
            <button className="searchbtn"
                onClick={handleGetValue}
                style={{ background:'green' }}
            >获取表单数层</button>
            <button className="searchbtn"
                onClick={()=> form.current.validateFields((res)=>{ console.log('是否通过验证:' ,res ) })}
                style={{ background:'orange' }}
            >动态验证表单</button>
            <button className="searchbtn" onClick={() => { form.current.setFieldsValue('des',{
                    rule: (value='') => value.length < 10,
                    message:'简介不超过十个字符'
                }) }}
                style={{ background:'purple' }}
            >动态设置校验规则</button>
       </div>
    </div>
}

export default Index

验证效果

接下来就是验证环节:

① 表单验证未通过

fail.gif

调用 submit ,验证失败的情况。

② 表单验证通过

success.gif

验证成功!

③ 获取表单的数据层

get.gif

通过 getFieldsValue 获取表单数据层。

④ 重置表单的数据层

reset.gif

通过 resetFields 重置表单。

⑤ 动态添加表单验证规则

dongtai.gif

通过 setFieldsValue 动态设置规则。

之前规则和提示文案 { rule: (value='') => value.length < 5 ,message:'简介不超过五个字符' }

动态设置规则 { rule: (value='') => value.length < 10, message:'简介不超过十个字符' }

六 总结

以上就是从 0 到 1 设计的表单验证系统,希望读者能够对着项目 demo 敲一遍,在实现过程中,我相信会有很多收获。

留言
Ctrl + Enter
全部评论(14)
console_man的头像
删除
Web前端
这两节阔以,很通透。
点赞
1
删除
感谢啦~
点赞
回复
王阿觉的头像
删除
全栈工程师 @ 广州玩得酷文化服务有限公司
再提一个问题,就是想问一下,如果 FormItem 中 不是一个 常规的input /select 而是一个自定义的 组件的时候,应该如何使用这里的规则呢
点赞
2
删除
原理是一样的呀 和input/select还是自定义的组件没有关系
点赞
回复
删除
对的 ,你可以发现 Input 组件之类的 props 和原生组件都是一样的
点赞
回复
王阿觉的头像
删除
全栈工程师 @ 广州玩得酷文化服务有限公司
FormStore 中 暂时未启用的 forceUpdate 可以进行一下什么操作呢?
点赞
回复
闲庭信步在掘金的头像
删除
这个相比于前几个月,作者是不是重构过
1
1
删除
小册一直在维护
1
回复
Neil酱37044的头像
删除
Android开发
表单验证太牛逼了,写的很好
4
1
删除
感谢啦
点赞
回复
不要问我从哪来的头像
删除
搬砖工人
表单验证这两节非常给力,一下豁然开朗的感觉 ,哈哈
1
1
删除
你看的这么快吗?告诉我你是按照顺序阅读的吗🤪
1
回复
dcbryant18067的头像
删除
写得很好,麻烦提供一个github地址😜
1
1
删除
第一节有
点赞
回复