使用React从0实现一个Antd4 form表单

394 阅读10分钟

今天写(抄袭)一个 从0开始实现一个antd4 Form表单 的例子,这个是学完了 高老师的课程后的一个总结,只为了巩固自己学到的,代码地址。下面就正式开始吧

第一步、先搭建开发环境

(备注:可以不使用craco,直接使用create-react-app也可以)

1.使用create-react-app来创建一个项目,并安装craco

// 1.创建项目
npx create-react-app form-nut
cd form-nu
// 安装craco
yarn add antd @craco/craco craco-less

2.修改package.json

/* package.json */
"scripts": {
-   "start": "react-scripts start",
-   "build": "react-scripts build",
-   "test": "react-scripts test",
+   "start": "craco start",
+   "build": "craco build",
+   "test": "craco test",
}

3.创建craco.config.js文件,并配置

/* craco.config.js */
// * 配置完成后记得重启下
const CracoLessPlugin = require("craco-less");

module.exports = {
  babel: {
    //用来支持装饰器
    plugins: [["@babel/plugin-proposal-decorators", { legacy: true }]],
  },
  plugins: [
    {
      plugin: CracoLessPlugin,
      options: {
        lessLoaderOptions: {
          lessOptions: { javascriptEnabled: true },
        },
        modifyLessRule: function () {
          return {
            test: /\.less$/,
            exclude: /node_modules/,
            use: [
              { loader: "style-loader" },
              {
                loader: "css-loader",
                options: {
                  modules: {
                    localIdentName: "[local]_[hash:base64:6]",
                  },
                },
              },
              { loader: "less-loader" },
            ],
          };
        },
      },
    },
  ],
};

第二步、初步实现

1.首先看下项目的目录

image.png

第一个红框是我们放组件的地方
第二个红框是使用的页面 第三个红框是在App.js中引用

2. 初步实现

// ./pages/Home/index.js
import React from 'react';
import Form, { Field } from '../../component/my-form';
import Input from '../../component/Input';

function FormNut() {
  return (
    <Form>
      <Field>
        <Input />
      </Field>
      <Field>
        <Input />
      </Field>
    </Form>
  );
}

export default FormNut;
// ../../component/Input.js
import React from 'react';

const Input = (props) => {
  return <input {...props} />;
};

class CustomizeInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = {};
  }
  render() {
    const { value, ...otherProps } = this.props;
    return (
      <div style={{ padding: 10 }}>
        <Input style={{ outline: 'none' }} value={value} {...otherProps} />
      </div>
    );
  }
}

export default CustomizeInput;

// ../../component/my-form/index.js
import _Form from './Form.js';
import Field from './Field.js';

const Form = _Form;
Form.Field = Field;

export default Form;
export { Field };
// ../../component/my-form/Form.js
import React from 'react';

function Form(props) {
  const { children } = props;
  return <form>{children}</form>;
}

export default Form;

// ../../component/my-form/Field.js
import React from 'react';

class Field extends React.Component {
  getControlled = () => {
    return {
      value: '',
      onChange: (e) => {
        console.log();
      },
    };
  };
  render() {
    const { children } = this.props;
    // 这一步的作用是将this.getControlled()的属性赋给Field的children
    const returnChildNode = React.cloneElement(children, this.getControlled());
    return returnChildNode;
  }
}

export default Field;

以上分别是页面代码,Input组件代码,Form的index文件,Form.js文件, Field.js组件的代码,下面会根据这些来逐步实现antd4的基本功能

第三步、状态数据传递

实现数据传递时我们首先要保存数据,这里选择使用context来作为数据仓库保存数据。
在antd3中我们如果要使用form的方法只能在类组件中用一个高阶组件来将改form实例传递到类组件中,使用方式为:

@Form()
class Demo extends Component {
    ....
}

在antd4中使用了Form的hooks,useForm来生成实例,并将改form实例传递给组件,这样就可以函数组件中使用form实例的方法。
首先我们创建一个useForm的文件

// ../../component/my-form/useForm.js
import React from 'react';
class FormStore {
  constructor() {
    // 这个是保存状态值的
    this.store = {};
  }
  //   form中获取某个表单项数据
  getFieldValue = (name) => {
    return this.store[name];
  };
  //   form中获取所有表单项数据
  getFieldsValue = () => {
    return { ...this.store };
  };
  //   form中设置表单值
  setFieldsValue = (newStore) => {
    this.store = { ...this.store, ...newStore };
  };
  getForm() {
     // form暴露的方法
    return {
      getFieldValue: this.getFieldValue,
      getFieldsValue: this.getFieldsValue,
      setFieldsValue: this.setFieldsValue,
    };
  }
}

function useForm() {
  // 使用ref是可以保证ref实时更新保持一致
  const storeRef = React.useRef();
  //   判断ref是否创建,如果创建了则直接返回,否则新创建一个
  if (!storeRef.current) {
    const formStore = new FormStore();
    storeRef.current = formStore.getForm();
  }
  return [storeRef.current];
}

export default useForm;

修改../../conponent/my-form/index.js文件,将useForm按照antd4的导出方式导出

import _Form from './Form.js';
import Field from './Field.js';
import useForm from './useForm';

const Form = _Form;
Form.Field = Field;
Form.useForm = useForm;

export default Form;
export { Field, useForm };

然后在Form中使用,修改./pages/Home/index.js

import React from 'react';
import Form, { Field } from '../../component/my-form';
import Input from '../../component/Input';

function FormNut() {
  const [form] = Form.useForm();
  return (
    <Form form={form}>
      <Field>
        <Input />
      </Field>
      <Field>
        <Input />
      </Field>
      <button>submit</button>
    </Form>
  );
}

export default FormNut;

接下来我们要实现数据的传递,这里我们需要用到context,表单的状态值和属性通过context来传递。
首先在my-form组件库中新建一个存放context文件的地方,名字就起做FormContext.js吧,接下来我们在FormContext.js中创建context并导出

// @/conponent/my-form/FormContext.js
import React from 'react';

const FormContext = React.createContext();

export default FormContext;

接下来就是如何使用这个context了,首先要在Form.js中的children外包裹一层Provider,这样各个Field就可以通过context来获取form传进去的值了

// @/component/my-form/Form.js
import React from 'react';
import FormContext from './FormContext.js';

function Form(props) {
  const { children, form } = props;
  return (
    <form
      onSubmit={(e) => {
         // 阻止默认提交事件
        e.preventDefault();
      }}
    >
    // 将form放到context中,子组件通过context可以进行访问
      <FormContext.Provider value={form}>{children}</FormContext.Provider>
    </form>
  );
}

export default Form;

接下来在Field.js中使用消费context

import React from 'react';
import FormContext from './FormContext';

class Field extends React.Component {
// 消费context
  static contextType = FormContext;
  getControlled = () => {
    return {
      value: '',
      onChange: (e) => {
        console.log(e);
      },
    };
  };
  render() {
    const { children } = this.props;
    console.log(this.context, 'this.context');
    // 这一步的作用是将this.getControlled()的属性赋给Field的children
    const returnChildNode = React.cloneElement(children, this.getControlled());
    return returnChildNode;
  }
}
export default Field;

代码中写了之后,我们运行下代码,然后可以在控制台中看到打印的this.context信息如下:

image.png 这是useForm.js中调用useForm方法后返回的属性

由于当前Field.js代码中给form表单项赋值的方法还没有完善,所以我们还需要继续优化Field.js文件,可以让我们在文本框输入内容,并且更新formStore中的值。现在讲Field.js文件中的代码修改为下面:

import React from 'react';
import FormContext from './FormContext';

class Field extends React.Component {
  static contextType = FormContext;
  getControlled = () => {
    // name是表单项的name值
    const { name } = this.props;
    // 通过context我们可以拿到设置和获取store中的值,并将输入内容展示到表单上
    const { getFieldValue, setFieldsValue } = this.context;
    return {
      // 获取值
      value: getFieldValue(name),
      onChange: (e) => {
        // 修改值
        setFieldsValue({ [name]: e.target.value });
      },
    };
  };
  render() {
    const { children } = this.props;
    console.log(this.context, 'this.context');
    // 这一步的作用是将this.getControlled()的属性赋给Field的children
    const returnChildNode = React.cloneElement(children, this.getControlled());
    return returnChildNode;
  }
}

export default Field;

接下来我们尝试在form加载页面使用 form.setFieldsValue来给表单设置初始值

// ./pages/Home/index.js 
import React, { useEffect } from 'react';
import Form, { Field } from '../../component/my-form';
import Input from '../../component/Input';

function FormNut() {
  const [form] = Form.useForm();
  useEffect(() => {
    console.log(form, 'form');
    form.setFieldsValue({ name: 'default' });
  }, []);
  return (
    <Form form={form}>
      <Field name="name">
        <Input />
      </Field>
      <Field name="age">
        <Input />
      </Field>
      <button>submit</button>
    </Form>
  );
}

export default FormNut;

然后在useForm.js中的setFieldsValue方法中加入日志打印,打印表单的初始值store
之后我们运行会发现,在使用form.setFieldsValue赋初始值后,日志是打印了,但是页面并没有给出初始值,所以我们这里需要对对应的form表单组件更新一下。react组件更新的方式有四种:ReactDOM.renderforceUpdatesetState、以及因为父组件而更新。这里因为我们的filed组件是类组件,所以如果需要使form的子组件更新最好就是使用forceUpdate
要实现表单在调用setFieldsValue时组件更新我们需要在useForm中维护一个表单实例,里面放的是Form下的Field表单,然后我们修改useForm.js文件

// useForm.js
import { useRef } from 'react';

class FormStore {
  constructor() {
    this.store = {};
    this.fieldEntetities = [];
  }

  // 注册实例
  // 注册与取消实例
  //   订阅与取消订阅
  registerEntetities = (entity) => {
    this.fieldEntetities.push(entity);
    return () => {
      this.fieldEntetities = this.fieldEntetities.filter(
        (item) => item !== entity
      );
      delete this.store[entity.props.name];
    };
  };

  getFieldValue = (name) => {
    return this.store[name];
  };
  getFieldsValue = () => {
    return { ...this.store };
  };

  // set
  setFieldValue = (newStore) => {
    // 1. 更新store
    this.store = {
      ...this.store,
      ...newStore,
    };
    // 1.循环fieldEntetities
    this.fieldEntetities.forEach((entity) => {
      // 2.拿到表单更新项的key值
      Object.keys(newStore).forEach((item) => {
        // 3.判断是哪项表单要更新,仅更新当前的表单项,避免了所有的表单项更新
        if (item === entity.props.name) {
          entity.onStoreChange();
        }
      });
    });
  };

  // 暴露方法
  getForm = () => {
    return {
      getFieldValue: this.getFieldValue,
      getFieldsValue: this.getFieldsValue,
      setFieldValue: this.setFieldValue,
      registerEntetities: this.registerEntetities,
    };
  };
}

export default function useForm() {
  const formRef = useRef();
  if (!formRef.current) {
    const formStore = new FormStore();
    formRef.current = formStore.getForm();
  }
  return [formRef.current];
}

其中this.fieldEntetities就是我们保存实例的数组,我们在这里暴露一个方法registerEntetities,这个是注册Filed自组件的,就是Form下放了几个Field,就会在这里放几个Field实例。因为我们实现了组件挂载时注册实例,所以当页面卸载的情况下我们也要卸载对应的Field组件,在Filed.js中使用的方法如下

// Field.js
import React from 'react';
import FormContext from './FormContext';

class Field extends React.Component {
  static contextType = FormContext;

  componentDidMount() {
    // useForm中注册实例的方法
    const { registerEntetities } = this.context;
    // this代表的就是改filed的项,同时将返回值赋值给unregister方法,在卸载组件的生命周期中调用
    this.unregister = registerEntetities(this);
  }

  // 组件卸载时执行该方法,目的是将该实例从fieldEntetities中删除,同时删除store中保存的值
  componentWillUnmount() {
    this.unregister();
  }

  // 这个是给useForm.js中调用setFieldsValue方法时调用的
  onStoreChange = () => {
    this.forceUpdate();
  };

  getControlled = () => {
    // name是表单项的name值
    const { name } = this.props;
    // 通过context我们可以拿到设置和获取store中的值,并将输入内容展示到表单上
    const { getFieldValue, setFieldsValue } = this.context;
    return {
      // 获取值
      value: getFieldValue(name),
      onChange: (e) => {
        // 修改值
        setFieldsValue({ [name]: e.target.value });
      },
    };
  };
  render() {
    const { children } = this.props;
    // 这一步的作用是将this.getControlled()的属性赋给Field的children
    const returnChildNode = React.cloneElement(children, this.getControlled());
    return returnChildNode;
  }
}

export default Field;

现在我们试试就可以在form表单初始化的时候给赋值了,同时我们将每个实例放到一个数组中,每次改变仅仅更新那个改变的数组,其他的数组是不受影响的,不然如果表单里面内容太多的话,你更新一个表单项,所有的都会更新,这样使用感受就会很差,会感觉表单有些卡。接下来我们继续实现表单校验的功能

image.png

第四步、实现表单校验

接下来我们来实现表单校验的功能,就是在提交时,校验必填的表单是否已经填写,未填的打印出错误信息。
首先我们在页面调用form组件的代码中加入onFinishonFinishFailed两个事件,分别表示校验成功和校验失败时需要执行的函数

// index.js
import React, { useEffect } from 'react';
import Form, { Field } from '../../component/my-form';
import Input from '../../component/Input';

function FormNut() {
  const [form] = Form.useForm();
  useEffect(() => {
    console.log(form, 'form');
    form.setFieldsValue({ name: 'default' });
  }, []);
  // 表单校验成功执行的事件
  const onFinish = (value) => {
    console.log(value);
  };
  // 表单校验失败执行的事件
  const onFinishFailed = (err) => {
    console.log(err);
  };
  return (
    <Form form={form} onFinish={onFinish} onFinishFailed={onFinishFailed}>
      <Field name="name" rules={[{ required: true, message: '必填项' }]}>
        <Input />
      </Field>
      <Field name="age">
        <Input />
      </Field>
      <button>submit</button>
    </Form>
  );
}

export default FormNut;

接下来,我们在Form.js中继续完善,代码如下:

// Form.js
import React from 'react';
import FieldContext from './FieldContext';

export default function Form({ children, form, onFinish, onFinishFaild }) {
// 调用userForm中的方法,将两个放useForm中
  form.setCallbacks({
    onFinish,
    onFinishFaild,
  });
  return (
    <form
      onSubmit={(e) => {
        // 为了阻止form的默认事件,以避免每次提交的时候都会自动刷新页面
        e.preventDefault();
        // 提交事件,后面会补充
        form.submit();
      }}
    >
      <FieldContext.Provider value={form}>{children}</FieldContext.Provider>
    </form>
  );
}

上面的代码中添加了form.setCallbacks和提交的时的form.submit代码,这个分别时将form中传入的onFinish和onFinishFailed函数添加进useForm中以及提交的事件代码

下面我们在useForm.js中添加他们,useForm.js代码如下

// useForm.js
import React from 'react';
class FormStore {
  constructor() {
    // 这个是保存状态值的
    this.store = {};
    // 存放Field实例的地方
    this.fieldEntetities = [];
    this.callBack = {};
  }

  registerEntetities = (entity) => {
    this.fieldEntetities.push(entity);
    return () => {
      // 将卸载的field表单项从fieldEntetities中删除
      this.fieldEntetities = this.fieldEntetities.filter(
        (item) => item !== entity
      );
      // 删除store中该表单保存的值
      delete this.store[entity.props.name];
    };
  };

  // 将form的函数传入callback中,是在form文件中调用
  setCallbacks = (newCallback) => {
    this.callBack = { ...this.callBack, ...newCallback };
  };

  // 校验通过遍历所有表单项的实例,然后取出每个表单项的name和rules来进行判断,如果是必填且未填的则放到错误信息数组中,返回该数组
  validate = () => {
    const errorArr = [];
    this.fieldEntetities.forEach((entity) => {
      const { name, rules } = entity.props;
      const value = this.getFieldValue(name);
      if (rules && rules[0].required && !value) {
        errorArr.push({ [name]: rules[0].message || '必填项', value: value });
      }
    });
    return errorArr;
  };

  // 提交
  submit = () => {
    const { onFinish, onFinishFailed } = this.callBack;
    const errorArr = this.validate();
    if (errorArr.length) {
      console.error(errorArr);
      onFinishFailed(errorArr);
    } else {
      onFinish && onFinish(errorArr);
    }
  };

  //   form中获取某个表单项数据
  getFieldValue = (name) => {
    console.log(this.store[name], 'name');
    return this.store[name];
  };
  //   form中获取所有表单项数据
  getFieldsValue = () => {
    return { ...this.store };
  };
  //   form中设置表单值
  setFieldsValue = (newStore) => {
    this.store = { ...this.store, ...newStore };
    // 循环fieldEntetities
    this.fieldEntetities.forEach((entity) => {
      // 拿到改变的表单项value,然后将键名与表单项的实例的name对比,
      // 如果对上了就表明当前改动的是这个表单项,然后调用该表单项的onStoreChange方法,进行强制更新
      Object.keys(newStore).forEach((item) => {
        if (item === entity.props.name) {
          entity.onStoreChange();
        }
      });
    });
  };
  getForm() {
    return {
      getFieldValue: this.getFieldValue,
      getFieldsValue: this.getFieldsValue,
      setFieldsValue: this.setFieldsValue,
      registerEntetities: this.registerEntetities,
      setCallbacks: this.setCallbacks,
      submit: this.submit
    };
  }
}

function useForm() {
  // 使用ref是可以保证ref实时更新保持一致
  const storeRef = React.useRef();
  //   判断ref是否创建,如果创建了则直接返回,否则新创建一个
  if (!storeRef.current) {
    const formStore = new FormStore();
    storeRef.current = formStore.getForm();
  }
  return [storeRef.current];
}

export default useForm;

以上代码添加了this.callBack属性以及this.setCallBacks方法和this.validate以及this.submit方法,
其中this.callBack是存放传入form表单回调函数的,this.setCallBacks是存入的动作,this.validate是提交前对表单进行校验的,this.submit是提交动作
这是我们测试给表单设置必填项但是不填写时提交会发现,控制台打印出错误的log日志,如下:

image.png

总结

到这里就结束了,这个里面主要涵盖了状态管理,context,hook,自定义hook等知识点。不过这是我刚开始写的文章,有点乱。。。。可能过段时间我也看不懂了,哈哈哈哈