全套SpringBoot讲义01-二https://developer.aliyun.com/article/1491507
7.表现层消息一致性处理
目前我们通过Postman测试后业务层接口功能时通的,但是这样的结果给到前端开发者会出现一个小问题。不同的操作结果所展示的数据格式差异化严重
增删改操作结果
true
查询单个数据操作结果
{ "id": 1, "type": "计算机理论", "name": "Spring实战 第5版", "description": "Spring入门经典教程" }
查询全部数据操作结果
[ { "id": 1, "type": "计算机理论", "name": "Spring实战 第5版", "description": "Spring入门经典教程" }, { "id": 2, "type": "计算机理论", "name": "Spring 5核心原理与30个类手写实战", "description": "十年沉淀之作" } ]
每种不同操作返回的数据格式都不一样,而且还不知道以后还会有什么格式,这样的结果让前端人员看了是很容易让人崩溃的,必须将所有操作的操作结果数据格式统一起来,需要设计表现层返回结果的模型类,用于后端与前端进行数据格式统一,也称为前后端数据协议
@Data public class R { private Boolean flag; private Object data; }
其中flag用于标识操作是否成功,data用于封装操作数据,现在的数据格式就变了
避免不知道null是异常还是查询的数据不存在,为false则表示抛出异常了
{{ "flag": true, "data":{ "id": 1, "type": "计算机理论", "name": "Spring实战 第5版", "description": "Spring入门经典教程" } }
表现层开发格式也需要转换一下
结果这么一折腾,全格式统一,现在后端发送给前端的数据格式就统一了,免去了不少前端解析数据的麻烦。
总结
- 设计统一的返回值结果类型便于前端开发读取数据
- 返回值结果类型可以根据需求自行设定,没有固定格式
- 返回值结果模型类用于后端与前端进行数据格式统一,也称为前后端数据协议
8.前后端联通性测试
后端的表现层接口开发完毕,就可以进行前端的开发了。
将前端人员开发的页面保存到lresources目录下的static目录中,建议执行maven的clean生命周期,避免缓存的问题出现。
在进行具体的功能开发之前,先做联通性的测试,通过页面发送异步提交(axios),这一步调试通过后再进行进一步的功能开发
//列表 getAll() { axios.get("/books").then((res)=>{ console.log(res.data); }); },
只要后台代码能够正常工作,前端能够在日志中接收到数据,就证明前后端是通的,也就可以进行下一步的功能开发了
总结
单体项目中页面放置在resources/static目录下
created钩子函数用于初始化页面时发起调用
页面使用axios发送异步请求获取数据后确认前后端是否联通
9.页面基础功能开发
F-1.列表功能(非分页版)
列表功能主要操作就是加载完数据,将数据展示到页面上,此处要利用VUE的数据模型绑定,发送请求得到数据,然后页面上读取指定数据即可
页面数据模型定义
data:{ dataList: [],//当前页要展示的列表数据 ... },
异步请求获取数据
//列表 getAll() { axios.get("/books").then((res)=>{ this.dataList = res.data.data; }); },
这样在页面加载时就可以获取到数据,并且由VUE将数据展示到页面上了
总结:
将查询数据返回到页面,利用前端数据绑定进行数据展示
F-2.添加功能
添加功能用于收集数据的表单是通过一个弹窗展示的,因此在添加操作前首先要进行弹窗的展示,添加后隐藏弹窗即可。因为这个弹窗一直存在,因此当页面加载时首先设置这个弹窗为不可显示状态,需要展示,切换状态即可默认状态
data:{ dialogFormVisible: false,//添加表单是否可见 ... },
切换为显示状态
//弹出添加窗口 handleCreate() { this.dialogFormVisible = true; },
由于每次添加数据都是使用同一个弹窗录入数据,所以每次操作的痕迹将在下一次操作时展示出来,需要在每次操作之前清理掉上次操作的痕迹
定义清理数据操作
//重置表单 resetForm() { this.formData = {}; },
切换弹窗状态时清理数据
//弹出添加窗口 handleCreate() { this.dialogFormVisible = true; this.resetForm(); },
至此准备工作完成,下面就要调用后台完成添加操作了
添加操作
//添加 handleAdd () { //发送异步请求 axios.post("/books",this.formData).then((res)=>{ //如果操作成功,关闭弹层,显示数据 if(res.data.flag){ this.dialogFormVisible = false; this.$message.success("添加成功"); }else { this.$message.error("添加失败"); } }).finally(()=>{ this.getAll(); }); },
将要保存的数据传递到后台,通过post请求的第二个参数传递json数据到后台
根据返回的操作结果决定下一步操作
如何是true就关闭添加窗口,显示添加成功的消息
如果是false保留添加窗口,显示添加失败的消息
无论添加是否成功,页面均进行刷新,动态加载数据(对getAll操作发起调用)
取消添加操作
//取消 cancel(){ this.dialogFormVisible = false; this.$message.info("操作取消"); },
总结
- 请求方式使用POST调用后台对应操作
- 添加操作结束后动态刷新页面加载数据
- 根据操作结果不同,显示对应的提示信息
- 弹出添加Div时清除表单数据
F-3.删除功能
模仿添加操作制作删除功能,差别之处在于删除操作仅传递一个待删除的数据id到后台即可
删除操作
// 删除 handleDelete(row) { axios.delete("/books/"+row.id).then((res)=>{ if(res.data.flag){ this.$message.success("删除成功"); }else{ this.$message.error("删除失败"); } }).finally(()=>{ this.getAll(); }); },
删除操作提示信息
// 删除 handleDelete(row) { //1.弹出提示框 this.$confirm("此操作永久删除当前数据,是否继续?","提示",{ type:'info' }).then(()=>{ //2.做删除业务 axios.delete("/books/"+row.id).then((res)=>{ if(res.data.flag){ this.$message.success("删除成功"); }else{ this.$message.error("删除失败"); } }).finally(()=>{ this.getAll(); }); }).catch(()=>{ //3.取消删除 this.$message.info("取消删除操作"); }); },
总结
请求方式使用Delete调用后台对应操作
删除操作需要传递当前行数据对应的id值到后台
删除操作结束后动态刷新页面加载数据
根据操作结果不同,显示对应的提示信息
删除操作前弹出提示框避免误操作
F-4.修改功能
修改功能可以说是列表功能、删除功能与添加功能的合体。几个相似点如下:
页面也需要有一个弹窗用来加载修改的数据,这一点与添加相同,都是要弹窗
弹出窗口中要加载待修改的数据,而数据需要通过查询得到,这一点与查询全部相同,都是要查数据
查询操作需要将要修改的数据id发送到后台,这一点与删除相同,都是传递id到后台
查询得到数据后需要展示到弹窗中,这一点与查询全部相同,都是要通过数据模型绑定展示数据
修改数据时需要将被修改的数据传递到后台,这一点与添加相同,都是要传递数据
所以整体上来看,修改功能就是前面几个功能的大合体
- 查询并展示数据
//弹出编辑窗口 handleUpdate(row) { axios.get("/books/"+row.id).then((res)=>{ if(res.data.flag){ //展示弹层,加载数据 this.formData = res.data.data; this.dialogFormVisible4Edit = true; }else{ this.$message.error("数据同步失败,自动刷新"); } }); },
修改操作
//修改 handleEdit() { axios.put("/books",this.formData).then((res)=>{ //如果操作成功,关闭弹层并刷新页面 if(res.data.flag){ this.dialogFormVisible4Edit = false; this.$message.success("修改成功"); }else { this.$message.error("修改失败,请重试"); } }).finally(()=>{ this.getAll(); }); },
总结
加载要修改数据通过传递当前行数据对应的id值到后台查询数据(同删除与查询全部)
利用前端双向数据绑定将查询到的数据进行回显(同查询全部)
请求方式使用PUT调用后台对应操作(同新增传递数据)
修改操作结束后动态刷新页面加载数据(同新增)
根据操作结果不同,显示对应的提示信息(同新增)
10.业务消息一致性处理
目前的功能制作基本上达成了正常使用的情况,什么叫正常使用呢?也就是这个程序不出BUG,如果我们搞一个BUG出来,你会发现程序马上崩溃掉。比如后台手工抛出一个异常,看看前端接收到的数据什么样子
{ "timestamp": "2021-09-15T03:27:31.038+00:00", "status": 500, "error": "Internal Server Error", "path": "/books" }
面对这种情况,前端的同学又不会了,这又是什么格式?怎么和之前的格式不一样?
{ "flag": true, "data":{ "id": 1, "type": "计算机理论", "name": "Spring实战 第5版", "description": "Spring入门经典教程" } }
看来不仅要对正确的操作数据格式做处理,还要对错误的操作数据格式做同样的格式处理
首先在当前的数据结果中添加消息字段,用来兼容后台出现的操作消息
@Data public class R{ private Boolean flag; private Object data; private String msg; //用于封装消息 }
后台代码也要根据情况做处理,当前是模拟的错误
@PostMapping public R save(@RequestBody Book book) throws IOException { Boolean flag = bookService.insert(book); return new R(flag , flag ? "添加成功^_^" : "添加失败-_-!"); }
然后在表现层做统一的异常处理,使用SpringMVC提供的异常处理器做统一的异常处理
@RestControllerAdvice public class ProjectExceptionAdvice { @ExceptionHandler(Exception.class) public R doOtherException(Exception ex){ //记录日志 //发送消息给运维 //发送邮件给开发人员,ex对象发送给开发人员 ex.printStackTrace(); return new R(false,null,"系统错误,请稍后再试!"); } }
页面上得到数据后,先判定是否有后台传递过来的消息,标志就是当前操作是否成功,如果返回操作结果false,就读取后台传递的消息
//添加 handleAdd () { //发送ajax请求 axios.post("/books",this.formData).then((res)=>{ //如果操作成功,关闭弹层,显示数据 if(res.data.flag){ this.dialogFormVisible = false; this.$message.success("添加成功"); }else { this.$message.error(res.data.msg); //消息来自于后台传递过来,而非固定内容 } }).finally(()=>{ this.getAll(); }); },
总结
- 使用注解@RestControllerAdvice定义SpringMVC异常处理器用来处理异常的
- 异常处理器必须被扫描加载,否则无法生效
- 表现层返回结果的模型类中添加消息属性用来传递消息到页面
11.页面功能开发
F-5.分页功能
分页功能的制作用于替换前面的查询全部,其中要使用到elementUI提供的分页组件
<!--分页组件--> <div class="pagination-container"> <el-pagination class="pagiantion" @current-change="handleCurrentChange" :current-page="pagination.currentPage" :page-size="pagination.pageSize" layout="total, prev, pager, next, jumper" :total="pagination.total"> </el-pagination> </div>
为了配合分页组件,封装分页对应的数据模型
data:{ pagination: { //分页相关模型数据 currentPage: 1, //当前页码 pageSize:10, //每页显示的记录数 total:0, //总记录数 } },
修改查询全部功能为分页查询,通过路径变量传递页码信息参数
getAll() { axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize).then((res) => { }); },
后台提供对应的分页功能
@GetMapping("/{currentPage}/{pageSize}") public R getAll(@PathVariable Integer currentPage,@PathVariable Integer pageSize){ IPage<Book> pageBook = bookService.getPage(currentPage, pageSize); return new R(null != pageBook ,pageBook); }
页面根据分页操作结果读取对应数据,并进行数据模型绑定
getAll() { axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize).then((res) => { this.pagination.total = res.data.data.total; this.pagination.currentPage = res.data.data.current; this.pagination.pagesize = res.data.data.size; this.dataList = res.data.data.records; }); },
对切换页码操作设置调用当前分页操作
//切换页码 handleCurrentChange(currentPage) { this.pagination.currentPage = currentPage; this.getAll(); },
总结
使用el分页组件
定义分页组件绑定的数据模型
异步调用获取分页数据
分页数据页面回显
F-6.删除功能维护
由于使用了分页功能,当最后一页只有一条数据时,删除操作就会出现BUG,最后一页无数据但是独立展示,对分页查询功能进行后台功能维护,如果当前页码值大于最大页码值,重新执行查询。其实这个问题解决方案很多,这里给出比较简单的一种处理方案
@GetMapping("{currentPage}/{pageSize}") public R getPage(@PathVariable int currentPage,@PathVariable int pageSize){ IPage<Book> page = bookService.getPage(currentPage, pageSize); //如果当前页码值大于了总页码值,那么重新执行查询操作,使用最大页码值作为当前页码值 if( currentPage > page.getPages()){ page = bookService.getPage((int)page.getPages(), pageSize); } return new R(true, page); }
F-7.条件查询功能
最后一个功能来做条件查询,其实条件查询可以理解为分页查询的时候除了携带分页数据再多带几个数据的查询。这些多带的数据就是查询条件。比较一下不带条件的分页查询与带条件的分页查询差别之处,这个功能就好做了
页面封装的数据:带不带条件影响的仅仅是一次性传递到后台的数据总量,由传递2个分页相关的数据转换成2个分页数据加若干个条件
后台查询功能:查询时由不带条件,转换成带条件,反正不带条件的时候查询条件对象使用的是null,现在换成具体条件,差别不大
查询结果:不管带不带条件,出来的数据只是有数量上的差别,其他都差别,这个可以忽略
经过上述分析,看来需要在页面发送请求的格式方面做一定的修改,后台的调用数据层操作时发送修改,其他没有区别
页面发送请求时,两个分页数据仍然使用路径变量,其他条件采用动态拼装url参数的形式传递
页面封装查询条件字段
pagination: { //分页相关模型数据 currentPage: 1, //当前页码 pageSize:10, //每页显示的记录数 total:0, //总记录数 name: "", type: "", description: "" },
页面添加查询条件字段对应的数据模型绑定名称
<div class="filter-container"> <el-input placeholder="图书类别" v-model="pagination.type" class="filter-item"/> <el-input placeholder="图书名称" v-model="pagination.name" class="filter-item"/> <el-input placeholder="图书描述" v-model="pagination.description" class="filter-item"/> <el-button @click="getAll()" class="dalfBut">查询</el-button> <el-button type="primary" class="butT" @click="handleCreate()">新建</el-button> </div>
将查询条件组织成url参数,添加到请求url地址中,这里可以借助其他类库快速开发,当前使用手工形式拼接,降低学习要求
getAll() { //1.获取查询条件,拼接查询条件 param = "?name="+this.pagination.name; param += "&type="+this.pagination.type; param += "&description="+this.pagination.description; console.log("-----------------"+ param); axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize+param).then((res) => { this.dataList = res.data.data.records; }); },
后台代码中定义实体类封查询条件
@GetMapping("{currentPage}/{pageSize}") public R getAll(@PathVariable int currentPage,@PathVariable int pageSize,Book book) { System.out.println("参数=====>"+book); IPage<Book> pageBook = bookService.getPage(currentPage,pageSize); return new R(null != pageBook ,pageBook); }
对应业务层接口与实现类进行修正
public interface IBookService extends IService<Book> { IPage<Book> getPage(Integer currentPage,Integer pageSize,Book queryBook); }
@Service public class BookServiceImpl2 extends ServiceImpl<BookDao,Book> implements IBookService { public IPage<Book> getPage(Integer currentPage,Integer pageSize,Book queryBook){ IPage page = new Page(currentPage,pageSize); LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<Book>(); lqw.like(Strings.isNotEmpty(queryBook.getName()),Book::getName,queryBook.getName()); lqw.like(Strings.isNotEmpty(queryBook.getType()),Book::getType,queryBook.getType()); lqw.like(Strings.isNotEmpty(queryBook.getDescription()),Book::getDescription,queryBook.getDescription()); return bookDao.selectPage(page,lqw); } }
页面回显数据
getAll() { //1.获取查询条件,拼接查询条件 param = "?name="+this.pagination.name; param += "&type="+this.pagination.type; param += "&description="+this.pagination.description; console.log("-----------------"+ param); axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize+param).then((res) => { this.pagination.total = res.data.data.total; this.pagination.currentPage = res.data.data.current; this.pagination.pagesize = res.data.data.size; this.dataList = res.data.data.records; }); },
总结
- 定义查询条件数据模型(当前封装到分页数据模型中)
- 异步调用分页功能并通过请求参数传递数据到后台
基础篇完结
基础篇到这里就全部结束了,在基础篇中带着大家学习了如果创建一个SpringBoot工程,然后学习了SpringBoot的基础配置语法格式,接下来对常见的市面上的实用技术做了整合,最后通过一个小的案例对前面学习的内容做了一个综合应用。整体来说就是一个最基本的入门,关于SpringBoot的实际开发其实接触的还是很少的,我们到实用篇和原理篇中继续吧,各位小伙伴,加油学习,再见。