本文旨在记录React开发过程中通用表格的构造,本文的目的是为了手把手的教会React入门的同学构建自己的Form表单,也为了以后自己复用起来方便。如果你觉得不错或者有所收获的话,还望不惜赐赞!
1. Form表单组件架构搭建
0. 引入react依赖
tsx
import React from "react";
1. 引入antd组件库
tsx
import { Button, Form, Input } from "antd";
2. 引入@ant-design/icons图标
tsx
import { LockOutlined, UserOutlined } from "@ant-design/icons";
3. 引入axios发送表单数据
tsx
import axios from 'axios';
4. 引入css文件
tsx
import "./FormDemo.css";
5. 写函数式组件的Props接口
tsx
export interface FormProps {}
6. 创建函数式组件FormDemo并默认向外暴露出去
tsx
const FormDemo = (props: FormProps) => {
return (<></>)
}
export default FormDemo;
7. 创建Form表单组件的ref
tsx
const formRef = React.useRef<typeof Form>();
8. 管理提交按钮的状态
tsx
const [okState, setOkState] = React.useState(false);
9. 记录提交按钮的点击时间--节流用
tsx
const [lastOperationTime, setLastOperationTime] = React.useState(0);
10. 创建提交失败的回调函数--失败信息提示
tsx
const onSubmitFailed = React.useCallback(() => { }, [formRef] );
11. 创建表单内容发生变化的回调函数
tsx
const onFormValuesChange = React.useCallback((changeValues: any, values: any) => { }, []);
12. 创建提交表单的回调函数
tsx
const onFinish = React.useCallback((values: any) => { }, []);
13. 构造Form表单
tsx
return (
<div id="my-form">
<Form // 结构为Form -> Form.Item
ref={formRef} // 表单标签的句柄
name={"form-demo"} // 渲染到文档上的form标签的id是form-demo <form id="form-demo">...
layout={"vertical"} // 排列的方向是纵向放置的
style={{ width: "100%" }} // 充满包裹容器
size={"large"}
onFinish={onFinish} // submit发生的回调
onValuesChange={onFormValuesChange} // 表单中的内容发生改变的时候发生的回调
>
</Form>
</div>
)
14. 构造用户名表单元素:Form.Item>Input
tsx
{/* 用户名输入框 */}
<Form.Item
name={"userName"} // 用户名
label={null} // 要么使用label要么使用input的placeHolder
rules={[
{ required: true, message: 'Please input your username!' },
{ min: 3, message: 'Username must be at least 3 characters long!' },
{ pattern: /^[a-zA-Z0-9]+$/, message: 'Username can only contain letters and numbers!' },
]} // 校验规则
>
<Input
style={{
height: 40,
}}
prefix={<UserOutlined />} // input输入框前面的小图标,使用的是ant-design/icons中的图标
placeholder={"请输入用户名..."} // 文字占位符
/>
</Form.Item>
15. 构造输入密码表单元素:Form.Item>Input.Password
tsx
{/* 密码输入框 */}
<Form.Item
name={"password"}
label={null}
rules={[
{ required: true, message: 'Please input your password!' },
{ min: 3, message: 'Username must be at least 3 characters long!' },
{ pattern: /^[a-zA-Z0-9]+$/, message: 'Password can only contain letters and numbers!' },
]} // 校验规则
>
<Input.Password
style={{ height: 40 }}
prefix={<LockOutlined />} // input输入框前面的小图标
placeholder={"请输入密码..."} // 文字占位符
/>
</Form.Item>
16. 构造登录按钮:Form.Item>Button
tsx
{/* 确认登录按钮 */}
<Form.Item style={{ marginTop: 58 }}>
<Button
type={"primary"}
style={{ width: "100%", height: 40 }}
htmlType={"submit"} // 这个属性决定了此按钮被点击的时候,间接调用了onFinish方法
disabled={okState}
>
登录
</Button>
</Form.Item>
表单架构完整代码
tsx
import React from "react";
import { Button, Form, Input, FormInstance } from "antd";
import { LockOutlined, UserOutlined } from "@ant-design/icons";
import axios from 'axios';
import "./Form.css";
export interface FormProps { }
const FormDemo = (props: FormProps) => {
const formRef = React.useRef<FormInstance<any> | null>();
const [okState, setOkState] = React.useState(false);
const [lastOperationTime, setLastOperationTime] = React.useState(0);
const onSubmitFailed = React.useCallback(() => { }, [formRef]);
const onFormValuesChange = React.useCallback((changeValues: any, values: any) => { }, []);
const onFinish = React.useCallback((values: any) => { }, []);
return (
<div id="my-form">
<Form // 结构为Form -> Form.Item
ref={formRef} // 表单标签的句柄
name={"form-demo"} // 渲染到文档上的form标签的id是form-demo <form id="form-demo">...
layout={"vertical"} // 排列的方向是纵向放置的
style={{ width: "100%" }} // 充满包裹容器
size={"large"}
onFinish={onFinish} // submit发生的回调
onValuesChange={onFormValuesChange} // 表单中的内容发生改变的时候发生的回调
>
{/* 用户名输入框 */}
<Form.Item
name={"userName"} // 用户名
label={null} // 要么使用label要么使用input的placeHolder
rules={[
{ required: true, message: 'Please input your username!' },
{ min: 3, message: 'Username must be at least 3 characters long!' },
{ pattern: /^[a-zA-Z0-9]+$/, message: 'Username can only contain letters and numbers!' },
]} // 校验规则
>
<Input
style={{
height: 40,
}}
prefix={<UserOutlined />} // input输入框前面的小图标,使用的是ant-design/icons中的图标
placeholder={"请输入用户名..."} // 文字占位符
/>
</Form.Item>
{/* 密码输入框 */}
<Form.Item
name={"password"}
label={null}
rules={[
{ required: true, message: 'Please input your password!' },
{ min: 3, message: 'Username must be at least 3 characters long!' },
{ pattern: /^[a-zA-Z0-9]+$/, message: 'Password can only contain letters and numbers!' },
]} // 校验规则
>
<Input.Password
style={{ height: 40 }}
prefix={<LockOutlined />} // input输入框前面的小图标
placeholder={"请输入密码..."} // 文字占位符
/>
</Form.Item>
{/* 确认登录按钮 */}
<Form.Item style={{ marginTop: 58 }}>
<Button
type={"primary"}
style={{ width: "100%", height: 40 }}
htmlType={"submit"} // 这个属性决定了此按钮被点击的时候,间接调用了onFinish方法
>
登录
</Button>
</Form.Item>
</Form>
</div>
)
}
export default FormDemo;
2. Form表单组件细节构造
2.1 父组件调用此表单组件的时候应该传入一个回调函数,此回调函数接受一个布尔值state作为入参;state表示当前表单的内容是否符合rules, 通过这个回调函数可以将当前表单是否满足预置规则传递出去。
tsx
// 补充接口的内容
export interface FormProps {
formStateChangeCallback: (state: boolean) => void;
}
...
// 补充从props中解析出回调
const { formStateChangeCallback } = props;
...
// 表单的内容发生变化的时候,先校验,根据校验的结果修改状态,然后通过回调函数传递到父组件中
const onFormValuesChange = React.useCallback((changeValues: any, values: any) => {
formRef.current?.validateFields()
.then(values => {
// 这种情况下表示校验通过了
formStateChangeCallback(true);
})
.catch(errors => {
// 这种也算是校验通过了
if(!errors.errorFields.length) {
formStateChangeCallback(true);
} else {
formStateChangeCallback(false);
}
})
}, [formStateChangeCallback, formRef]);
2.2 当表单的内容发生变化的时候,管理提交按钮的状态的变量okState的值也应该发生变化
tsx
// 表单内容改变回调
const onFormValuesChange = React.useCallback((changeValues: any, values: any) => {
formRef.current?.validateFields()
.then(values => {
// 这种情况下表示校验通过了
formStateChangeCallback(true);
setOkState(true);
})
.catch(errors => {
// 这种也算是校验通过了
if(!errors.errorFields.length) {
formStateChangeCallback(true);
setOkState(true);
} else {
formStateChangeCallback(false);
setOkState(false);
}
})
}, [formStateChangeCallback, formRef]);
...
// 使用okState控制提交按钮的状态
{/* 确认登录按钮 */}
<Form.Item style={{ marginTop: 58 }}>
<Button
type={"primary"}
style={{ width: "100%", height: 40 }}
htmlType={"submit"} // 这个属性决定了此按钮被点击的时候,间接调用了onFinish方法
disabled={!okState}
>
登录
</Button>
</Form.Item>
2.3 解决formRef类型报错的问题
tsx
// 引入类型
import { FormInstance } from 'antd/lib/form';
...
// 修改useRef的初值
const formRef = React.useRef<FormInstance>(null);
2.4 构造表单提交失败的回调函数
此例中,如果表单能够提交,则说明输入是符合规则要求的;如果提交失败了,则证明提交到后端的数据被服务器拒绝了,这种情况只有一种可能,那就是:密码或者用户名错误:
tsx
const onSubmitFailed = React.useCallback(
() => {
// 使用Form实例对象的setFields方法可以根据name和对应的FormItem关联起来
if (formRef?.current) formRef.current.setFields([
{
name: "password",
errors: ["userName or password is wrong!"],
},
]);
}, [formRef]
);
2.5 构造发送http请求的函数
发送给后端服务器使用的是axios,发送的内容除了表单数据还有操作日志
tsx
// 日志接口
type SubmitLog = {
topic: string;
decs: string;
user: string;
timer: number;
}
...
// 表单数据发送函数 -- 注意此方法需要在组件开发完毕之后分离出去
const login = (formData:Record<PropertyKey, any>, operationLog:SubmitLog) => {
const postData = {
...formData,
operationLog,
};
const response = axios.post('http://localhost:6666/login', postData, {
headers: {
'Content-Type': 'application/json',
},
});
return response;
}
其中,formData是表单数据,而operationLog则是根据提交时候的环境生成的操作日志,操作日志中具有时间戳等信息。
2.6 对表单的密码进行加密--使用第三方库crypto-js完成
tsx
// 引入crypto-js
import CryptoJS from "crypto-js";
// 字符串加密函数
// 密码加密函数 -- 注意此方法需要在组件开发完毕之后分离出去
const encrypt = (content: string) => {
if (!content) {
content = "";
}
const sKey = CryptoJS.enc.Utf8.parse("crypto5c870991230ad");
const iv = CryptoJS.enc.Utf8.parse("cryptoa3ebc56458ff7");
const rawBytes = CryptoJS.enc.Utf8.parse(content);
const encrypted = CryptoJS.AES.encrypt(rawBytes, sKey, {
iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.toString();
}
2.7 构件函数,对服务端返回的信息做持久化存储
tsx
// 持久化存储函数 -- 注意此方法需要在组件开发完毕之后分离出去
const localDataStorage = (data4Storage: any) => void data4Storage.forEach(
(pair: any) => void localStorage.setItem(pair.key, pair.value || "")
)
2.8 对登录操作做节流--防止登陆操作频繁触发
对登录事件做节流的本质是对onFinish函数做节流,实现原理就是记录每一次onFinish调用的时间,等到下次调用的时候比较当前时间和记录时间,如果小于gap值则不执行。
tsx
const onFinish = React.useCallback((values: any) => {
// 对于提交事件做代码上的节流,规定一秒之内最多点击一次
const _now = Date.now();
const _gap = _now - lastOperationTime;
if ( _gap < 1000) return;
// 更新操作时间
setLastOperationTime(_now);
}, [lastOperationTime]);
2.9 完善提交表单的回调函数onFinish
- 对密码进行加密:调用encrypt方法即可
- 构造提交日志:话题、描述、用户名、时间戳
- 发送请求:调用login方法即可
- 处理请求返回值:1. 如果服务器返回失败的结果,需要调用onSubmitFailed方法将失败结果反馈给表示层; 2.如果提交成功,需要调用父组件可能传入的回调函数。
2.9.1 密码进行加密
tsx
// 使用encrypt方法对密码进行加密
values.password = values.password ? encrypt(values.password) : "";
2.9.2 构造提交日志
tsx
// 构造登录日志,登录日志作为http请求报文的一部分发送给服务器,作为服务器更新状态的依据
const submitLog = {
topic: "login",
desc: "login",
user: values.userName,
time: _now,
};
2.9.3 登录
tsx
login({ ...values }, submitLog)
2.9.4 提交成功之后执行父组件的回调
tsx
// 更新接口
export interface FormProps {
formStateChangeCallback: (state: boolean) => void;
callback?: (params?:any) => void;
}
...
// 从props解析出来
const { formStateChangeCallback, callback } = props;
...
// 提交成功之后调用
// 调用账户登录方法,方法返回promise对象,根据promise对象的状态可以判断出登录是否成功
login({ ...values }, submitLog)
.then(({data}) => {
const { code, result: { data: { lastLoginTime, token } } = { data: { lastLoginTime: _now, token: '' } } } = data;
if (code === 200) {
// 执行父组件可能传递的登陆成功的回调
callback?.();
} else {
throw new Error();
}
})
.catch((e) => {
// 走到这一步说明http发送失败了,这种情况下直接表示登录失败,调用登录失败处理方法
onLoginFailed();
});
2.9.4 解析服务器返回的数据
tsx
login({ ...values }, submitLog)
.then(({data}) => {
const { code, result: { data: { lastLoginTime, token } } = { data: { lastLoginTime: _now, token: '' } } } = data;
if (code === 200) {
// 登录成功之后需要将一些信息做本地化存储
localDataStorage([
{ key: 'LOGIN_TIME', value: lastLoginTime },
{ key: 'LOCAL_TOKEN_KEY', value: token },
{ key: 'USER_NAME', value: values.userName || "" },
]);
// 执行父组件可能传递的登陆成功的回调
callback.();
} else {
throw new Error();
}
})
.catch((e) => {
// 走到这一步说明http发送失败了,这种情况下直接表示登录失败,调用登录失败处理方法
onLoginFailed();
});
onFinish的全部代码
tsx
const onFinish = React.useCallback((values: any) => {
// 对于提交事件做代码上的节流,规定一秒之内最多点击一次
const _now = Date.now();
const _gap = _now - lastOperationTime;
if ( _gap < 1000) return;
// 更新操作时间
setLastOperationTime(_now);
// 使用encrypt方法对密码进行加密
values.password = values.password ? encrypt(values.password) : "";
// 构造登录日志,登录日志作为http请求报文的一部分发送给服务器,作为服务器更新状态的依据
const submitLog = {
topic: "login",
desc: "login",
user: values.userName as string,
time: _now,
};
// 调用账户登录方法,方法返回promise对象,根据promise对象的状态可以判断出登录是否成功
login({ ...values }, submitLog)
.then(({data}) => {
const { code, result: { data: { lastLoginTime, token } } = { data: { lastLoginTime: _now, token: '' } } } = data;
if (code === 200) {
// 登录成功之后需要将一些信息做本地化存储
localDataStorage([
{ key: 'LOGIN_TIME', value: lastLoginTime },
{ key: 'LOCAL_TOKEN_KEY', value: token },
{ key: 'USER_NAME', value: values.userName || "" },
]);
// 使用前端路由跳转至主页面
// navigate("/home/main");
// 执行父组件可能传递的登陆成功的回调
callback(true);
} else {
throw new Error();
}
})
.catch((e) => {
// 走到这一步说明http发送失败了,这种情况下直接表示登录失败,调用登录失败处理方法
onSubmitFailed();
});
}, [lastOperationTime, callback, onSubmitFailed]);
2.10 设置一些样式
// 在Form.css中。
css
#my-form{
width: 300px;
height: 450px;
overflow: hidden;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
margin: auto;
padding: 180px 20px 0 20px;
border: 1px solid grey;
}
3. Form表单的全部代码
tsx
import React from "react";
import { Button, Form, Input } from "antd";
import { LockOutlined, UserOutlined } from "@ant-design/icons";
import { FormInstance } from 'antd/lib/form';
import axios from 'axios';
import CryptoJS from "crypto-js"; // @4.1.2
import "./Form.css";
export interface FormProps {
formStateChangeCallback: (state: boolean) => void;
callback: (params: any) => void;
}
type SubmitLog = {
topic: string;
desc: string;
user: string;
time: number;
}
const encrypt = (content: string) => {
if (!content) {
content = "";
}
const sKey = CryptoJS.enc.Utf8.parse("crypto5c870991230ad");
const iv = CryptoJS.enc.Utf8.parse("cryptoa3ebc56458ff7");
const rawBytes = CryptoJS.enc.Utf8.parse(content);
const encrypted = CryptoJS.AES.encrypt(rawBytes, sKey, {
iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.toString();
}
// 表单数据发送函数 -- 注意此方法需要在组件开发完毕之后分离出去
const login = (formData: Record<PropertyKey, any>, operationLog: SubmitLog) => {
const postData = {
...formData,
operationLog,
};
const response = axios.post('http://localhost:6666/login', postData, {
headers: {
'Content-Type': 'application/json',
},
});
return response;
}
const localDataStorage = (data4Storage: any) => void data4Storage.forEach(
(pair: any) => void localStorage.setItem(pair.key, pair.value || "")
)
const FormDemo = (props: FormProps) => {
const { formStateChangeCallback, callback } = props;
const formRef = React.useRef<FormInstance>(null);
const [okState, setOkState] = React.useState(false);
const [lastOperationTime, setLastOperationTime] = React.useState(0);
const onSubmitFailed = React.useCallback(
() => {
// 使用Form实例对象的setFields方法可以根据name和对应的FormItem关联起来
if (formRef?.current) formRef.current.setFields([
{
name: "password",
errors: ["userName or password is wrong!"],
},
]);
}, [formRef]
);
// 表单内容改变回调
const onFormValuesChange = React.useCallback((changeValues: any, values: any) => {
formRef.current?.validateFields()
.then(values => {
// 这种情况下表示校验通过了
formStateChangeCallback(true);
setOkState(true);
})
.catch(errors => {
// 这种也算是校验通过了
if(!errors.errorFields.length) {
formStateChangeCallback(true);
setOkState(true);
} else {
formStateChangeCallback(false);
setOkState(false);
}
})
}, [formStateChangeCallback, formRef]);
// 表单submit回调函数
const onFinish = React.useCallback((values: any) => {
// 对于提交事件做代码上的节流,规定一秒之内最多点击一次
const _now = Date.now();
const _gap = _now - lastOperationTime;
if ( _gap < 1000) return;
// 更新操作时间
setLastOperationTime(_now);
// 使用encrypt方法对密码进行加密
values.password = values.password ? encrypt(values.password) : "";
// 构造登录日志,登录日志作为http请求报文的一部分发送给服务器,作为服务器更新状态的依据
const submitLog = {
topic: "login",
desc: "login",
user: values.userName as string,
time: _now,
};
// 调用账户登录方法,方法返回promise对象,根据promise对象的状态可以判断出登录是否成功
login({ ...values }, submitLog)
.then(({data}) => {
const { code, result: { data: { lastLoginTime, token } } = { data: { lastLoginTime: _now, token: '' } } } = data;
if (code === 200) {
// 登录成功之后需要将一些信息做本地化存储
localDataStorage([
{ key: 'LOGIN_TIME', value: lastLoginTime },
{ key: 'LOCAL_TOKEN_KEY', value: token },
{ key: 'USER_NAME', value: values.userName || "" },
]);
// 使用前端路由跳转至主页面
// navigate("/home/main");
// 执行父组件可能传递的登陆成功的回调
callback(true);
} else {
throw new Error();
}
})
.catch((e) => {
// 走到这一步说明http发送失败了,这种情况下直接表示登录失败,调用登录失败处理方法
onSubmitFailed();
});
}, [lastOperationTime, callback, onSubmitFailed]);
return (
<div id="my-form">
<Form // 结构为Form -> Form.Item
ref={formRef} // 表单标签的句柄
name={"form-demo"} // 渲染到文档上的form标签的id是form-demo <form id="form-demo">...
layout={"vertical"} // 排列的方向是纵向放置的
style={{ width: "100%" }} // 充满包裹容器
size={"large"}
onFinish={onFinish} // submit发生的回调
onValuesChange={onFormValuesChange} // 表单中的内容发生改变的时候发生的回调
>
{/* 用户名输入框 */}
<Form.Item
name={"userName"} // 用户名
label={null} // 要么使用label要么使用input的placeHolder
rules={[
{ required: true, message: 'Please input your username!' },
{ min: 3, message: 'Username must be at least 3 characters long!' },
{ pattern: /^[a-zA-Z0-9]+$/, message: 'Username can only contain letters and numbers!' },
]} // 校验规则
>
<Input
style={{
height: 40,
}}
prefix={<UserOutlined />} // input输入框前面的小图标,使用的是ant-design/icons中的图标
placeholder={"请输入用户名..."} // 文字占位符
/>
</Form.Item>
{/* 密码输入框 */}
<Form.Item
name={"password"}
label={null}
rules={[
{ required: true, message: 'Please input your password!' },
{ min: 3, message: 'Username must be at least 3 characters long!' },
{ pattern: /^[a-zA-Z0-9]+$/, message: 'Password can only contain letters and numbers!' },
]} // 校验规则
>
<Input.Password
style={{ height: 40 }}
prefix={<LockOutlined />} // input输入框前面的小图标
placeholder={"请输入密码..."} // 文字占位符
/>
</Form.Item>
{/* 确认登录按钮 */}
<Form.Item style={{ marginTop: 58 }}>
<Button
type={"primary"}
style={{ width: "100%", height: 40 }}
htmlType={"submit"} // 这个属性决定了此按钮被点击的时候,间接调用了onFinish方法
disabled={!okState}
>
登录
</Button>
</Form.Item>
</Form>
</div>
)
}
export default FormDemo;
4. 使用express写一个后端验证
js
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json()); // 使用内置的 JSON 解析中间件
app.post('/login', (req, res) => {
const lastLoginTime = new Date().getTime().toString();
const token = Math.random().toString(16);
const responseData = {
code: 200,
result: {
data: {
lastLoginTime,
token,
},
},
};
res.json(responseData);
});
const port = 6666;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
5. 后续优化
5.1 国际化
tsx
import { useIntl } from "react-intl";
import { SOME_KEY} from "@/locales/I18nKeys";
const intl = useIntl();
intl.formatMessage({ id: SOME_KEY} }),
5.2 前端路由跳转
tsx
import { useNavigate } from "react-router-dom";
const navigate = useNavigate();
navigate("/home");