🎄概述
这个项目专注于实现企业核心的用户中心系统 是基于SpringBoot后端 + React前端的全栈项目 实现了用户注册、登录、查询等基础功能 这篇文章是第2部分开发记录 为前端部分
⚡前置文章
Java工程师 用户中心项目 Java后端实战
🎄项目瘦身 · 目录结构
现在观察项目目录 过于复杂 需要进行瘦身 移除目前不需要的组价
移除国际化支持 执行移除命令
i18n-remove
点击左侧绿色按钮
删除swagger、e2e文件夹
public
目录下的静态文件可以通过根目录/
直接访问到
🎄React基本的使用语法总结
🔮TSX文件
TSX是一种文件扩展名 代表TypeScriptXML 是一种特殊的TypeScript文件,用于开发React应用程序中的组件 TSX文件中可以同时编写JSX代码和TypeScript代码,🎯这有助于开发者在JavaScript语法中编写具有静态类型检查的React组件。JSX语法是在JavaScript代码中嵌入XML标签的一种方式,🎯它允许我们直接在JavaScript代码中编写类似HTML的标记 在编译过程中 TSX文件会被转换为标准的JavaScript文件(通常使用.js或.jsx扩展名)以供浏览器解析和执行 因此,TSX文件在React应用程序中发挥着关键的作用,它使得开发者可以更轻松地编写和维护可复用的UI组件
在.tsx文件中,我们通常会定义一个函数组件或类组件,并在这个组件的函数体或render方法中使用JSX语法来描述组件的UI结构 🔵return语句用于在函数组件或类组件中返回JSX元素,这些JSX元素描述了组件在屏幕上应该显示的内容和结构
例如,下面是一个简单的函数组件的例子:
import React from 'react'; function MyComponent() { 🔵return ( <div> <h1>Hello, World!</h1> <p>This is a React component.</p> </div> ); } export default MyComponent;
在这个例子中,return语句返回了一个包含
<div>
、<h1>
和<p>
等JSX元素的根元素。当MyComponent组件被渲染时,React会将这些JSX元素转换为对应的DOM元素,并将它们插入到页面中的相应位置。总结 .tsx文件中的return语句的含义是返回组件的UI结构,供React框架进行渲染
🍭import from
导入会使用到的组件
// 导入会使用到的组件
import {🎯DefaultFooter} from '@ant-design/pro-components';
const 😎Footer: React.FC = () => {
// 定义要在后面使用的变量 🔷defaultMessage 🔷currentYear
🔷const defaultMessage = '@BigBigMeng技术出品';
🔷const currentYear = new Date().getFullYear();
// 🍭return的含义是什么?return语句的含义是将组件的UI结构返回给React框架进行渲染
return (
<🎯DefaultFooter
// copyright和links 🤪目前猜测是DefaultFooter组件内部定义的变量 所以可以直接使用
// 至于copyright和links组件的位置呈现则由DefaultFooter内部定义的样式决定
copyright={`${🔷currentYear} ${🔷defaultMessage}`}
links={[
{
key: '我亦无他',
title: '我亦无他',
href: '#',
blankTarget: true,
},
{
key: '唯手熟尔',
title: '唯手熟尔',
href: '#',
blankTarget: true,
},
]}
/>
);
};
// 将这个组件导出 名为😎Footer 注意这个变量的名字和上面定义的一致
export default 😎Footer;
🍭export default NAME
绿色框中的
Footer
就是导出的组件
🍭对象解构
const{属性1,属性2}=某个对象; 这样解构以后 后面就可以直接使用属性1和属性2
const {status, type: loginType} = userLoginState;
🎄跨域设置
🔮跨域·前端正向代理
后端配置
context-path
:🎯
server:
port: 8888
servlet:
context-path: /api # 🎯
前端配置代理:(把下图的"userCenter"修改为"api")
编写
api.ts
文件 这个文件存放前端的应用编程接口(把下图的"userCenter"修改为"api")
看到
oneapi.json
文件 api这个字符串是系统自定义的 所以将之前的配置的userCenter全部修改为api
尝试请求 发现仍然携带了userCenter字符串
逐个排查哪个文件中有userCenter字符串并将其删除 最终发现是在
app.tsx
文件中 之前自己定义的
删除后重新执行 [HPM] Error occurred while trying to proxy request /api/user/login from localhost:8000 to http://locathost:8888 (ENOTFOUND) (https://nodejs.org/ api/errors.html#errors_common_system_errors)
发现被自己坑了💩 locathost🟥写错了 修改为localhost重新执行 还是存在以下问题
修改跨域配置 如下 添加
withCredentials: true
dev: {
// localhost:8000/api/** -> http://localhost:8888/api/**
'/api': {
// 正向代理 要代理的地址
target: 'http://localhost:8888',
withCredentials: true, // 配置存储Cookie
changeOrigin: true,
pathRewrite: { 'api': 'api' }, // 请求路径重写?
},
}
再次执行 浏览器存储了后端生成的Cookie
🔮跨域·后端配置
采用在后端解决跨域问题的方式 这个时候前端就不需要配置代理了 只需要添加一个访问前缀
后端配置类配置跨域支持 启动前后端进行访问
package bbm.com.config;
/**
@author Liu Xianmeng
@createTime 2023/9/26 10:40
@instruction 跨域配置
*/
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 允许跨域的路径
.allowedOrigins("*") // 允许跨域请求的源
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许请求的方法
.allowedHeaders("*") // 允许跨域请求包含的头部信息
.allowCredentials(true) // 是否发送Cookie
.maxAge(3600); // 预检请求的有效期,单位为秒
}};}}
请求错误
IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value “*” since that cannot be set on the “Access-Control-Allow-Origin” response header. To allow credentials to a set of origins, list them explicitly or consider using “allowedOriginPatterns” instead
解决办法:将
.allowedOrigins
🎯替换成.allowedOriginPatterns
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 允许跨域的路径
🎯.allowedOriginPatterns("*") // 允许跨域请求的源
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许请求的方法
.allowedHeaders("*") // 允许跨域请求包含的头部信息
.allowCredentials(true) // 是否发送Cookie
.maxAge(3600); // 预检请求的有效期,单位为秒
}};}}
重新启动 执行访问后端登录接口测试 测试成功✅
🎄进行前端功能开发
🔮登录
🍭第0阶段·数据追根溯源
从登录的原始出发点进行分析 当我们点击登录按钮的时候 前端的哪个js函数会被触发?
下面是登录页面的登录函数 这个函数调用了一个
login()
方法 这个login()方法从何而来?
文件的最上方显示这个login()方法是从
@/services/ant-design-pro/api
目录文件中引入的
打开
api.ts
文件 可以看到其中定义的login方法 这个login()方法使用到了一个变量API.LoginParams
这个登录参数变量很显然是要封装登录参数的
那这个
API.LoginParams
变量在哪个文件中进行定义的呢?
点进来查看:API命名空间定义在
typings.d.ts
文件文件下 除了定义有LoginParams
变量 还有其他的api.ts
文件之所以能只用API namespace
变量 是因为和typings.d.ts
文件在同一个文件夹下
typings.d.ts
文件还定义其他的很多ts变量:
到这为止 我们就大概了解了React + Ant Design Pro
框架的数据组织方式 后面 我们来使用这些已经定义好的一系列数据文件
🍭第1阶段 -> 修改前端数据
修改
typings.d.ts
数据 🎯LoginParams
declare namespace API {
type CurrentUser = {..};
type LoginResult = {..};
// 登录参数封装到变量LoginParams
type 🎯LoginParams = {
userAccount?: string; // 命名和后端保持一致
userPwd?: string; // 命名和后端保持一致
// autoLogin?: boolean; // 去掉
// type?: string; // 去掉
};
}
修改
typings.d.ts
数据 🎯添加一个和后端一致的消息返回实体ResponseEntity
type ResponseEntity = {
code?: number; // 返回状态码
message?: string; // 返回简要消息
map?: object; // 携带的数据对象
}
修改
typings.d.ts
数据 🎯修改当前用户CurrentUser
变量 和后端的User实体保持一致
// 只定义前端可以显示的数据项 比如userPwd就不需要定义了
type CurrentUser = {
userId?: number;
userNickname?: string;
userAccount?: string;
userAvatar?: string;
userGender?:number;
userPhone?: string;
userEmail?: string;
userStatus?: number;
userRole?: number;
userCode?: string;
userCreateTime?: Date;
};
❓❓❓修改过的这些type变量如何使用呢?看到下面的截屏代码:
❓❓❓上面的映射关系是在哪里定义的?在
oneapi.json
文件中定义的
修改
oneapi.json
文件
"components": {
"schemas": {
"CurrentUser": {...省略...},
"ResponseEntity": {
"type": "object",
"properties": {
"code": {
"type": "number"
},
"message": {
"type": "string"
},
"map": {
"type": "object"
}
}
},
"PageParams": {
"type": "object",
"properties": {
"userAccount": {
"type": "string"
},
"userPwd": {
"type": "string"
}
}
},
...省略...
}
}
🍭第1阶段 -> 执行测试
查看前端响应
查看后端响应 发现数据为空 说明两个问题:
(1)前端的请求已经被后端收到(2)前端的数据并没有传到后端
这个时候我发现自己在后端少写了一个注解 添加@RequestBody注解 以支持后端自动封装此对象 💩这个操作是必须的 否则后端无法识别封装
🍭第2阶段·检查前端请求·重新测试
发现传送的参数名不对 应该是
userAccount
和userPwd
才对 那问题在哪?😕😕😕
查看Login组件 发现是因为表单字段的name属性没有修改
修改name属性 和下面的
LoginParams
保持一致 同样也和后端用户字段保持一致
// 20230926 登录参数封装到变量LoginParams
type LoginParams = {
userAccount?: string; // 命名和后端保持一致
userPwd?: string; // 命名和后端保持一致
// autoLogin?: boolean; // 去掉
// type?: string; // 去掉
};
修改后重新测试 完美✨
🍭第3阶段·前端页面跳转
在上面的第2阶段执行完之后发现有一个404请求:
点击查看 是请求当前用户404 这个是意料之内的 因为在前后端我还没有处理这个逻辑 也就是后端返回登录成功的消息后 前端的处理
接下来查看前端对应的处理代码 首先看到登录的提交函数如下 其中
handleSubmit
函数在得到rst(就是前面定义的ResponseEntity)对象之后 执行了🎯🎯🎯fetchUserInfo()
函数 之所以登录后没有跳转 就是因为fetchUserInfo()
函数没有执行成功
/**
* 这是一个React函数组件 命名为Login 它使用了React的Hooks功能 具体使用了1️useState和2️useModel
*/
const Login: React.FC = () => {
... 代码省略
const 🎯🎯🎯fetchUserInfo = async () => {
⚡const userInfo = await initialState?.fetchUserInfo?.();
if (userInfo) {
await setInitialState((s) => ({
...s,
currentUser: userInfo,
}));
}
};
/**
* handleSubmit 是一个异步函数 用于处理提交的表单值
* 该函数接收一个名为values的参数 类型为API.LoginParams
*/
const handleSubmit = async (values: API.LoginParams) => {
try {
// 1️⃣ 首先 通过解构赋值将values对象的属性传递给login函数进行登录操作
// 并将返回结果存储在 rst 变量中
const rst = await login({...values});
// 2️⃣ 对rst执行判空操作
if (rst) {
// 显示提示 -> 是否登陆成功
message.success(rst.message);
// 🎯🎯🎯获取登录用户的信息
await fetchUserInfo();
/*
下面代码段的作用是 如果存在浏览器的历史记录对象(history)
则获取浏览器历史记录中的查询参数(query) 并从中提取重定向参数(redirect)
然后使用history.push方法将页面重定向到重定向参数所指定的地址
如果重定向参数不存在 则将页面重定向到根路径('/')
*/
// (1)首先判断浏览器历史记录对象(history)是否存在 如果不存在 则直接返回 不执行后续代码
if (!history) return;
// (2)如果存在浏览器历史记录对象(history) 则通过history.location获取当前页面的URL信息
const { query } = history.location;
// (3)从URL中的查询参数(query)中提取重定向参数(redirect)
const { redirect } = query as {
redirect: string;
};
// (4)使用history.push方法将页面重定向到重定向参数所指定的地址
// 如果重定向参数不存在 则将页面重定向到根路径('/')
history.push(redirect || '/');
return;
}
}
};
... 代码省略
}
接下来我么追查🎯
fetchUserInfo()
函数的代码行:⚡const userInfo = await initialState?.fetchUserInfo?.(); 继续追查initialState?.fetchUserInfo
方法的调用 这个方法定义在app.tsx
文件
继续追查
queryCurrentUser()
函数 看看具体的代码 到这里就明白了 🎯/api/currentUser
路径的后端接口还没有实现 所以出现前面的404 从而成功登录后得不到跳转
/** 获取当前的用户 GET /api/currentUser */
export async function currentUser(options?: { [key: string]: any }) {
return request<{
data: API.CurrentUser;
}>('/api/🎯currentUser', {
method: 'GET',
...(options || {}),
});
}
所以现在补充编写后端的
currentUser
的Controller接口实现 将后端Redis存储的用户信息的🎯KEY修改为ticket(之前的实现是KEY=userAccount:ticket) 以适配前端的/api/currentUser
请求路径
@RequestMapping(value = "/currentUser", method = RequestMethod.GET)
@ResponseBody
public User currentUser(@CookieValue String ticket) {
if(ticket == null) {
return null;
} else {
return getSafetyUser( (User) redisTemplate.opsForValue().🎯get(ticket) );
}
}
完成后 重启后端 执行前端的测试 报400错误
再看后端控制台信息 告诉我们前端没有携带登录凭证CooKie
这是因为前后端的跨域造成的 虽然已经配置了后端的跨域设置 前端也需要配置 才能在发送请求的时候携带对应
IP:Port
的Cookie
前端跨域配置补充 🎯withCredentials: true
export default {
dev: {
// localhost:8000/api/** -> http://localhost:8888/api/**
'/api': {
target: 'http://localhost:8888',
🎯withCredentials: true, // 配置存储Cookie
changeOrigin: true,
// 请求路径重写?如果将"api"重写为"api"则后端的 context-path='/api'
pathRewrite: { 'api': 'api' },
},
},
};
接下来注释Mock数据
重新登录 即可进入欢迎页面
🔮注册
🍭建立页面修改样式
有了前面的前端的登录的实现基础 接下来可以直接复制登录页面进行改造 开发注册页面
看源代码有没有登录按钮
点击查看引入的组件源码 在
LoginForm
组件的index.js
文件找到了 将其修改
查看效果
🍭编写注册接口·测试1
api.ts文件
/** 注册 POST /api/user/register */
export async function register(body: 🎯API.RegisterParams, options?: { [key: string]: any }) {
return request<🎯API.ResponseEntity>('/api/user/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
Register.index.tsx文件
const Register: React.FC = () => {
const [userRegisterState, setUserRegisterState] = useState<API.🎯ResponseEntity>({});
const [type, setType] = useState<string>('account');
const { initialState, setInitialState } = useModel('@@initialState');
const fetchUserInfo = async () => {
const userInfo = await initialState?.fetchUserInfo?.();
if (userInfo) {
await setInitialState((s) => ({
...s,
currentUser: userInfo,
}));
}
};
const handleSubmit = async (values: API.🎯RegisterParams) => {
try {
// 注册 rst是返回的结果
const rst = await register({...values});
if (rst) {
message.success(rst.message);
await fetchUserInfo();
...
return;
}
console.log(rst);
} catch (error) {
...
}
};
}
执行测试
查看后端
🍭修改后端·测试2
查看问题所在 -> Controller方法要加上@RequestBody注解 才能将UserRegister自动封装起来
重新执行测试 注册成功
把登录页面获取currentUser的操作去掉
最终注册页的代码
import Footer from '@/components/Footer';
import { register } from '@/services/ant-design-pro/api';
import {
LockOutlined,
UserOutlined,
} from '@ant-design/icons';
import {
LoginForm,
// ProFormCaptcha,
ProFormText,
} from '@ant-design/pro-components';
import { message, Tabs } from 'antd';
import React, { useState } from 'react';
import { history } from 'umi';
import styles from './index.less';
const Register: React.FC = () => {
const [type, setType] = useState<string>('account');
const handleSubmit = async (values: API.RegisterParams) => {
try {
const rst = await register({...values});
if (rst) {
message.success(rst.message);
if (!history) return;
const { query } = history.location;
const { redirect } = query as {
redirect: string;
};
history.push(redirect || '/');
return;
}
console.log(rst);
} catch (error) {
const defaultRegisterFailureMessage = '注册失败,请重试!';
message.error(defaultRegisterFailureMessage);
}
};
return (
<div className={styles.container}>
<div className={styles.content}>
<LoginForm
submitter={{
searchConfig : {
submitText : '注册'
}
}}
logo={<img alt="logo" src="https://bigmeng-bucket-100.oss-cn-qingdao.aliyuncs.com/bigmeng.png" />}
title="用户中心·注册"
subTitle={'实现企业必备用户中心后台管理方法'}
initialValues={{ autoLogin: true, }}
onFinish={async (values: API.RegisterParams) => {
await handleSubmit(values as API.RegisterParams);
}}
>
<Tabs activeKey={type} onChange={setType}>
<Tabs.TabPane key="account" tab={'使用账户密码注册'} />
{/*<Tabs.TabPane key="mobile" tab={'手机号注册'} />*/}
</Tabs>
{type === 'account' && (
<>
<ProFormText
name='userAccount'
initialValue={'bigbigmeng'} // 20230928 设置默认值
fieldProps={{
size: 'large',
prefix: <UserOutlined className={styles.prefixIcon} />,
}}
placeholder={'请输入账号'}
rules={[
{
required: true,
message: '用户名是必填项!',
},
]}
/>
<ProFormText.Password
name="userPwd"
initialValue={'bigbigmeng'} // 20230928 设置默认值
fieldProps={{
size: 'large',
prefix: <LockOutlined className={styles.prefixIcon} />,
}}
placeholder={'请输入密码'}
rules={[
{
required: true,
message: '密码是必填项!',
},
]}
/>
<ProFormText.Password
name="userRePwd"
initialValue={'bigbigmeng'} // 20230928 设置默认值
fieldProps={{
size: 'large',
prefix: <LockOutlined className={styles.prefixIcon} />,
}}
placeholder={'请输入密码'}
rules={[
{
required: true,
message: '确认密码是必填项!',
},
]}
/>
</>
)}
</LoginForm>
</div>
<Footer />
</div>
);
};
export default Register;
查看后端和数据库表结果
🍭总结
到此 注册功能也开发完毕~✨
🔮主页显示用户信息
🍭问题分析
🍭定位头像的组件位置
找组件发现找不到:
直接全局搜索avatar:
🍭修改·查看效果
修改字段名
查看前端效果
🎄进行用户管理页面的开发
🔮安装 AntDesignPro-Components
npm i @ant-design/pro-components --save
🔮导入管理页面
import { useRef } from 'react';
import type { ProColumns, ActionType } from '@ant-design/pro-table';
import ProTable, { TableDropdown } from '@ant-design/pro-table';
import { search } from "@/services/ant-design-pro/api";
import {Image} from "antd";
// 配置管理页面要显示的表头(用户信息)
const columns: ProColumns<API.CurrentUser>[] = [
{
dataIndex: 'userId',
valueType: 'indexBorder',
width: 48,
},
{
title: '用户账户',
dataIndex: 'userAccount',
copyable: true,
},
{
title: '头像',
dataIndex: 'userAvatar',
// 渲染用户头像
render: (_, record) => (
<div>
<Image src={record.userAvatar} width={80} />
</div>
),
},
{
title: '用户昵称',
dataIndex: 'userNickname',
copyable: true,
},
{
title: '性别',
dataIndex: 'userGender',
valueType: 'select',
valueEnum: {
1: { text: '男'},
0: { text: '女'},
},
},
{
title: '用户编号',
dataIndex: 'userCode',
},
{
title: '角色',
dataIndex: 'userRole',
valueType: 'select',
valueEnum: {
0: { text: '普通用户', status: 'Success' },
1: {
text: '管理员',
status: 'Warning',
},
},
},
{
title: '创建时间',
dataIndex: 'userCreateTime',
valueType: 'dateTime',
},
{
title: '操作',
valueType: 'option',
render: (text, record, _, action) => [
<a
key="editable"
onClick={() => {
// @ts-ignore
action?.startEditable?.(record.userId);
}}
>
编辑
</a>,
<a href={record.userAvatar} target="_blank" rel="noopener noreferrer" key="view">
查看
</a>,
<TableDropdown
key="actionGroup"
onSelect={() => action?.reload()}
menus={[
{ key: 'copy', name: '复制' },
{ key: 'delete', name: '删除' },
]}
/>,
],
},
];
// 导出页面
export default () => {
const actionRef = useRef<ActionType>();
return (
<ProTable<API.CurrentUser>
columns={columns}
actionRef={actionRef}
cardBordered
request = {async (params = {}, sort, filter) => {
console.log(sort, filter);
// ⚡⚡⚡获取后端的所有用户数据 ⚡⚡⚡search()函数的代码见后面的描述
const userList = await search();
return {
// 返回给页面要呈现的数据
data: userList.map.usersPageInfo.list
}
}}
editable={{
type: 'multiple',
}}
columnsState={{
persistenceKey: 'pro-table-singe-demos',
persistenceType: 'localStorage',
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
form={{
// 由于配置了 transform,提交的参与与定义的不同这里需要转化一下
syncToUrl: (values, type) => {
if (type === 'get') {
return {
...values,
created_at: [values.startTime, values.endTime],
};
}
return values;
},
}}
pagination={{
pageSize: 5,
}}
dateFormatter="string"
headerTitle="高级表格"
/>
);
};
🔮配置管理页面路由
{
path: '/admin',
name: '管理页',
icon: 'crown',
access: 'canAdmin', // 只有admin用户才能访问的路径
routes: [
🎯{ path: '/admin/userMng', name: '用户管理', icon: 'user', component: './manage/UserMng' }🎯,
{ path: '/admin/sub-page2', name: '商品管理', icon: 'smile', component: './Welcome' },
{ component: './404' },
],
},
⚡前端请求后端所有用户数据函数
暂时不考虑分页的问题
/**
* 根据查询条件 获取所有用户信息
* searchCondition:API.CurrentUser,
* pageRule:API.PageParams,
*/
export async function search(options? : {[key:string] : any}) {
console.log("api.ts -> search() -> 获取所有的用户信息")
return request<API.ResponseEntity>('/api/user/search', {
method: "POST",
...(options || {}),
})
}