表单在前端项目中广泛存在,特别在中后台管理系统中,表单繁多且逻辑复杂。遇到需要处理大量大型表单的项目,表单组件的选择就尤为重要,因为它决定着你是否能准时下班。
寻找了一番,没有发现简单易上手的,轻量并且性能上优越的表单组件。大部分的表单走的还是全表单更新路线,这也是造成性能差的最根本的原因。同时尝试过 uform,不可否认非常优秀,但是用起来就不是很顺手。
由于目前项目中使用的是 mobx 状态管理库,心里莫名的就想着何不使用 mobx 自己实现一个轻量的表单组件?mobx 正好具有优越的性能。
简介
前端算是已经全面推行 Typescript,该组件使用 Typescript 达到全面的类型检查
- 语言:
React、Typescript - 依赖:
mobx、mobx-react
表单模块:
-
Store
一方面管理表单所有的数据,包括表单数据,验证信息,显示隐藏信息,注册验证函数等;另一方面需要提供修改表单数据,修改校验错误,提交表单,重置表单等功能
-
Form 组件
提供
store的挂载场所,以及提供context -
Field
提供表单字段以及控件之间的绑定功能,字段校验等;这也是表单渲染的最小粒度。控件接入方式需要支持两种:
children为ReactElement,这类为通用简单的接入方式children为函数即renderProps, 可以接入任何复杂组件,适用于复杂场景
-
Consumer
由于表单是属于局部渲染,那么需要一种方式可以获取表单数据并且在数据变化时,局部重渲染
-
useForm
使表单支持
hooks写法
当然还有 FormProvider 可以用来集中处理多表单,使用场景不高。
大致实现
Store
Store 为一个标准的 mobx store, 提供数据,以及修改数据的方法,通过绑定 Field 组件实现局部渲染
export class Store<V> {
@observable readonly values: V;
@observable errors: FormErrors<V> = {};
@observable touched: FormTouched<V> = {};
@observable visible: FormVisible<V> = {};
......
setErrors = (errors: FormErrors<V>) => {...};
setVisible = (visible: FormVisible<V>) => {...}
setValues = <K extends keyof V>(values: Pick<V, K>, validate = true) => {...}
submit = async (validate = true): Promise<SubmitReturnType<V>> => {...}
reset = <K extends keyof V>(values?: Pick<V, K>) => {...}
......
}
Form
以下列出大致接口与实现,主要提供 store 挂载点,通过 context 为下级组件提供数据访问点
export interface FormProps<V> {
/**
* 表单初始值,当 store 传入为一个 Store 实例,则忽略该初始值,因为 Store 实例化时已经提供了初始值
*/
initialValue?: V;
/** 表单名字,配合 FormProvider 组件使用 */
name?: string;
/**
* initialValue 更改是否使用新值更新 UI
*/
enableReinitialize?: boolean;
/**
* 表单渲染之前的操作
*/
beforeRender?: (actions: FormActions<V>) => void;
/**
* 监听表单字段变化
*/
effect?: (name: keyof V, values: V, actions: FormActions<V>) => void;
/**
* Form 内部的状态从外部传入, 使用 useForm 时,需要将 useForm 返回的 store 赋予该字段
*/
store?: Store<V> | (new (values: V) => Store<V>);
}
export class Form<V> extends React.PureComponent<FormProps<V>> implements FormInstance<V> {
private readonly store: Store<V>;
constructor(props: FormProps<V>) {
super(props);
this.store = this.createStore();
......
}
......
get values(): Readonly<V> {
return this.store.values;
}
get errors(): Readonly<FormErrors<V>> {
return this.store.errors;
}
get visible(): Readonly<FormVisible<V>> {
return this.store.visible;
}
render() {
return <StoreContext.Provider value={this.store}>{this.props.children}</StoreContext.Provider>;
}
}
Field
该组件是一个比价复杂的组件,该组件不包含任何 UI,只提供数据,可以与任何 React 组件结合为需要的 FormItem,比如结合 antd FotmItem, 可参考文末 github 地址查看 demo。以下为该组件的部分核心接口
export interface FieldDescriptionProps {
/** 字段描述 */
label?: React.ReactNode;
/** 是否做必填校验 */
required?: boolean;
/** 自定义必填提示信息 */
requiredText?: string;
}
......
export interface FieldProps<V, K extends keyof V> extends FieldDescriptionProps {
/** 表单字段名称 */
name: K;
/** 绑定到的字段名的值改变了,就会重新 render 组件 */
bindNames?: string[];
/** 表单验证支持输入函数与正则, 输入正则情况,表单组件 onChange 必须返回 string 类型的值 */
validate?: Validate<V, K> | ValidateRegExp[];
/** 表单验证成功回调 */
validateSuccess?: (value: V[K]) => void;
/** 表单控件 */
children?: React.ReactNode | ((renderPropsConfig: RenderPropsConfig<V, K>) => React.ReactNode);
}
Consumer
该组件实现绑定特定数据并渲染 UI 功能
export interface ConsumerProps<V> {
/** names 包含的字段值变化后,重新渲染该组件,不填写该项默认所有字段变化都重渲染 */
bindNames?: Array<keyof V>;
children: (values: Readonly<V>) => React.ReactNode;
}
export class Consumer<V> extends React.PureComponent<ConsumerProps<V>> {
componentDidMount() {
// 特定条件,绑定的字段数据变化,那么更新该组件
if (......) {
this.forceUpdate();
}
}
......
render() {
// 将所有值传递为 children
return this.props.children(this.context.values);
}
}
useForm
提供 Store 以及各种表单操作方法
export interface UseFormReturn<V> {
form: FormInstance<V>;
/**
* 该值赋予 Form store 属性
*/
store: Store<V>;
}
/**
* Form hooks
* @param values 表单初始值
* @param effect 表单初始化后可以做一些操作
*/
export function useForm<V>(values: V | (() => V), effect?: (actions: FormActions<V>) => void): UseFormReturn<V> {
const store = React.useRef<Store<V>>();
const formInstance = React.useRef<FormInstance<V>>();
const isFirstRender = useIsFirstRender();
if (isFirstRender) {
......
}
return {
store: store.current!,
form: formInstance.current!,
};
}
基础案例
import * as React from "react";
import { Button } from "antd";
import { Form, FormActions, useForm } from "src/form";
import { Field } from "./antd-style/Field";
import { Input } from "./antd-style/Input";
interface FormState {
email: string;
password: string;
}
const defaultFormState: FormState = {
email: "",
password: "",
};
export const FormDemo = () => {
const { store, form } = useForm(defaultFormState);
const submit = async () => {
const result = await form.submit();
if (result.ok) {
// 表单提交
}
};
return (
<Form store={store}>
<Field name="email" label="邮箱" required>
<Input />
</Field>
<Field
name="password"
label="密码"
required
validate={[{ pattern: /\w{4,}/, message: "密码长度最少4位" }]}
>
<Input />
</Field>
<Button onClick={submit}>提交</Button>
</Form>
);
};