最近有一个很是复杂的表单需求,可能须要对表单作“任何事情”,现有的 UI
组件库选用的是 Ant Design
简称 antd
。它的 Form
表单已经帮咱们把“表单项校验”、“表单项错误信息”等常见操做所有封装好了。使用起来很是便捷。翻看了 antd Form
源码发现其核心能力都是经过 rc-field-form
库,提供出来的。所以阅读它的源码将是做者项目开始前必需要作的。
本文将模拟 rc-field-form
库,手写一个“学习版” ,深刻学习其思想。javascript
若是本文对你有所帮助,请点个👍 吧!html
rc-field-form
使用的是 Dumi
和 father-build
对组件库进行打包,为了保持一致,做者也将使用这两个工具来完成项目。前端
dumi
中文发音嘟米,是一款为组件开发场景而生的文档工具,与 father-builder
一块儿为开发者提供一站式的组件开发体验, father-builder
负责构建,而 dumi
负责组件开发及组件文档生成。
java
father-build
属于 father
(集文档与组件打包一体的库)的一部分,专一于组件打包。
node
使用 @umijs/create-dumi-lib
来初始化项目。这个脚手架整合了上面说起的两个工具。react
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
吧。
完整代码地址git
对于常用 react
开发的同窗来讲, antd
应该都不会陌生。开发中常常遇到的表单大多会使用 antd
中的 Form
系列组件完成,而 rc-field-form
又是 antd Form
的重要组成部分,或者说 antd Form
是对 rc-field-form
的进一步的封装。
想要学习它的源码,首先仍是得知道如何使用它,否则难以理解源码的一些深层次的含义。
github
首先来实现以下图所示的表单,相似于咱们写过的登陆注册页面。
代码示例:npm
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
实例也是整个库的核心。json
经过对 rc-field-form
源码的学习,咱们先来搭建一个基础框架。
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
实例的单例模式。
定义了全局 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;
复制代码
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> } 复制代码
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
方法,更新组件;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。
点击查看本小节代码
以前咱们都是这样去初始化表单的值:
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
只能打印 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
})
...
}
复制代码
经过 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 作的。
它是一个能够对数据进行异步校验的库, 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
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
目录下有相应的使用案例能够查看。
点击查看本小节代码
前面介绍过了,这个项目采用的是 dumi + father-builder
工具,所以在发布到 npm
这块是特别方便的,在登陆 npm
以后,只须要执行 npm run release
便可。
线上包地址:lion-form
本地项目经过执行命令 npm i lion-form
便可使用。
一、配置 .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
命令。
二、设置 github pages
设置完成后,再次执行 npm run deploy
,便可访问线上组件库文档地址。
本文从工程搭建,源码编写以及线上发布这几个步骤去描述如何完整的编写一个 React
通用组件库。
经过 Form
组件库的编写也让咱们学习到:
Form
组件, Field
组件是经过一个全局的 context
做为纽带关联起来的,它们共享 FormStore
中的数据方法,很是相似 redux
工做原理。Field
组件实例注册到全局的 FormStore
中,实现了在任意位置调用 Field
组件实例的属性和方法,这也是为何 Field
使用 class
组件编写的缘由(由于函数组件没有实例)。async-validator
实现了表单验证的功能。
学习优秀开源库的源码过程是不开心的,可是收获会是很是大的, Dont Worry Be Happy
。