前言
最近有一个非常复杂的表单需求,可能需要对表单做“任何事情”,现有的 UI
组件库选用的是 Ant Design
简称 antd
。它的 Form
表单已经帮我们把“表单项校验”、“表单项错误信息”等常见操作全部封装好了。使用起来非常便捷。翻看了 antd Form
源码发现其核心能力都是通过 rc-field-form
库,提供出来的。因此阅读它的源码将是作者项目开始前必须要做的。
本文将模拟 rc-field-form
库,手写一个“学习版” ,深入学习其思想。
如果本文对你有所帮助,请点个👍 吧!
工程搭建
rc-field-form
使用的是 Dumi
和 father-build
对组件库进行打包,为了保持一致,作者也将使用这两个工具来完成项目。
Dumi
dumi
中文发音嘟米,是一款为组件开发场景而生的文档工具,与 father-builder
一起为开发者提供一站式的组件开发体验, father-builder
负责构建,而 dumi
负责组件开发及组件文档生成。
father-build
father-build
属于 father
(集文档与组件打包一体的库)的一部分,专注于组件打包。
脚手架创建项目
使用 @umijs/create-dumi-lib
来初始化项目。这个脚手架整合了上面提及的两个工具。
mkdir lion-form // 创建lion-form文件夹
cd lion-form // 进入文件夹
npm init -y // 初始化 package.json
npx @umijs/create-dumi-lib // 初始化整体项目结构
项目结构说明
├──README.md // 文档说明
├──node_modules // 依赖包文件夹
├──package.json // npm 包管理
├──.editorconfig // 编辑器风格统一配置文件
├──.fatherrc.ts // 打包配置
├──.umirc.ts // 文档配置
├──.prettierrc // 文本格式化配置
├──tsconfig.json // ts 配置
└──docs // 仓库公共文档
└──index.md // 组件库文档首页
└──src
└──index.js // 组件库入口文件
启动项目
npm start 或 yarn start
集文档,打包为一体的组件库就这样快速的搭建完成了。下面就让我们来手写一个 rc-field-form
吧。
源码编写
rc-field-form
对于经常使用 react
开发的同学来说, antd
应该都不会陌生。开发中经常遇到的表单大多会使用 antd
中的 Form
系列组件完成,而 rc-field-form
又是 antd Form
的重要组成部分,或者说 antd Form
是对 rc-field-form
的进一步的封装。
想要学习它的源码,首先还是得知道如何使用它,不然难以理解源码的一些深层次的含义。
简单的示例
首先来实现如下图所示的表单,类似于我们写过的登录注册页面。
代码示例:
import React, { Component, useEffect} from 'react'
import Form, { Field } from 'rc-field-form'
import Input from './Input'
// name 字段校验规则
const nameRules = {required: true, message: '请输入姓名!'}
// password 字段校验规则
const passwordRules = {required: true, message: '请输入密码!'}
export default function FieldForm(props) {
// 获取 form 实例
const [form] = Form.useForm()
// 提交表单时触发
const onFinish = (val) => {
console.log('onFinish', val)
}
// 提交表单失败时触发
const onFinishFailed = (val) => {
console.log('onFinishFailed', val)
}
// 组件初始化时触发,它是React原生Hook
useEffect(() => {
form.setFieldsValue({username: 'lion'})
}, [])
return (
<div> <h3>FieldForm</h3> <Form form={form} onFinish={onFinish} onFinishFailed={onFinishFailed}> <Field name='username' rules={[nameRules]}> <Input placeholder='请输入姓名' /> </Field> <Field name='password' rules={[passwordRules]}> <Input placeholder='请输入密码' /> </Field> <button>Submit</button> </Form> </div>
)
}
// input简单封装
const Input = (props) => {
const { value,...restProps } = props;
return <input {...restProps} value={value} />;
};
这种写法还是非常便捷的,不再需要像 antd3
一样使用高阶函数包裹一层。而是直接通过 Form.useForm()
获取到 formInstance
实例, formInstance
实例身上承载了表单需要的所有数据及方法。
通过 form.setFieldsValue({username: 'lion'})
这段代码就不难发现,可以通过 form
去手动设置 username
的初始值。也可以理解成所有的表单项都被 formInstance
实例接管了,可以使用 formInstance
实例做到任何操作表单项的事情。 formInstance
实例也是整个库的核心。
基础框架搭建
通过对 rc-field-form
源码的学习,我们先来搭建一个基础框架。
useForm
- 通过
Form.useForm()
获取formInstance
实例; formInstance
实例对外提供了全局的方法如setFieldsValue
、getFieldsValue
;- 通过
context
让全局可以共享formInstance
实例。
src/useForm.tsx
import React , {useRef} from "react";
class FormStore {
// stroe 用来存储表单数据,它的格式:{"username": "lion"}
private store: any = {};
// 用来存储每个 Field 的实例数据,因此在store中可以通过 fieldEntities 来访问到每个表单项
private fieldEntities: any = [];
// 表单项注册到 fieldEntities
registerField = (entity:any)=>{
this.fieldEntities.push(entity)
return () => {
this.fieldEntities = this.fieldEntities.filter((item:any) => item !== entity)
delete this.store[entity.props.name]
}
}
// 获取单个字段值
getFieldValue = (name:string) => {
return this.store[name]
}
// 获取所有字段值
getFieldsValue = () => {
return this.store
}
// 设置字段的值
setFieldsValue = (newStore:any) => {
// 更新store的值
this.store = {
...this.store,
...newStore,
}
// 通过 fieldEntities 获取到所有表单项,然后遍历去调用表单项的 onStoreChange 方法更新表单项
this.fieldEntities.forEach((entity:any) => {
const { name } = entity.props
Object.keys(newStore).forEach(key => {
if (key === name) {
entity.onStoreChange()
}
})
})
}
// 提交数据,这里只简单的打印了store中的数据。
submit = ()=>{
console.log(this.getFieldsValue());
}
// 提供FormStore实例方法
getForm = (): any => ({
getFieldValue: this.getFieldValue,
getFieldsValue: this.getFieldsValue,
setFieldsValue: this.setFieldsValue,
registerField: this.registerField,
submit: this.submit,
});
}
// 创建单例formStore
export default function useForm(form:any) {
const formRef = useRef();
if (!formRef.current) {
if (form) {
formRef.current = form;
} else {
const formStore = new FormStore();
formRef.current = formStore.getForm() as any;
}
}
return [formRef.current]
}
其中 FormStore
是用来存储全局数据和方法的。 useForm
是对外暴露 FormStore
实例的。从 useForm
的实现可以看出,借助 useRef
实现了 FormStore
实例的单例模式。
FieldContext
定义了全局 context
。
import * as React from 'react';
const warningFunc: any = () => {
console.log("warning");
};
const Context = React.createContext<any>({
getFieldValue: warningFunc,
getFieldsValue: warningFunc,
setFieldsValue: warningFunc,
registerField: warningFunc,
submit: warningFunc,
});
export default Context;
Form 组件
- 传递
FieldContext
; - 拦截处理
submit
事件; - 渲染子节点。
src/Form.tsx
import React from "react";
import useForm from "./useForm";
import FieldContext from './FieldContext';
export default function Form(props:any) {
const {form, children, ...restProps} = props;
const [formInstance] = useForm(form) as any;
return <form {...restProps} onSubmit={(event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); event.stopPropagation(); // 调用了formInstance 提供的submit方法 formInstance.submit(); }} > {/* formInstance 当做全局的 context 传递下去 */} <FieldContext.Provider value={formInstance}>{children}</FieldContext.Provider> </form> }
Field 组件
- 把自己注册到
FormStore
中; - 拦截子元素为其添加
value
以及onChange
属性。
src/Field.tsx
import React,{Component} from "react";
import FieldContext from "./FieldContext";
export default class Field extends Component {
// Filed 组件获取 FieldContext
static contextType = FieldContext;
private cancelRegisterFunc:any;
// Field 挂载时,把自己注册到FieldContext中,也就是上面提及的 fieldEntities 数组中。
componentDidMount() {
const { registerField } = this.context;
this.cancelRegisterFunc = registerField(this);
}
// Field 组件卸载时,调用取消注册,就是从 fieldEntities 中删除。
componentWillUnmount() {
if (this.cancelRegisterFunc) {
this.cancelRegisterFunc()
}
}
// 每个 Field 组件都应该包含 onStoreChange 方法,用来更新自己
onStoreChange = () => {
this.forceUpdate()
}
// Field 中传进来的子元素变为受控组件,也就是主动添加上 value 和 onChange 属性方法
getControlled = () => {
const { name } = this.props as any;
const { getFieldValue, setFieldsValue } = this.context
return {
value: getFieldValue(name),
onChange: (event:any) => {
const newValue = event.target.value
setFieldsValue({[name]: newValue})
},
}
}
render() {
const {children} = this.props as any;
return React.cloneElement(children, this.getControlled())
}
}
Form
组件的基础框架就此搭建完成了,它已经可以实现一些简单的效果,下面我们在 docs
目录写个例子。
docs/examples/basic.tsx
...省略了部分代码
export default function BasicForm(props) {
const [form] = Form.useForm()
useEffect(() => {
form.setFieldsValue({username: 'lion'})
}, [])
return (
<Form form={form}> <Field name='username'> <Input placeholder='请输入姓名' /> </Field> <Field name='password'> <Input placeholder='请输入密码' /> </Field> <button>提交</button> </Form>
)
}
解析:
- 组件初始化时调用
form.setFieldsValue({username: 'lion'})
方法; setFieldsValue
根据传入的参数,更新了store
值,并通过name
找到相应的Field
实例;- 调用
Field
实例的onStoreChange
方法,更新组件; - 组件更新,初始值就展示到界面上了。
Form
Form 组件获取 ref
antd
文档上有这么一句话:“我们推荐使用 Form.useForm
创建表单数据域进行控制。如果是在 class component
下,你也可以通过 ref
获取数据域”。
使用方式如下:
export default class extends React.Component {
formRef = React.createRef()
componentDidMount() {
this.formRef.current.setFieldsValue({username: 'lion'})
}
render() {
return (
<Form ref={this.formRef}> <Field name='username'> <Input /> </Field> <Field name='password'> <Input /> </Field> <button>Submit</button> </Form>
)
}
}
通过传递 formRef
给 Form
组件。获取 Form
的 ref
实例,但是我们知道 Form
是通过函数组件创建的,函数组件没有实例,无法像类组件一样可以接收 ref
。因此需要借助 React.forwardRef
与 useImperativeHandle
。
src/Form.tsx
export default React.forwardRef((props: any, ref) => {
... 省略
const [formInstance] = useForm(form) as any;
React.useImperativeHandle(ref, () => formInstance);
... 省略
})
React.forwardRef
解决了,函数组件没有实例,无法像类组件一样可以接收ref
属性的问题;useImperativeHandle
可以让你在使用ref
时,决定暴露什么给父组件,这里我们将formInstance
暴露出去,这样父组件就可以使用formInstance
了。
关于 React Hooks
不熟悉的同学可以阅读作者的这篇文章:React Hook 从入门应用到编写自定义 Hook。
初始值 initialValues
之前我们都是这样去初始化表单的值:
useEffect(() => {
form.setFieldsValue({username: 'lion'})
}, [])
显然这样初始化是不够优雅的,官方提供了 initialValues
属性让我们去初始化表单项的,下面让我们来支持它吧。
src/useForm.ts
class FormStore {
// 定义初始值变量
private initialValues = {};
setInitialValues = (initialValues:any,init:boolean)=>{
// 初始值赋给initialValues变量,这样 formInstance 就一直会保存一份初始值
this.initialValues = initialValues;
// 同步给store
if(init){
// setValues 是rc-field-form提供的工具类,作者这里全部copy过来了,不用具体关注工具类的实现
// 这里知道 setValues 会递归遍历 initialValues 返回一个新的对象。
this.store = setValues({}, initialValues, this.store);
}
}
getForm = (): any => ({
... 这里省略了外部使用方法
// 创建一个方法,返回内部使用的一些方法
getInternalHooks:()=>{
return {
setInitialValues: this.setInitialValues,
}
}
});
}
src/Form.tsx
export default React.forwardRef((props: any, ref) => {
const [formInstance] = useForm(form) as any;
const {
setInitialValues,
} = formInstance.getInternalHooks();
// 第一次渲染时 setInitialValues 第二个参数是true,表示初始化。以后每次渲染第二个参数都为false
const mountRef = useRef(null) as any;
setInitialValues(initialValues, !mountRef.current);
if (!mountRef.current) {
mountRef.current = true;
}
...
}
useRef
返回一个可变的 ref
对象,其 current
属性被初始化为传入的参数( initialValue
)。返回的 ref
对象在组件的整个生命周期内保持不变。
submit
在此之前,提交 submit
只能打印 store
里面的值,这并不能满足我们的需求,我们需要它可以回调指定函数。
src/useForm.ts
class FormStore {
private callbacks = {} as any; //用于存放回调方法
// 设置callbases
setCallbacks = (callbacks:any) => {
this.callbacks = callbacks;
}
// 暴露setCallbacks方法到全局
getForm = (): any => ({
...
getInternalHooks: () => {
return {
setInitialValues: this.setInitialValues,
setCallbacks: this.setCallbacks
};
},
});
// submit 时,去callbacks中取出需要回调方法执行
submit = () => {
const { onFinish } = this.callbacks;
onFinish(this.getFieldsValue())
};
}
src/Form.tsx
export default React.forwardRef((props: any, ref) => {
const { ..., onFinish, ...restProps } = props;
const [formInstance] = useForm(form) as any;
const {
setCallbacks,
} = formInstance.getInternalHooks();
// 获取外部传入的onFinish函数,注册到callbacks中,这样submit的时候就会执行它
setCallbacks({
onFinish
})
...
}
Field
shouldUpdate
通过 shouldUpdate
属性控制 Field
的更新逻辑。当 shouldUpdate
为方法时,表单的每次数值更新都会调用该方法,提供原先的值与当前的值以供你比较是否需要更新。
src/Field.tsx
export default class Field extends Component {
// 只改造这一个函数,根据传入的 shouldUpdate 函数的返回值来判断是否需要更新。
onStoreChange = (prevStore:any,curStore:any) => {
const { shouldUpdate } = this.props as any;
if (typeof shouldUpdate === 'function') {
if(shouldUpdate(prevStore,curStore)){
this.forceUpdate();
}
}else{
this.forceUpdate();
}
}
}
src/useForm.js
class FormStore {
// 之前写了一个registerField是用来设置Field实例的存储,再添加一个获取的方法
getFieldEntities = ()=>{
return this.fieldEntities;
}
// 新增一个方法,用来通知Field组件更新
notifyObservers = (prevStore:any) => {
this.getFieldEntities().forEach((entity: any) => {
const { onStoreChange } = entity;
onStoreChange(prevStore,this.getFieldsValue());
});
}
// 现在设置字段值之后直接调用 notifyObservers 方法进行更新组件
setFieldsValue = (curStore: any) => {
const prevStore = this.store;
if (curStore) {
this.store = setValues(this.store, curStore);
}
this.notifyObservers(prevStore);
};
}
好了更新的逻辑也差不多写完了,虽然并非跟原库保持一致(原库考虑了更多的边界条件),但是足矣帮助我们理解其思想。
表单验证
根据用户设置的校验规则,在提交表单时或者任何其他时候对表单进行校验并反馈错误。
读源码的时候发现,底层做校验使用的是 async-validator 做的。
async-validator
它是一个可以对数据进行异步校验的库, ant.design
与 Element ui
的 Form
组件都使用了它做底层校验。
安装
npm i async-validator
基本用法
import AsyncValidator from 'async-validator'
// 校验规则
const descriptor = {
username: [
{
required: true,
message: '请填写用户名'
},
{
pattern: /^\w{6}$/
message: '用户名长度为6'
}
]
}
// 根据校验规则构造一个 validator
const validator = new AsyncValidator(descriptor)
const data = {
username: 'username'
}
validator.validate(data).then(() => {
// 校验通过
}).catch(({ errors, fields }) => {
// 校验失败
});
关于 async-validator
详细使用方式可以查阅它的 github 文档。
Field 组件设置校验规则
<Field
label="Username"
name="username"
rules={[
{ required: true, message: 'Please input your username!' },
{ pattern: /^\w{6}$/ }
]}
>
<Input />
</Form.Item>
如果校验不通过,则执行 onFinishFailed
回调函数。
[注意] 原库还支持在 rules
中设置自定义校验函数,本组件中已省略。
组件改造
src/useForm.ts
class FormStore {
// 字段验证
validateFields = ()=>{
// 用来存放字段验证结果的promise
const promiseList:any = [];
// 遍历字段实例,调用Field组件的验证方法,获取返回的promise,同时push到promiseList中
this.getFieldEntities().forEach((field:any)=>{
const {name, rules} = field.props
if (!rules || !rules.length) {
return;
}
const promise = field.validateRules();
promiseList.push(
promise
.then(() => ({ name: name, errors: [] }))
.catch((errors:any) =>
Promise.reject({
name: name,
errors,
}),
),
);
})
// allPromiseFinish 是一个工具方法,处理 promiseList 列表为一个 promise
// 大致逻辑:promiseList 中只要有一个是 rejected 状态,那么输出的promise 就应该是 reject 状态
const summaryPromise = allPromiseFinish(promiseList);
const returnPromise = summaryPromise
.then(
() => {
return Promise.resolve(this.getFieldsValue());
},
)
.catch((results) => {
// 合并后的promise如果是reject状态就返回错误结果
const errorList = results.filter((result:any) => result && result.errors.length);
return Promise.reject({
values: this.getFieldsValue(),
errorFields: errorList
});
});
// 捕获错误
returnPromise.catch(e => e);
return returnPromise;
}
// 提交表单的时候进行调用字段验证方法,验证通过回调onFinish,验证失败回调onFinishFailed
submit = () => {
this.validateFields()
.then(values => {
const { onFinish } = this.callbacks;
if (onFinish) {
try {
onFinish(values);
} catch (err) {
console.error(err);
}
}
})
.catch(e => {
const { onFinishFailed } = this.callbacks;
if (onFinishFailed) {
onFinishFailed(e);
}
});
};
}
现在的核心问题就是 Field
组件如何根据 value
和 rules
去获取校验结果。
src/Field.tsx
export default class Field extends Component {
private validatePromise: Promise<string[]> | null = null
private errors: string[] = [];
// Field组件根据rules校验的函数
validateRules = ()=>{
const { getFieldValue } = this.context;
const { name } = this.props as any;
const currentValue = getFieldValue(name); // 获取到当前的value值
// async-validator 库的校验结果是 promise
const rootPromise = Promise.resolve().then(() => {
// 获取所有rules规则
let filteredRules = this.getRules();
// 获取执行校验的结果promise
const promise = this.executeValidate(name,currentValue,filteredRules);
promise
.catch(e => e)
.then((errors: string[] = []) => {
if (this.validatePromise === rootPromise) {
this.validatePromise = null;
this.errors = errors; // 存储校验结果信息
this.forceUpdate(); // 更新组件
}
});
return promise;
});
this.validatePromise = rootPromise;
return rootPromise;
}
// 获取 rules 校验结果
public getRules = () => {
const { rules = [] } = this.props as any;
return rules.map(
(rule:any) => {
if (typeof rule === 'function') {
return rule(this.context);
}
return rule;
},
);
};
// 执行规则校验
executeValidate = (namePath:any,value:any,rules:any)=>{
let summaryPromise: Promise<string[]>;
summaryPromise = new Promise(async (resolve, reject) => {
// 多个规则遍历校验,只要有其中一条规则校验失败,就直接不需要往下进行了。返回错误结果即可。
for (let i = 0; i < rules.length; i += 1) {
const errors = await this.validateRule(namePath, value, rules[i]);
if (errors.length) {
reject(errors);
return;
}
}
resolve([]);
});
return summaryPromise;
}
// 对单挑规则进行校验的方法
validateRule = async (name:any,value:any,rule:any)=>{
const cloneRule = { ...rule };
// 根据name以及校验规则生成一个校验对象
const validator = new RawAsyncValidator({
[name]: [cloneRule],
});
let result = [];
try {
// 把value值传入校验对象,进行校验,返回校验结果
await Promise.resolve(validator.validate({ [name]: value }));
}catch (e) {
if(e.errors){
result = e.errors.map((c:any)=>c.message)
}
}
return result;
}
}
到此为止我们就完成了一个简单的 Form
表单逻辑模块的编写。本文每小节的代码都可以在 github
上查看,而且在 dosc
目录下有相应的使用案例可以查看。
线上发布
发布到 npm
前面介绍过了,这个项目采用的是 dumi + father-builder
工具,因此在发布到 npm
这块是特别方便的,在登录 npm
之后,只需要执行 npm run release
即可。
线上包地址:lion-form
本地项目通过执行命令 npm i lion-form
即可使用。
发布组件库文档
1、配置 .umirc.ts
import { defineConfig } from 'dumi';
let BaseUrl = '/lion-form'; // 仓库的路径
export default defineConfig({
// 网站描述配置
mode: 'site',
title: 'lion form',
description: '前端组件开发。',
// 打包路径配置
base: BaseUrl,
publicPath: BaseUrl + '/', // 打包文件时,引入地址生成 BaseUrl/xxx.js
outputPath: 'docs-dist',
exportStatic: {}, // 对每隔路由输出html
dynamicImport: {}, // 动态导入
hash: true, //加hash配置,清除缓存
manifest: {
// 内部发布系统规定必须配置
fileName: 'manifest.json',
},
// 多国语顺序
locales: [
['en-US', 'English'],
['zh-CN', '中文'],
],
// 主题
theme: {
'@c-primary': '#16c35f',
},
});
配置完成后,执行 npm run deploy
命令。
2、设置 github pages
设置完成后,再次执行 npm run deploy
,即可访问线上组件库文档地址。
总结
本文从工程搭建,源码编写以及线上发布这几个步骤去描述如何完整的编写一个 React
通用组件库。
通过 Form
组件库的编写也让我们学习到:
Form
组件,Field
组件是通过一个全局的context
作为纽带关联起来的,它们共享FormStore
中的数据方法,非常类似redux
工作原理。- 通过把每个
Field
组件实例注册到全局的FormStore
中,实现了在任意位置调用Field
组件实例的属性和方法,这也是为什么Field
使用class
组件编写的原因(因为函数组件没有实例)。 - 最后也借助了
async-validator
实现了表单验证的功能。
学习优秀开源库的源码过程是不开心的,但是收获会是非常大的, Dont Worry Be Happy
。
今天的文章深入学习并手写 React Ant Design4 表单核心库 rc-field-form分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/20581.html