大型表单性能差?撸一个组件解决它

1,528 阅读5分钟

表单在前端项目中广泛存在,特别在中后台管理系统中,表单繁多且逻辑复杂。遇到需要处理大量大型表单的项目,表单组件的选择就尤为重要,因为它决定着你是否能准时下班。

寻找了一番,没有发现简单易上手的,轻量并且性能上优越的表单组件。大部分的表单走的还是全表单更新路线,这也是造成性能差的最根本的原因。同时尝试过 uform,不可否认非常优秀,但是用起来就不是很顺手。

由于目前项目中使用的是 mobx 状态管理库,心里莫名的就想着何不使用 mobx 自己实现一个轻量的表单组件?mobx 正好具有优越的性能。

简介

前端算是已经全面推行 Typescript,该组件使用 Typescript 达到全面的类型检查

  • 语言:ReactTypescript
  • 依赖:mobxmobx-react

表单模块:

  1. Store

    一方面管理表单所有的数据,包括表单数据,验证信息,显示隐藏信息,注册验证函数等;另一方面需要提供修改表单数据,修改校验错误,提交表单,重置表单等功能

  2. Form 组件

    提供 store 的挂载场所,以及提供 context

  3. Field

    提供表单字段以及控件之间的绑定功能,字段校验等;这也是表单渲染的最小粒度。控件接入方式需要支持两种:

    1. childrenReactElement,这类为通用简单的接入方式
    2. children 为函数即 renderProps, 可以接入任何复杂组件,适用于复杂场景
  4. Consumer

    由于表单是属于局部渲染,那么需要一种方式可以获取表单数据并且在数据变化时,局部重渲染

  5. 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>
  );
};

github

github.com/amazecc/mob…