Da-API平台-项目开发
项目开发
前端:ant design pro脚手架进行开发
后端:基于springboot-init通用后台模板进行开发
增删改查、登录功能实现
前端接口调用:后端使用遵循 openapi 的规范的 swagger 文档,使用前端 Ant Design Pro 框架集成的 oneapi 插件自动生成。
基础配置参考:(nodejs大于16.14小于18)、idea下载2020.1版本以上(需使用MyBatis X插件)
前端(yarn、node、npm安装)
【1】nodejs安装配置
【2】webstorm安装配置(或VSCode)
后端
【1】idea安装配置
【2】Java安装配置
【3】maven安装配置
其他工具
【1】mysql
【2】redis
【4】git
前端项目
1.项目构建配置
基础配置
【1】项目初始化
构建前端项目,搭建基于react的ant design pro项目脚手架
# 使用 npm 加载脚手架
npm i @ant-design/pro-cli -g
# 创建前端项目
pro create api-platform-frontend
# 选择umi版本(可以尝试umi4踩新坑,umi3提示选择简单还是全量脚手架),原使用umi3 simple
# 进入指定项目安装项目依赖
cd api-platform-frontend
tyarn 或者 npm install
# 脚手架初始完成,进入开发状态
npm run start
# 项目启动完成访问http://localhost:8000/,第一次初始化加载会比较慢
查看package.json文件,对照相应的启动命令看其配置,如果执行npm run dev
命令本质上执行的是start:dev,可以看其指令配置中MOCK=none,由于此时还没有接入后台数据接口,因此需要借助MOCK模拟数据接口实现访问
这个时候去访问前端页面是存在错误的(根据提示输入用户名密码)
重启项目npm run start
访问,然后登录访问主页(start以模拟数据方式运行,dev命令禁用了MOCK调用自己定义的后端)
【2】项目瘦身
框架本身生成的东西有很多,但实际上一些小型项目中不需要引用这么复杂的内容,为了优化代码结构,此处通过清理项目一些可能不会用到的东西给项目瘦身
(1)国际化移除:/src/locales
国际化:集成了多种语言:中文、英文... ,考虑项目主要在国内访问,去除国际化配置;如果要使用国际化,还要去配置不同语言的引用
在package.json文件中找到i18n-remove
,这里提供了移除国际化的脚本( "i18n-remove": "pro i18n-remove --locale=zh-CN --write",),执行指令pro i18n-remove --locale=zh-CN --write
, 如果指令执行失败,可以参考官方提供的issue解决方案
执行完成可以看到其移除了一些配置文件,但locales文件需手动删除,完成清理后重启项目检查是否正常运行
重启项目报错,参考issue解决方案(移除国际化出现SyntaxError: Export 'SelectLang' is not defined. (11:55))
解决上述问题再次重启尝试,确认主页右上角的语言切换已被移除
(2)移除测试工具
删除工程下的test包:
2.模块功能开发
后端项目
1.项目构建配置
项目引入
【1】引入springboot-init(通用后台模板):新建api-platform-backend文件夹,引入springboot-init代码文件
【2】File->Settings配置项目依赖仓库(如果出现maven依赖引入错误则确认是否要指定版本号,在maven官网中选择一个比较多人下载使用的版本进行配置)
maven路径配置:C:/custom/develop/CONFIG/maven/apache-maven-3.5.2
maven仓库配置:E:\workspace\maven-repo\repository\idea-repo\idea-repo-setting.xml、E:\workspace\maven-repo\repository\idea-repo\repo-data
配置完成刷新项目maven,检查项目是否正常引入maven配置
【3】修改项目属性为当前要开发的项目
重命名项目(原有项目名称为springboot-init)
借助idea对项目进行重命名,随后修改pom.xml文件中的springboot-init关键字为自己的项目名称(ctrl+shift+R全局搜索)
如果配置的description属性相应修改为项目介绍或说明
配置完成重新刷新maven配置即可
插件引入
【1】下载markdown插件:通过idea软件提供的方式下载(File->Settings->plugins->输入markdown下载插件)、或者通过外部安装导入(直接安装)
【2】下载mybatis x插件:File->Settings->plugins->输入mybatis x下载插件
【3】下载GenerateAllSetter插件,可以一键生成对象的所有get、set方法,生成一个充满假数据的类、充满假数据的对象(引入完成将光标放在对应实体,随后按快捷键Alt+Enter,选择要生成的代码)
项目配置:mysql数据库配置
【1】idea连接本地数据库(Database选项卡连接本地数据库即可)
如果没有配置数据库,则还需进一步配置数据源
【2】模板默认数据库名称为my_db,选中my_db(按ctrl+shift+R全局搜索,eclipse版快捷键为ctrl+H)替换为自己的数据库名
【3】运行数据库文件,创建数据表
【4】修改数据库配置application.yml
项目配置:redis数据库
【1】修改redis配置,在application中配置了redis seesion,如果没有使用到redis需要将session的store-type改为none(即存储会话不用redis存储,如果指定了redis此处需为redis)
(使用了redis需要将本地redis开启,在redis安装目录中启动redis-server.exe)
项目启动
【1】配置完成启动项目,访问http://localhost:8101/api/doc.html#/home,则可看到swagger构建的后台管理页面
【2】通过接口进行访问测试
2.模块功能开发
【1】数据库设计
(1)设计概念
任何系统实际上都可以抽象成管理系统。那么对于这个系统,最基本的功能就是接口管理。
-- 接口信息
create table if not exists `interface_info`
(
`id` bigint not null auto_increment comment '主键' primary key,
`name` varchar(256) not null comment '名称',
`description` varchar(256) null comment '描述',
`url` varchar(512) not null comment '接口地址',
`requestHeader` text null comment '请求头',
`responseHeader` text null comment '响应头',
`status` int default 0 not null comment '接口状态(0-关闭,1-开启)',
`method` varchar(256) not null comment '请求类型',
`userId` bigint not null comment '创建人',
`createTime` datetime default CURRENT_TIMESTAMP not null comment '创建时间',
`updateTime` datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
`isDelete` tinyint default 0 not null comment '是否删除(0-未删, 1-已删)'
) comment '接口信息';
(2)如何生成模拟数据?
借助SQL之父,自动生成SQL语句和模拟数据
在开发新项目时,通常需要编写 SQL 语句来创建数据库表并手动添加假数据进行测试。然而,随着项目的增多和表格的复杂度,手动编写和添加数据的成本越来越高,也越来越浪费时间。SQL 之父可以自动生成 SQL 语句和模拟数据,从而显著提高开发效率,让开发者专注于业务逻辑的实现。
可以修改通用字段,字段采用驼峰式,让前后端一致
(3)如何确认接口信息需要什么字段?
推荐方案:去查看实际接口调用情况,参考swagger提供的访问页面,查看请求信息,分析接口信息
设计好数据库,借助SQL之父,然后一键生成相应的数据表(这里程序有个小 bug,把 CURRENT_TIMESTAMP 两边的单引号去掉,否则可能建表出错),随后将生成的SQL语句执行
-- 接口信息
create table if not exists `interface_info`
(
`id` bigint not null auto_increment comment '主键' primary key,
`name` varchar(256) not null comment '名称',
`description` varchar(256) null comment '描述',
`url` varchar(512) not null comment '接口地址',
`requestHeader` text null comment '请求头',
`responseHeader` text null comment '响应头',
`status` int default 0 not null comment '接口状态(0-关闭,1-开启)',
`method` varchar(256) not null comment '请求类型',
`userId` bigint not null comment '创建人',
`createTime` datetime default CURRENT_TIMESTAMP not null comment '创建时间',
`updateTime` datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
`isDelete` tinyint default 0 not null comment '是否删除(0-未删, 1-已删)'
) comment '接口信息';
# 模拟生成数据
insert into `interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('刘笑愚', '吴琪', 'www.wesley-trantow.com', '万鸿煊', '王越彬', 0, '黄雪松', 26826);
【2】后端业务模块开发-接口信息管理
(1)mybatis x插件生成业务代码(构建后台MVC层)
选择指定的表,右键选择Mybatis-X Generator
如果点击完成没反应就回退上一步再次尝试(可能mybatis x插件有点小问题)
查看生成代码位置(generator文件夹),下一步迁移生成的代码,将代码放置在对应的目录,此处需注意Mapper.xml文件应存放在resource文件夹下(相应修改xml文件内namespace对照到其mapper接口文件,默认生成在resource/mapper文件夹),且对应引入包名原来为genetor也要联动修改替换为自己的路径,进而构建完成entity、dao、service层。迁移完成删除generator,随后修改引用(包名不同导致报错)
构建controller层:复制现有的controller实现(参考PostController实现),全局替换注意关键字不要被替换掉。
此处预留了请求参数校验,相应的要对xxxRequest进行配置,例如InterfaceInfoAddRequest、InterfaceInfoUpdateRequest、InterfaceInfoQueryRequest、InterfaceInfoEditRequest,对应参照post中的请求参数校验,在model/dto下常见interfaceinfo文件夹,将post文件夹中的XXXRequest作为参考复制进去(对应修改名称)
找到Interfaceinfo对应的实体(此处需注意逻辑删除,需要在实体类isDelete属性中添加@TableLogic注解),将这些字段全部复制到对应的xxxRequest(后期再根据业务需求进行调整即可)
根据每个xxxRequest结合业务场景增删字段,真正定义需要的字段。参考InterfaceinfoAddRequest(一些不需要业务输入的字段是不需要定义的)
以此类推,查询Request、更新Request则分析用户会根据什么字段查询,一步步操作,完成xxxRequest请求参数实体类的定义
随后再查看controller中飘红信息,按照既定思路添加缺失的方法接口即可(类似地参考PostService实现),相应修改InterfaceInfoService添加
编写校验接口的逻辑,完善其他接口实现,启动后台项目测试接口,接口调通说明业务功能模块构建完成,一些业务细节则根据实际需求进行调整即可,随后进一步构建前端页面
扩展问题:❓思考为什么要定义不同的request请求参数实体
访问不同接口请求校验的参数不同,原有应用开发可能为了贪方便直接用实体来替代所谓的xxxRequest,但是在前后端交互的时候就需要额外通过文档或者其他方式让前端知道哪些接口要传哪些参数、哪些接口不需要传哪些参数,通过这种方式反而增加沟通成本。
但如果通过设定既定规则明确指定请求参数,借助swagger生成的接口文档则体现其重要作用,前端可以知道本次请求具体需要调用什么参数,主要是为了屏蔽某些接口交互前端不需要关注的字段、避免一些重要字段暴露在前端,要理解通用实体的两面性。例如userId、isDel这些参数尤其注意)
所以基于上述情况一般建议接口对应请求参数要一对一,除非一些特别通用的场景(例如根据id删除,这个时候就只需要一个id,则可定义DeleteRequest作为请求参数进行操作即可,归根结底还是代码编写规范习惯)(还有一种比较坏的习惯就是通过继承的方式继承所有字段,这种方式无异于将实体类的字段全部暴露出去,不推荐)
想象一下,如果只用一个通用对象处理所有事情,当前端开发人员查看你的接口文档时,他们应该如何知道哪些参数是必需的,哪些参数是可选的?你可能需要额外添加备注来说明这一点。而且,如果有些参数并不需要前端提交,如userid、id等,你还需要把它们包含在文档中吗?这可能会造成前端开发人员的困扰。另一个问题是,如果你在对象中公开了一些字段,而开发者又没有留意到,就可能导致问题。例如,如果你公开了 userid,而且在编码时没有注意,如果用户提交了一个 userid,然后这个 userid 被无意中写入数据库,导致用户控制了这个 userid,这将是一个问题。 如果你觉得写三个对象很麻烦,那你可以按照自己的开发习惯来。然而,从规范的角度出发,建议为每个接口创建一个单独的对象,除非这个对象是特别通用的。例如,对于只需要一个 ID 参数的删除请求,你可以将其转换为通用的。总的来说,通用的时候就使用通用对象,不通用的时候就不要强行通用。
【3】前端业务模块开发-接口信息管理
(1)前端模板项目介绍
.umi | 框架自动生成的隐藏文件(不用管) |
compoents | 业务通用组件 |
pages | 业务页面入口和常用模板 |
services | 后台接口服务 |
app.tsx | 整个Ant Design Pro框架的主要文件 |
.editorconfig .elintignore .eslintrc.js .prettierignore .prettierc.js | eslint、prettier插件配置用于保证前端代码规范 webstorm中使用ctrl+alt+L可以美化代码 |
其中,app.ts 是 Ant Design Pro 的核心入口文件,其中的 getInitialState 函数用于在首次打开页面时加载某些用户信息。例如,用户信息等。除非是登录页面,否则它会自动跳转,这都是由 Ant Design Pro 预先编写的逻辑,可以无需过分关注
美化配置启用
以webstore为例,File->Settings->
(2)前端模块快速开发
目前项目开发最先需要解决的问题是如何让前端调用后台接口,实现接口信息的增删改查管理。通常,会在前端定义 TS 类型对象,并手动编写调用后台的方法,例如获取当前登录用户、退出登录等。 Ant Design Pro 框架已经支持自动化生成这些接口,利用 openapi 插件实现接口的自动生成。
思考问题:如果后端已经定义了各种接口,前端如何自动生成相应的接口调用代码呢?需要一个文档或者介质来同步这些信息。这就是需要使用 openapi 规范的地方。只要为 oneapi 插件提供基于其规范的文档,就可以实现接口的自动生成。
什么是 openapi 呢?简单地说,openapi 是一种接口文档的规范,可以理解为接口文档的格式或者规则。举个例子,常用的 Swagger 这种后端接口文档,就是遵循了 openapi 规范。
参考swagger接口文档,可以查看文档主页信息,访问路径:http://localhost:8101/api/v2/api-docs
可以看到这些
上述为v2版本,查看v3版本对应json可以看到基于openapi规范生成的文档(此处需注意版本问题,原有项目默认swagger生成文档为v2版本,参考为v3版本,尝试踩坑看是否存在一些隐形的问题)
(3)前端借助openapi快速构建
修改openapi配置
查看config.ts文件,删除多余内容,修改配置
修改完成,点击package.json,找到openapi指令运行:(终端输入yarn run openapi)
启动完成提示生成service文件,在service目录下多出了一个api-platform目录,里面自动生成了调用后端指定接口的方法代码
从上述文件中可以看到已经自动生成了接口调用代码,不需要手动额外编辑(此处也进一步验证了上面为什么后端接口要封装不同的request请求参数,主要还是基于统一规范,提升开发效率)
测试接口请求(修改请求配置)
【1】调整requestErrorConfig.ts文件为requestConfig.ts文件(并将errorConfig修改为requestConfig),并修改相关引用
【2】修改app.tsx中对requestConfig.ts的引用(即fix错误引用信息)
此处需注意app.tsx中引用了相关文件并且套了原有的errorConfig对象,为了简化代码结构,此处可以直接在requestConfig.ts文件中进行配置(本质上app.tsx就是引入requestConfig.ts的配置)
# app.tsx
import { requestConfig } from './requestConfig';
export const request = {
...requestConfig,
};
# requestConfig.ts文件添加请求后台接口url配置
export const requestConfig: RequestConfig = {
// 后台数据交互url定义
baseURL:'http://localhost:8101',
....
}
前后端请求对接
用dev模式开启(yarn run dev),启动项目登录校验(如果登录出错则检查调用接口是否正常),出现404请求是因为请求接口地址不对(请求接口地址是脚手架自动生成的,需要手动配置修改)
【1】修改后端请求接口,index.tsx文件,修改handleSubmit方法实现
【2】此处需注意要将原有接口调用实现调整为自定义接口调用内容:
请求接口调整为调用上面openapi生成的接口请求(现成方法),即修改请求方法和响应判断
# 修改后参考代码
// 1.引入自定义登录接口API
import { userLoginUsingPost } from '@/services/api-platform-backend/userController';
// 2.处理请求调用方法
const handleSubmit = async (values: API.UserLoginRequest) => {
try {
// 调用 userLoginUsingPOST 方法进行用户登录,values 为包含登录信息(如用户名和密码)的对象
const res = await userLoginUsingPost({
...values,
});
// 检查返回的 res 对象中是否包含 data 属性,如果包含则表示登录成功
if (res.data) {
// 创建一个新的 URL 对象,并获取当前 window.location.href 的查询参数
const urlParams = new URL(window.location.href).searchParams;
// 将用户重定向到 'redirect' 参数指定的 URL,如果 'redirect' 参数不存在,则重定向到首页 ('/')
history.push(urlParams.get('redirect') || '/');
// 用登录用户的数据更新初始状态
setInitialState({
loginUser: res.data
});
return;
}
// 如果抛出异常
} catch (error) {
// 定义默认的登录失败消息
const defaultLoginFailureMessage = '登录失败,请重试!';
// 在控制台打印出错误
console.log(error);
// 使用 message 组件显示错误信息
message.error(defaultLoginFailureMessage);
}
};
上述代码修改完成,但还有部分细节待调整(实际业务代码开发过程应该按照提示一步步进行操作,不求一步到位,而是一步步完善功能,确定好开发思路再去做下一步),修改完成定位飘红错误,将index.tsx中的原始框架的东西调整为自身项目要引入配置的内容
调整1:原API.LoginParams调整为API.UserRegisterRequest(即此处要传入的参数应该为请求注册实体)
// 修正前
actions={['其他登录方式 :', <ActionIcons key="icons" />]}
onFinish={async (values) => {
await handleSubmit(values as API.LoginParams);
}}
// 修正后
actions={['其他登录方式 :', <ActionIcons key="icons" />]}
onFinish={async (values) => {
await handleSubmit(values as API.UserRegisterRequest);
}}
调整2:用户表单值调整:原框架自动生成的表单数据值和自身项目的实体name不匹配则需要进行调整
(例如此处的用户名密码修改对应name属性为后台定义的userAccount、userPassword)
调整3:登录用户全局状态
用户登录完成,前端从后台加载用户信息后,会将数据保存到全局状态中。默认脚手架提供并未保存到全局状态,所以要进行相应的修改。
(1)修改项目根目录下的typing.d.ts
文件,定义全局状态类型(InitialState),保存用户状态(当成全局变量)
/**
* 全局状态类型定义
*/
interface InitialState{
// 登录用户信息
loginUser?:API.UserVO
}
(2)修改app.tsx的getInitialState 的方法
app.tsx的getInitialState 的方法:每当首次访问页面时,就会被执行用以获取用户信息和当前的全局状态
这段代码的实现是,当页面首次加载的时候,获取全局要保存的数据(此处为上面定义的用户登录信息/用户登录状态),随后执行用户信息获取操作(参考下面fetchUserInfo的实现用于获取当前登录用户信息)
修改fetchUserInfo方法实现,调用后台接口获取登录用户信息(类似地需要import引入接口方法getLoginUserUsingGet,随后执行)
// 1.引入获取用户登录信息接口
import { getLoginUserUsingGet } from '@/services/api-platform-backend/userController';
// 2.调整Promise要获取的全局变量,调用方法获取用户信息
export async function getInitialState(): Promise<InitialState> {
// 当页面首次加载时,获取要全局保存的数据,比如用户登录信息
const state: InitialState = {
// 初始化登录用户的状态,初始值设为undefined
loginUser: undefined,
}
try {
// 调用getLoginUserUsingGET()函数,尝试获取当前已经登录的用户信息
const res = await getLoginUserUsingGet();
// 如果从后端获取的数据不为空,就把获取到的用户数据赋值给state.loginUser
if (res.data) {
state.loginUser = res.data;
}
// 如果在获取用户信息的过程中发生错误,就把页面重定向到登录页面
} catch (error) {
history.push(loginPath);
}
// 返回修改后的状态
return state;
};
清理无关的代码块:结合上述流程可以实现用户信息状态保存操作,因此此处清理脚手架原有的实现
最终修改的内容为:
(3)修改前端页面登录信息展示
上述操作处理完成,用户登录后前端保存登录状态,随后将这个登录状态信息显示在页面,因此需要修改用户登录信息的真正参数
(4)测试相应:yarn run dev模式启动项目测试登录接口
至此,登录接口完成验证,但可能会出现一个问题:当启动项目登录页面的时候,发现点击登录按钮需要点击登录两次才能进去?
这个问题是由于 React 组件更新的异步性质引起的。在调用 setInitialState 后,状态可能并没有立即更新,而又立即执行了 history.push,试图导航到一个依赖于新状态的页面。这就导致了你需要点击两次登录按钮才能看到预期的页面。
🚀解决方案1:确保 setInitialState 在 history.push 之前完成: 尝试使用 async/await 来确保 setInitialState 在 history.push 之前完成。在 index.tsx 中的 handleSubmit 修改为以下形式
这个方法进入登录页之后得等上 5 秒左右,然后才能点击登录,点击一次就可以;如果我们访问后一出现登录页就点击登录,就还需要点击两次登录,问题并没有完全解决;这里是因为 setInitialState 函数的执行需要等待一段时间,或者函数内部有某些延迟操作,导致 setInitialState 函数并没有立即将状态更新,直接加个 setTimeOut 延迟 100ms,确保 setInitialState 在 history.push 之前完成。(最直接的操作就是直接设定延迟,以确保状态保存在跳转页面之前完成)
(5)完成上述操作,已经基本打通前后端调用逻辑,此处还需调整一些细节问题即可
(4)交互细节调整
登录状态失效
基于上述操作,发现登录之后刷新页面后又提示跳转重新登录,说明前端没有设定cookies,进而导致登录状态失效
一般情况下如何保存这个状态:当成功登录后,后端会在前端设置一个 cookie。设立 cookie 后,每当前端页面刷新时,它会执行什么操作呢?类似之前提到的 app.tsx 文件吗?每次页面首次加载时,它都会执行 getInitialState 函数。所以,当首次加载页面的时候,这个方法就会被调用以获取当前登录用户的信息。获取到信息后,我们只需将其设置到全局变量中,初始化变量即可。
解决方案:此处只需要相应修改requestConfig.ts文件配置(设定请求响应的withCredentials属性)
export const requestConfig: RequestConfig = {
// 后台数据交互url定义
baseURL:'http://localhost:8101',
// 设定跨域请求是否要携带cookies
withCredentials:true,
...
}
完成设置之后再次尝试访问,则可以看到页面正常
头像加载
页面主页头像数据加载:相应调整页面信息即可(头像url可以指定图床图片数据、一些登录用户信息相应调整为自定的参数配置即可)
- 头像加载(对应匹配数据库的user_avatar)
- 用户信息展示:修改AvatarDropdown.tsx文件,将currentUser信息修改为loginUser、name修改为userName(注意是用户的name属性修改,不要全部替换错其他内容)
这里有一个调试的小问题需注意,在调整的过程中发现就算输入了正确的图片路径图片还是无法加载,一步步排查代码,一开始考虑是否为登录信息没有正确拿到(可以打印或者alter数据校验),找到loading组件,排查错误,分别检查用户登录状态是否正常
测试后发现原来校验用户登录信息的时候指定的userName为null(初始化信息的时候没有指定用户名称),导致此处loading组件一直加载
衍生一个小细节:为什么要让前后端做数据校验?因为在测试接口的时候可能会比较随意模拟一些数据生成,虽然接口调用成功,但是如果组合业务场景就容易暴露出问题,类似此处基础数据一些核心字段没有数据,就会导致前端在进行数据校验的时候拿不到数据而限制了某些动作,因此在开发过程中一定要注意细节
注销接口交互
修改AvatarDropdown.tsx内容,搜索loginOut()方法实现,将默认的loginOut方法修改为后端自己的方法(调用后端接口userLogoutUsingPost)
# 1.引入接口方法
import { userLogoutUsingPost } from '@/services/api-platform-backend/userController';
# 2.修改loginOut()方法调用为userLogoutUsingPost(调用后端接口),并实现页面跳转(登录注销调用后台接口响应成功,随后跳转到登录页面)
const onMenuClick = useCallback(
(event: MenuInfo) => {
const { key } = event;
if (key === 'logout') {
flushSync(() => {
setInitialState((s) => ({ ...s, loginUser: undefined }));
});
// 调用后台接口实现登录注销
userLogoutUsingPost();
// 跳转到登录界面
const { search, pathname } = window.location;
const redirect = pathname + search;
history.replace('/user/login', { redirect })
return;
}
history.push(`/account/${key}`);
},
# 3.删除原有的loginOut()方法
调整完成,测试登录注销,注销完成跳转到登录页面
(5)接口信息管理模块开发(InterceInfo/index.tsx)
模块开发:做一个管理员可以控制的接口信息管理模块
开发思路:首先,要了解前端是怎么区分权限的。基于上述版本,登录后会发现原本有三个菜单栏,但是管理员菜单消失了。其原因就在于前端进行了权限校验。找到access.ts文件,这个是 Ant Design Pro 内置的一套权限管理机制。
调整说明:修改currentUser为loginUser,随后数据库修改登录用户的权限(userRole属性)即可,只要登录用户后台的权限为admin则认定其为管理员(一般情况下这个是需要后台管理员进行维护的,而不是手动修改)
# 修改access.ts文件
export default function access(initialState: InitialState | undefined) {
const { loginUser } = initialState ?? {};
return {
canUser: loginUser,
// 如果loginUser存在,并且用户角色为 'admin',说明该用户是管理员
canAdmin: loginUser?.userRole === 'admin',
};
}
权限校验实现分析
可以选择脚手架生成的TableList去分析,打开pages/TableList/index.tsx文件,查看其实现有handleAdd、handleUpdate、handleRemove(分别对应增删改)
即当点击对应按钮,它会自动触发调用相应的方法执行
继续查阅:const TableList: React.FC = () => {}
的定义,可以看到这个表格用到Ant Design Pro components 的 ProTable 组件
基于上述思路,可以先从前端页面开发入手,将现有的前端页面改造为自己想要的接口信息管理页,最简单的就是先将页面调整为信息展示(先不考虑其他触发功能实现,后续再一步步进行完善)
接口信息查询
# 1.修改原const columns: ProColumns<API.RuleList>[]的一些表头
const columns: ProColumns<API.InterfaceInfo>[],相应配置表头信息
# 2.查看前端页面有【查询表格】按钮,其调用接口请求后台数据调整为后台定义的接口
分析这个 request 会在什么时候触发请求?
当刚打开页面或刚加载表格时;
是你手动点击刷新按钮时;
当点击查询按钮时;
也就是说这个请求函数何时被调用,完全由这个组件来管理。你只需要设定请求函数如何调用,以及应该请求哪个后端接口即可。而不再需要反复绑定和编写事件处理程序。
此处后端响应数据正常,但是前端却无法正常响应,因此考虑是后端响应和前端接收处理的时候出了问题,点击request查看其要求的参数规则
已经获取到了数据,但是为何会报错呢?如果在替换接口后出现错误,可能的原因是什么?如果遇到这种情况,会如何进行排查呢?
首先,需要确定你的请求参数与 request 的请求参数是否一致。其次,响应值是否与 request 的响应值相匹配。因此,建议尽量避免完全替换。
# 参考代码实现
// 1.引入接口信息管理相关API
import { listInterfaceInfoByPageUsingPost } from '@/services/api-platform-backend/interfaceInfoController';
// 2.修改调用API接口
// 原脚手架默认调用API接口
// request={rule}
// request={listInterfaceInfoByPageUsingPost} // 直接调用的话无法渲染,因为响应数据交互不匹配
// 根据request规则,重新编写请求和响应处理
request={async (params, sort: Record<string, SortOrder>, filter: Record<string, React.ReactText[] | null>) => {
const res = await listInterfaceInfoByPageUsingPost({
...params
})
if (res?.data) {
return {
data: res?.data.records || [],
success: true,
total: res.total,
}
}
}}
重新刷新页面,数据正常显示
至此已初步完成前后端交互构建的基本内容,后续需进一步优化操作即可
优化前端页面
【1】删除无用的页面信息(欢迎页、管理页等)
- 先清理路由:router.ts文件修改(config/router.ts)
- 重命名接口管理目录:原TableList用作InterfaceInfo进行模块开发,需重命名
- ❌优化接口管理页:一些飘红报错信息可能不影响实际系统运行,结合实际情况调整即可(InterfaceInfo模块下的index.tsx文件修改),此处暂不作调整
request={async (params, sort: Record<string, SortOrder>, filter: Record<string, React.ReactText[] | null>) => {
const res: any = await listInterfaceInfoByPageUsingGET({
...params
})
// 如果后端请求给你返回了接口信息
if (res?.data) {
// 返回一个包含数据、成功状态和总数的对象
return {
data: res?.data.records || [],
success: true,
total: res?.data.total || 0,
};
} else {
// 如果数据不存在,返回一个空数组,失败状态和零总数
return {
data: [],
success: false,
total: 0,
};
}
}}
- 启动项目,检查调整效果
【2】样式更换
- 脚手架提供的页面上的样式更换按钮可以自己选择,点击设置样式,选择自己喜欢的样式配置进行更改,选择完成【拷贝样式】
- 随后在代码中进行配置(拷贝完成,在src/config/defaultSettings.js 进行配置)
const Settings: ProLayoutProps & {
pwa?: boolean;
logo?: string;
} = {
navTheme: 'light',
layout: "top",
contentWidth: "Fixed",
fixedHeader: false,
fixSiderbar: true,
colorPrimary: "#FAAD14",
splitMenus: false,
title: 'API-Platform',
pwa: true,
logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
iconfontUrl: '',
token: {
// 参见ts声明,demo 见文档,通过token 修改样式
//https://procomponents.ant.design/components/layout#%E9%80%9A%E8%BF%87-token-%E4%BF%AE%E6%94%B9%E6%A0%B7%E5%BC%8F
},
};
- 页面效果
新增添加功能
实现新增功能:相应调整页面的按钮信息使得其适配要开发的功能
开发流程:了解一个新增功能的实现,从按钮入手,点击【新建】触发模态框组件,随后通过填充模态框信息,点击按钮发送请求,完成新增操作
如果不好定位代码,则通过前端页面跳转流程进行跟踪,例如此处的新建,点击后弹出模块框,然后根据关键信息定位代码块,可以看到【新建】模态框被定义在index.tsx中,为了更好进行管理,此处可以参考修改操作,单独将新建新建模态框拆出来,然后再引入,便于后续维护
- 新建CreateModal.tsx文件(也可以从UpdateForm.tsx里copy一份),填充下列信息(主要用于封装【新建模态框】)(原修改逻辑比较复杂,此处需要简化,只需要关注【新建模态框】需要什么内容即可)
思考:在使用模态框组件的地方传递一个columns,为什么需要 columns 呢?
因为在这个模态框中,会有很多需要用户填写的表单项。然而,这些信息其实完全可以从 columns 参数中获取,例如让用户填写接口名称、描述等,没必要重复编写这些信息。
import type { ProColumns } from '@ant-design/pro-components';
import { ProTable } from '@ant-design/pro-components';
import '@umijs/max';
import { Modal } from 'antd';
import React from 'react';
export type Props = {
columns: ProColumns<API.InterfaceInfo>[];
// 当用户点击取消按钮时触发
onCancel: () => void;
// 当用户提交表单时,将用户输入的数据作为参数传递给后台
onSubmit: (values: API.InterfaceInfo) => Promise<void>;
// 模态框是否可见
visible: boolean;
// values不用传递
// values: Partial<API.RuleListItem>;
};
const CreateModal: React.FC<Props> = (props) => {
// 使用解构赋值获取props中的属性
const { visible, columns, onCancel, onSubmit } = props;
return (
// 创建一个Modal组件,通过visible属性控制其显示或隐藏,footer设置为null把表单项的'取消'和'确认'按钮去掉
<Modal visible={visible} footer={null} onCancel={() => onCancel?.()}>
{/* 创建一个ProTable组件,设定它为表单类型,通过columns属性设置表格的列,提交表单时调用onSubmit函数 */}
<ProTable
type="form"
columns={columns}
onSubmit={async (value) => {
onSubmit?.(value);
}}
/>
</Modal>
);
};
export default CreateModal;
- 在index.tsx中引入【CreateModal.tsx】(可以参考修改模态框的实现)
// 1.引入CreateModal.tsx组件
// 接入自定义模态框或组件
import CreateModal from './components/CreateModal';
// 2.构建模态框
{/* 创建一个CreateModal组件,用于在点击新增按钮时弹出 */}
<CreateModal
columns={columns}
// 当取消按钮被点击时,设置更新模态框为false以隐藏模态窗口
onCancel={() => {
handleModalOpen(false);
}}
// 当用户点击提交按钮之后,调用handleAdd函数处理提交的数据,去请求后端添加数据(这里的报错不用管,可能里面组件的属性和外层的不一致)
onSubmit={(values) => {
handleAdd(values);
}}
// 根据更新窗口的值决定模态窗口是否显示
visible={createModalOpen}
/>
随后启动测试,查看模态框是否正常引入
由图示可以看到,如果想要取消创建、更新时间(这两个是系统自动生成的),则需相应配置。前面配置中提到这些属性都是从表单中继承过来的,因此此处则在相应模块的index.tsx配置属性的hideInForm配置,让其隐藏
- 后端接口接入:完成上述配置,则可进一步调用新增接口完成操作(通过新增模态框的【提交】按钮定位代码实现),相应修改onSubmit提交时触发的操作
# 修改新增接口信息提交的触发操作
需注意常量定义的先后,如果定义的常量放在使用后面则可能会提示没有定义,因此一般将方法操作放在后面
- 至此,再次测试新建功能(输入表单项):提示新建成功随后模态框关闭,数据并没有正常插入,进一步定位问题,发现后台校验出错
可通过设置断点排查出错:例如此处后台校验到传入前端的name为空,因此检查添加表单中是否有name属性,由于表单属性是继承表格内容的,因此如果表格中没有name属性,则表单也不会有,因此在表格定义中加入name属性显示即可
BUG修复:表格中要有表单所必须的属性定义,才能给表单正常继承,以此类推,相应的要填充好其他属性(尤其检查与后端交互的属性内容)
除此之外,一些系统数据库设计中指定的属性也要尤其注意(例如此处前端没有指定requestParams,而后台数据库限制其非空则会报错),解决方案最好是前端补充字段(尽量避免数据库层面的变更),数据库设计必须严谨,避免开发过程中频繁维护(从命名、类型、长度等都要精细考虑)
- 再次调试onSubmit调用的handleAdd方法,修改前端校验逻辑
// 把参数的类型改成InterfaceInfo
const handleAdd = async (fields: API.InterfaceInfo) => {
const hide = message.loading('正在添加');
try {
// 把addRule改成addInterfaceInfoUsingPOST
await addInterfaceInfoUsingPost({
...fields,
});
hide();
// 如果调用成功会提示'创建成功'
message.success('创建成功');
// 创建成功就关闭这个模态框
handleModalOpen(false);
return true;
} catch (error: any) {
hide();
// 否则提示'创建失败' + 报错信息
message.error('创建失败,' + error.message);
return false;
}
};
// 把参数的类型改成InterfaceInfo
const handleAdd = async (fields: API.InterfaceInfo) => {
const hide = message.loading('正在添加');
try {
// 把addRule改成addInterfaceInfoUsingPOST
await addInterfaceInfoUsingPost({
...fields,
});
hide();
// 如果调用成功会提示'创建成功'
message.success('创建成功');
// 创建成功就关闭这个模态框
handleModalOpen(false);
return true;
} catch (error: any) {
hide();
// 否则提示'创建失败' + 报错信息
message.error('创建失败,' + error.message);
return false;
}
};
- 完成上述操作,测试新增接口
【4】模块细节完善
(1)前端响应拦截器细化处理
调整requestConfig.ts配置文件,在全局响应拦截器中添加判断,并清理掉默认的一些无关的配置,主要以自身项目为定位参考
# 优化后的requestConfig.ts文件(注意后台连接端口配置)
import type { RequestOptions } from '@@/plugin-request/request';
import type { RequestConfig } from '@umijs/max';
import { message, notification } from 'antd';
// 错误处理方案: 错误类型
enum ErrorShowType {
SILENT = 0,
WARN_MESSAGE = 1,
ERROR_MESSAGE = 2,
NOTIFICATION = 3,
REDIRECT = 9,
}
// 与后端约定的响应数据格式
interface ResponseStructure {
success: boolean;
data: any;
errorCode?: number;
errorMessage?: string;
showType?: ErrorShowType;
}
/**
* @name 错误处理
* pro 自带的错误处理, 可以在这里做自己的改动
* @doc https://umijs.org/docs/max/request#配置
*/
export const requestConfig: RequestConfig = {
// 后台数据交互url定义
baseURL:'http://localhost:8101',
// 设定跨域请求是否要携带cookies
withCredentials:true,
// 请求拦截器
requestInterceptors: [
(config: RequestOptions) => {
// 拦截请求配置,进行个性化处理。
const url = config?.url?.concat('?token = 123');
return { ...config, url };
},
],
// 响应拦截器
responseInterceptors: [
(response) => {
// 拦截响应数据,进行个性化处理
const { data } = response as unknown as ResponseStructure;
// 打印响应数据用于调试
console.log('data', data);
// 当响应的状态码不为0,抛出错误
if (data.code !== 0) {
throw new Error(data.message);
}
// 如果一切正常,返回原始的响应数据
return response;
},
]
};
(2)新增数据表单校验
参考前端校验表单项官方文档说明,有两种方式
- 方式1:在接口名称表单项添加 formItemProps,设定
required:true
- 方式2:在 formItemProps 里增加 rules 进行校验,在 rules 里可以写一个数组来定义多条规则(业务提示、前端校验)
(3)修改功能开发
- 以此类推,参考新增模块开发,新建一个UpdateModal.tsx,编辑模态框内容
import type { ProColumns, ProFormInstance } from '@ant-design/pro-components';
import { ProTable } from '@ant-design/pro-components';
import '@umijs/max';
import { Modal } from 'antd';
import React, { useEffect, useRef } from 'react';
// 定义组件的属性类型
export type Props = {
// 表单中需要编辑的数据
values: API.InterfaceInfo;
// 表格的列定义
columns: ProColumns<API.InterfaceInfo>[];
// 当用户点击取消按钮时触发
onCancel: () => void;
// 当用户提交表单时,将用户输入的数据作为参数传递给后台
onSubmit: (values: API.InterfaceInfo) => Promise<void>;
// 控制模态框是否可见
visible: boolean;
};
// 定义更新模态框组件
const UpdateModal: React.FC<Props> = (props) => {
// 从props中获取属性
const { values, visible, columns, onCancel, onSubmit } = props;
// 使用React的useRef创建一个引用,以访问ProTable中的表单实例
const formRef = useRef<ProFormInstance>();
// 防止修改的表单内容一直是同一个内容,要监听values的变化
// 使用React的useEffect在值改变时更新表单的值
useEffect(() => {
if (formRef) {
formRef.current?.setFieldsValue(values);
}
}, [values]);
// 返回模态框组件
return (
// 创建一个Modal组件,通过visible属性控制其显示或隐藏,footer设置为null把表单项的'取消'和'确认'按钮去掉
<Modal visible={visible} footer={null} onCancel={() => onCancel?.()}>
{/* 创建一个ProTable组件,设定它为表单类型,将表单实例绑定到ref,通过columns属性设置表格的列,提交表单时调用onSubmit函数 */}
<ProTable
type="form"
formRef={formRef}
columns={columns}
onSubmit={async (value) => {
onSubmit?.(value);
}}
/>
</Modal>
);
};
export default UpdateModal;
- 原有index.tsx已经有引入外部修改模态框的定义,因此此处对照将UpdateForm修改为UpdateModal(尽量不要全局替换,而是要理解操作的核心)
# 1.引入修改模态框
import UpdateModal from './components/UpdateModal';
# 2.将原来的UpdateForm替换为下面的内容(此处需注意如果仅仅只是替换文本则有可能忽略其中的实现导致项目报错,因此要理解操作核心而不是只会CV)
<UpdateModal
onSubmit={async (value) => {
const success = await handleUpdate(value);
if (success) {
handleUpdateModalOpen(false);
setCurrentRow(undefined);
if (actionRef.current) {
actionRef.current.reload();
}
}
}}
onCancel={() => {
handleUpdateModalOpen(false);
if (!showDetail) {
setCurrentRow(undefined);
}
}}
// 传递信息修改为visible(这个对应UpdateModal中定义)
visible={ updateModalOpen }
values={currentRow || {}}
/>
- 修改更新节点:调整为对接后台模块
const handleUpdate = async (fields: FormValueType) => {
const hide = message.loading('修改中...');
try {
// 调用后台接口执行修改操作
await updateInterfaceInfoUsingPost({
...fields,
});
hide();
// 调用成功提示信息
message.success('修改成功');
return true;
} catch (error:any) {
hide();
// 否则提示操作失败+报错信息
message.error('操作失败,'+error.message);
return false;
}
};
完成上述操作,测试修改模态框能够正常启动,与后台接口进行交互
- 基于上述操作,随后模态框正常响应,但是修改数据是要根据数据id进行操作的,否则后台接口响应就会报错,因此此处需做相应调整
由前面的操作可知,模态框定义继承了表单的属性,按理来说是可以拿到表单数据的(包括id等信息),这点可以通过打印验证
首先要理解操作的流程,然后再去分析代码。此处用户点击提交之后触发onSubmit方法,这个方法中执行了handleUpdate方法并传递了一个value参数(这个参数实际上就是来自内部组件传递的值),因此再去定位到内部组件(UpdateModal)在哪里定义了这个columns
如果说需要保存用户当前选中的数据项id,此处有个currentRow
选项,直接根据这个获取id即可,替换为InterfaceInfo,然后处理handleUpdate方法中进行数据填充即可(注意定义和引用的先后顺序)
请求修改,此处发现数据修改操作插入中文失败(可能是数据库创建的时候一些字段定义问题,后期待调整)
(4)删除功能开发
- 在表单项中添加一个删除按钮
- 修改handleRemove方法(相应地要注意变量的定义和应用顺序,避免使用变量报未定义错误)
# 1.引入删除接口
import { deleteInterfaceInfoUsingPost} from '@/services/api-platform-backend/interfaceInfoController';
# 2.修改handleRemove方法
const handleRemove = async (record: API.InterfaceInfo) => {
// 设置加载中的提示为'正在删除'
const hide = message.loading('正在删除');
if (!record) return true;
try {
// 把removeRule改成deleteInterfaceInfoUsingPOST
await deleteInterfaceInfoUsingPost({
// 拿到id就能删除数据
id: record.id
});
hide();
// 如果调用成功会提示'删除成功'
message.success('删除成功');
// 删除成功自动刷新表单
actionRef.current?.reload();
return true;
} catch (error: any) {
hide();
// 否则提示'删除失败' + 报错信息
message.error('删除失败,' + error.message);
return false;
}
};
- 测试接口模块
完成上述操作,基本构建了接口信息管理的CRUD模块,存在部分细节待完善,目前主要先处理业务流程,打通一个API调用平台的核心功能,后续再进一步完善业务流程