前言
上节我们简单完成了项目的创建,原本计划这节直接写登录模块,但是发现在写业务代码之前还有一些需要提前准备的内容,接口返回信息的封装内容又比较多,所以单独将接口返回信息的封装做为一节。
为什么要封装接口返回信息
从个人开发经验来看,以前项目都是前后端一体,前端通过jsp、freemarker等技术实现,所以基本就是前端需要什么直接返回对应的html片段。但在前后端分离的项目中,前端所需的数据都要通过请求后端接口获取,然后前端在将数据填充到dom中,这种情况下前端就需要判断请求的数据是正常数据还是异常数据,正常就填充到dom中,如果异常则给出提示等。
后端接口返回信息封装
实现状态码枚举类HttpCode,这里主要定义几类常用的状态码及消息,方便前端确认请求返回数据状态。
这里用到了lombok,可以减少很多重复代码:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
在com.ruoxi包下创建constant.enums包,新增HttpCode枚举类,代码如下:
package com.ruoxi.constant.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum HttpCode {
SUCCESS(true, "200", "请求成功"),
FAILED(false, "400", "请求失败"),
SERVER_ERROR(false, "500", "服务器内部错误,请联系管理员"),
NO_LOGIN(false, "401", "未登陆或登陆超时,请重新登陆"),
NO_PERMISSION(false, "403", "用户权限不足");
//响应是否成功
private final Boolean success;
//响应状态码
private final String code;
//响应信息
private final String message;
}
实现接口返回信息封装类HttpResp,该类就是用于封装接口返回信息的类,主要包括当前请求处理状态、消息、返回业务信息等。在com.ruoxi.model.dto包下创建类HttpResp,具体代码如下:
package com.ruoxi.model.dto;
import com.ruoxi.constant.enums.HttpCode;
import lombok.Data;
import java.util.HashMap;
@Data
public class HttpResp {
//响应是否成功
private Boolean success;
//响应状态码
private String code;
//响应信息
private String message;
//响应业务数据
private HashMap<String, Object> data = new HashMap<>();
//无参构造
public HttpResp() {
}
//含参构造方法
public HttpResp(HttpCode httpCode) {
this.success(httpCode.getSuccess()).code(httpCode.getCode()).message(httpCode.getMessage());
}
//成功
public static HttpResp success() {
return new HttpResp(HttpCode.SUCCESS);
}
//失败
public static HttpResp failed() {
return new HttpResp(HttpCode.FAILED);
}
//系统错误
public static HttpResp error() {
return new HttpResp(HttpCode.SERVER_ERROR);
}
//用户未登陆
public static HttpResp noLogin() {
return new HttpResp(HttpCode.NO_LOGIN);
}
//用户无权限
public static HttpResp noPermission() {
return new HttpResp(HttpCode.NO_PERMISSION);
}
//设置响应是否成功
public HttpResp success(Boolean success) {
this.success = success;
return this;
}
//设置响应状态码
public HttpResp code(String code) {
this.code = code;
return this;
}
//设置响应消息
public HttpResp message(String message) {
this.message = message;
return this;
}
//设置响应数据
public HttpResp data(HashMap<String, Object> data) {
this.data = data;
return this;
}
//设置响应数据
public HttpResp data(String key, Object data) {
this.data.put(key, data);
return this;
}
}
基础的封装就做好了,现在使用并测试下响应信息的封装。我们对上节的HelloWorldController的helloword()方法进行改造,实现两个数的加法,并将请结果封装后返回的效果,代码如下:
package com.ruoxi.controller;
import com.ruoxi.model.dto.HttpResp;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloWorldController {
@RequestMapping("/hello")
public HttpResp helloWorld(@RequestParam(value = "a",required = false) Integer a
,@RequestParam(value = "b",required = false) Integer b) {
return HttpResp.success().data("a+b",a+b);
}
}
修改好代码后,重启项目,浏览器访问http://localhost:8080/ruoxi/hello?a=1&b=1,查看效果。
从返回结果中我们就可以看到封装后的效果了,这里data中a+b对应的值2就是我们两个参数的和。小白可能会问,直接返回2不是更简单,封装后这么多内容又有什么用。这里我们继续用这个案例来解释,如果用户不按照我们的要求输入,传入的参数中把b改成了c会怎么?
很明显,这里报错了,从控制台日志可以看到报错内容,就是参数b不见了为null。这样的报错信息展示给用户肯定是不够明确的,即使前端来处理,也不知到发生了什么错误,该给用户提示什么样的内容。所以为了使异常信息更加明确,我们把代码稍微改动一下:
package com.ruoxi.controller;
import com.ruoxi.model.dto.HttpResp;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloWorldController {
@RequestMapping("/hello")
public HttpResp helloWorld(@RequestParam(value = "a",required = false) Integer a
,@RequestParam(value = "b",required = false) Integer b) {
//如果a、b为空,则返回失败
if(null==a||null==b){
return HttpResp.failed().message("参数a、b均不能为空");
}
return HttpResp.success().data("a+b",a+b);
}
}
这里我们加了判断,如果a、b只要为空,就返回错误提示。浏览器访问http://localhost:8080/ruoxi/hello?a=1&c=1,此时前端通过success及code就知道请求返回了异常信息,具体异常原因就在message里,前端像用户展示message的内容,用户就知道怎么回事了。这就是封装返回信息的作用及必要性了。
这里只是简单的举个例子,后面的开发中会有更深刻的体会。
前端请求结果处理
下面通过一个案例来看看前端该怎么优雅处理后端封装后返回的数据。
必要依赖安装及配置
1.UI组件:Element Plus
前端UI组件我们采用Element Plus,控制台输入npm install element-plus,等待安装完成。
将element plus的组件样式、图表库引入,在src/main.js文件中加入以下内容:
//element plus css
import "element-plus/es/components/message/style/css";
import "element-plus/es/components/message-box/style/css";
//element plus icon
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
加入后应该是如下所示:
为了缩小前端项目的体积,我们这里采用按需加载的方式。这里需要安装两个依赖,执行命令 npm install -D unplugin-vue-components unplugin-auto-import ,安装好后在vite.config.js中加入以下代码:
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
plugins: [
vue(),
//elemtn-plus ui组件自动导入
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
],
加入后如图所示:
以上我们就完成了element-plus的安装,可以简单验证一下,修改我们之前创建的HelloWorldView.vue文件,代码如下:
<script setup></script>
<template>
<el-button type="danger"><el-icon><ChatDotRound /></el-icon>Hello World </el-button>
</template>
<style scoped></style>
然后运行项目,浏览器中输入页面对应路径查看效果,这里我们使用了button组件及icon图表,结果如下:
2.网络请求工具:axios
前端我们使用axios作为请求工具,具体用法可以参考官方文档http://www.axios-js.com/。下面我们基于axios进行一些配置,以及通过axios的拦截器处理后端响应数据。
控制台输入 npm install axios ,等待下载完成。
创建文件src/plugins/axios/index.js,写入以下内容:
import axios from "axios";
const http = axios.create({
baseURL: "http://localhost:8080/ruoxi"
});
// axios request拦截器
http.interceptors.request.use(
(config) => {
return config;
},
(error) => {
return Promise.reject(error);
}
);
// axios respone拦截器
http.interceptors.response.use(
(response) => {
const { success, data, message } = response.data;
//请求成功
if (success) {
return Promise.resolve(data);
}
//请求失败
switch(code){
case 401:
//未登录
break;
case 403:
//没有权限
break;
case 404:
//请求资源不存在
break;
case 500:
//服务器内部错误
break;
default:
//其他错误
break;
}
return Promise.reject(message);
},
(error) => {
console.log(error);
const errMsg = error.response ? "系统响应错误(" + error.response.status + ")" : "服务已断开";
return Promise.reject(errMsg);
}
);
export default http;
这里主要配置了后端的项目路径、请求拦截器及响应拦截器,其中响应拦截器就是专门用来处理后端返回的内容。以上代码的解读:通过后端返回的success字段判断当前请求是否成功,成功则将data 数据resolve给调用着,如果失败,则根据错误代码判断是什么错误,并做出相关处理,如reject错误提示给调用者等。
调用后端接口测试
下面我们通过前端实现调用接口计算a+b的结果。为了接口的规范及降低后期维护成本,建议前端接口的定义都放在一个文件夹下统一定义,创建src/api文件夹,之后所有的接口文件都建立在这个文件夹下。创建src/api/HelloWord.js,写入以下代码:
import http from "@/plugins/axios";
//计算a+b
export const hello = ({ a, b }) => {
return http({
url: "/hello",
method: "get",
params: {
a,
b
}
});
};
这里就相当于定义了一个名为hello的函数,参数是a和b,返回的是axios发起的get请求函数。其中url参数就是请求地址,为什么会写/hello而不是完整的后端接口地址http://localhost:8080/ruoxi/hello?这里我们可以注意下前面的axios配置文件中baseURL参数,该参数就是给axios定义了一个基础路径,在发送请求时axios会把baseURL与url参数拼接,当然你也可以直接写完整的接口地址,axios对完整的接口地址不会再用baseURL拼接。所以为了简单高效果,url参数我们直接写/hello就可以了。
定义好了函数,该调用函数发起请求计算a+b的结果了。再HelloWorldView.vue文件中引入hello函数并实现两个参数输入框及提交按钮,完整代码如下:
<script setup>
import { hello } from "../api/HelloWord";
import { ElMessage } from "element-plus";
import { ref } from "vue";
//表单数据
const formData = ref({ a: null, b: null });
//请求结果
const result = ref(null);
//提交按钮操作
const handleSubmit = () => {
hello(formData.value)
.then((res) => {
//请求成功
result.value = res.result;
})
.catch((message) => {
//请求失败
ElMessage.error(message);
});
};
</script>
<template>
<el-form>
<el-form-item label="参数 a">
<el-input v-model="formData.a" placeholder="请输入a"></el-input>
</el-form-item>
<el-form-item label="参数 b">
<el-input v-model="formData.b" placeholder="请输入b"></el-input>
</el-form-item>
<el-form-item label="a+b的结果:">
<span>{{ result }}</span>
</el-form-item>
</el-form>
<el-form-item>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</el-form-item>
</template>
<style scoped></style>
修改后端HelloWorldController的hello接口返回的内容数,HelloWorldController完整代码如下:
package com.ruoxi.controller;
import com.ruoxi.model.dto.HttpResp;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloWorldController {
@RequestMapping("/hello")
public HttpResp helloWorld(@RequestParam(value = "a",required = false) Integer a
,@RequestParam(value = "b",required = false) Integer b) {
//如果a、b为空,则返回失败
if(null==a||null==b){
return HttpResp.failed().message("参数a、b均不能为空");
}
return HttpResp.success().data("result",a+b);
}
}
到这里我们基本就可以测试了,但是当点击提交发起请求的时候会报错,打开浏览器调试器可以看到报错原因,如下:
这是跨域访问接口导致的报错,我们前端地址是http://localhost:5173/#/hello,后端接口地址是http://localhost:8080/ruoxi/hello,两个地址的端口是不一致的,出于接口安全,一般后端接口是不允许这种不同ip或者不同端口(也就是跨域)访问自己的。
我们有两种办法解决这个问题,一是在前端设置代理,让我们发起的请求从8080端口发起,这样前端端就会骗过后端,完成访问;二是在后端进行设置,可通过拦截器或@CrossOrigin注解进行跨域放行。这里我们通过前端的方式去实现,后端对跨域访问放行会存在一定的风险。方式一的实现方法:在vite.config.js中加入以下配置内容:
//api代理配置,仅开发模式有效
server: {
proxy: {
"/api": {
target: "http://localhost:8080",
changeOrigin: true,
rewrite: (path) => path.replace("/api", "/ruoxi")
}
},
open: true
}
根据这里我们设置的代理,axios的baseURL需要修改为/api
const http = axios.create({
//请求基础路径
baseURL: "/api"
});
修改后,重启前端项目,浏览器访问HellWorldView页面,输入参数提交查看结果。
当参数为空时,前端展示报错内容
至此,我们就完成了接口信息的封装,如果你对封装的还有不是很理解的地方,你可以试想以下,上面的案例中如果后端仅返回计算结果而不封装结果,前端该如何处理,什么时候展示数据,什么时候提示错误消息,不是不能实现,而需要花费大量的成本去处理。
关于本项目
项目代码我已经上传之gitee上,如有需要可以进行参考。地址:RuoXi: 从零搭建通用管理系统后台SpringBoot+Vue详细流程案例RuoXi